From 62fe3b57ad31fced7468f79b144c3449510ec000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pasteau?= <4895034+ClementPasteau@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:00:57 +0100 Subject: [PATCH 1/7] Handle new subscription dialog design for new users --- .../res/premium/premium_dialog_background.png | Bin 0 -> 24561 bytes .../PrivateAssetPackInformationPage.js | 1 + newIDE/app/src/Course/CourseChapterView.js | 1 + .../ExportAndShare/ShareDialog/InviteHome.js | 5 +- .../LeaderboardAppearanceDialog.js | 10 +- .../LeaderboardOptionsDialog.js | 5 +- .../MaxLeaderboardCountAlertMessage.js | 1 + .../GameDashboard/Widgets/ServicesWidget.js | 1 + .../MaxProjectCountAlertMessage.js | 1 + .../EducationMarketingSection/index.js | 1 + .../ManageEducationAccountDialog/index.js | 1 + newIDE/app/src/MainFrame/RouterContext.js | 3 +- .../app/src/Profile/CurrentUsageDisplayer.js | 6 +- newIDE/app/src/Profile/RedeemCodeDialog.js | 1 + .../Subscription/GetSubscriptionCard.js | 14 + .../PromotionSubscriptionPlan.js | 469 ++++++++++++++++++ .../PromotionSubscriptionPlan.module.css | 21 + .../SubscriptionOptions.js | 178 +++++++ .../SubscriptionOptions.module.css | 45 ++ .../PromotionSubscriptionDialog/index.js | 238 +++++++++ .../Subscription/SubscriptionChecker.js | 1 + .../Subscription/SubscriptionDetails.js | 1 + .../Subscription/SubscriptionDialog.js | 56 +-- .../Subscription/SubscriptionPendingDialog.js | 13 +- .../SubscriptionSuggestionContext.js | 88 +++- .../app/src/ProjectCreation/AIPromptField.js | 5 +- .../src/ProjectManager/LoadingScreenEditor.js | 9 +- .../src/UI/CustomSvgIcons/ShieldChecked.js | 20 + newIDE/app/src/UI/CustomSvgIcons/ThumbsUp.js | 20 + newIDE/app/src/UI/Dialog.js | 50 +- newIDE/app/src/UI/RaisedButton.js | 2 +- newIDE/app/src/UI/User/UserChip.js | 1 + newIDE/app/src/Utils/Analytics/EventSender.js | 8 +- newIDE/app/src/Utils/UseOpenInitialDialog.js | 26 +- .../src/VersionHistory/UseVersionHistory.js | 1 + .../PromotionSubscriptionDialog.stories.js | 140 ++++++ .../SubscriptionDialog.stories.js | 3 +- .../SubscriptionPendingDialog.stories.js | 4 + .../SubscriptionSuggestionContext.stories.js | 1 + 39 files changed, 1358 insertions(+), 93 deletions(-) create mode 100644 newIDE/app/public/res/premium/premium_dialog_background.png create mode 100644 newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/PromotionSubscriptionPlan.js create mode 100644 newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/PromotionSubscriptionPlan.module.css create mode 100644 newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/SubscriptionOptions.js create mode 100644 newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/SubscriptionOptions.module.css create mode 100644 newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/index.js create mode 100644 newIDE/app/src/UI/CustomSvgIcons/ShieldChecked.js create mode 100644 newIDE/app/src/UI/CustomSvgIcons/ThumbsUp.js create mode 100644 newIDE/app/src/stories/componentStories/Profile/Subscription/PromotionSubscriptionDialog.stories.js diff --git a/newIDE/app/public/res/premium/premium_dialog_background.png b/newIDE/app/public/res/premium/premium_dialog_background.png new file mode 100644 index 0000000000000000000000000000000000000000..8c15fafa4ce7b47657995545a80acef8b3e4bcf8 GIT binary patch literal 24561 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 literal 0 HcmV?d00001 diff --git a/newIDE/app/src/AssetStore/PrivateAssets/PrivateAssetPackInformationPage.js b/newIDE/app/src/AssetStore/PrivateAssets/PrivateAssetPackInformationPage.js index ccad52c0049c..9282415683a5 100644 --- a/newIDE/app/src/AssetStore/PrivateAssets/PrivateAssetPackInformationPage.js +++ b/newIDE/app/src/AssetStore/PrivateAssets/PrivateAssetPackInformationPage.js @@ -635,6 +635,7 @@ const PrivateAssetPackInformationPage = ({ openSubscriptionDialog({ analyticsMetadata: { reason: 'Claim asset pack', + recommendedPlanId: 'gdevelop_gold', }, filter: 'individual', }) diff --git a/newIDE/app/src/Course/CourseChapterView.js b/newIDE/app/src/Course/CourseChapterView.js index 81446fca658e..22f628440168 100644 --- a/newIDE/app/src/Course/CourseChapterView.js +++ b/newIDE/app/src/Course/CourseChapterView.js @@ -285,6 +285,7 @@ const CourseChapterView = React.forwardRef( openSubscriptionDialog({ analyticsMetadata: { reason: 'Unlock course chapter', + recommendedPlanId: 'gdevelop_silver', }, }) } diff --git a/newIDE/app/src/ExportAndShare/ShareDialog/InviteHome.js b/newIDE/app/src/ExportAndShare/ShareDialog/InviteHome.js index 1cef6e0ad8e6..aef54bfed057 100644 --- a/newIDE/app/src/ExportAndShare/ShareDialog/InviteHome.js +++ b/newIDE/app/src/ExportAndShare/ShareDialog/InviteHome.js @@ -323,7 +323,10 @@ const InviteHome = ({ cloudProjectId }: Props) => { {!hasSufficientPermissionsWithSubscription && !!projectUserAcls && fetchError !== 'project-not-owned' && ( - + Get a startup subscription to invite collaborators into your diff --git a/newIDE/app/src/GameDashboard/LeaderboardAdmin/LeaderboardAppearanceDialog.js b/newIDE/app/src/GameDashboard/LeaderboardAdmin/LeaderboardAppearanceDialog.js index a696c645a265..6c30a4caf9b5 100644 --- a/newIDE/app/src/GameDashboard/LeaderboardAdmin/LeaderboardAppearanceDialog.js +++ b/newIDE/app/src/GameDashboard/LeaderboardAdmin/LeaderboardAppearanceDialog.js @@ -347,7 +347,10 @@ function LeaderboardAppearanceDialog({ /> {!canUseTheme ? ( - + @@ -400,7 +403,10 @@ function LeaderboardAppearanceDialog({ disabled={!useCustomCss || isLoading} /> {!canUseCustomCss ? ( - + diff --git a/newIDE/app/src/GameDashboard/LeaderboardAdmin/LeaderboardOptionsDialog.js b/newIDE/app/src/GameDashboard/LeaderboardAdmin/LeaderboardOptionsDialog.js index bb5497b948d1..2e861ec4cb4f 100644 --- a/newIDE/app/src/GameDashboard/LeaderboardAdmin/LeaderboardOptionsDialog.js +++ b/newIDE/app/src/GameDashboard/LeaderboardAdmin/LeaderboardOptionsDialog.js @@ -253,7 +253,10 @@ function LeaderboardOptionsDialog({ } /> {!canDisableLoginInLeaderboard && ( - + diff --git a/newIDE/app/src/GameDashboard/LeaderboardAdmin/MaxLeaderboardCountAlertMessage.js b/newIDE/app/src/GameDashboard/LeaderboardAdmin/MaxLeaderboardCountAlertMessage.js index 543f0ae68012..c98ecf93e033 100644 --- a/newIDE/app/src/GameDashboard/LeaderboardAdmin/MaxLeaderboardCountAlertMessage.js +++ b/newIDE/app/src/GameDashboard/LeaderboardAdmin/MaxLeaderboardCountAlertMessage.js @@ -55,6 +55,7 @@ const MaxLeaderboardCountAlertMessage = () => { ) } hideButton={!leaderboardLimits.canMaximumCountPerGameBeIncreased} + recommendedPlanIdIfNoSubscription="gdevelop_silver" > diff --git a/newIDE/app/src/GameDashboard/Widgets/ServicesWidget.js b/newIDE/app/src/GameDashboard/Widgets/ServicesWidget.js index 7b161d7c10d6..5f9628a2b545 100644 --- a/newIDE/app/src/GameDashboard/Widgets/ServicesWidget.js +++ b/newIDE/app/src/GameDashboard/Widgets/ServicesWidget.js @@ -97,6 +97,7 @@ const ServicesWidget = ({ openSubscriptionDialog({ analyticsMetadata: { reason: 'Leaderboard count per game limit reached', + recommendedPlanId: 'gdevelop_silver', }, }) } diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/MaxProjectCountAlertMessage.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/MaxProjectCountAlertMessage.js index 7448ba102e70..e67aa67a44f2 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/MaxProjectCountAlertMessage.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/MaxProjectCountAlertMessage.js @@ -54,6 +54,7 @@ export const MaxProjectCountAlertMessage = ({ margin }: Props) => { ) } hideButton={!canMaximumCountBeIncreased} + recommendedPlanIdIfNoSubscription="gdevelop_silver" > diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/EducationMarketingSection/index.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/EducationMarketingSection/index.js index f5d7473c14c3..984846a62df8 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/EducationMarketingSection/index.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/EducationMarketingSection/index.js @@ -240,6 +240,7 @@ const EducationMarketingSection = ({ filter: 'education', analyticsMetadata: { reason: 'Callout in Classroom tab', + recommendedPlanId: 'gdevelop_education', }, }); }, diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/TeamSection/ManageEducationAccountDialog/index.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/TeamSection/ManageEducationAccountDialog/index.js index 52195ad77c39..200f02c15f9b 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/TeamSection/ManageEducationAccountDialog/index.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/TeamSection/ManageEducationAccountDialog/index.js @@ -636,6 +636,7 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => { openSubscriptionDialog({ analyticsMetadata: { reason: 'Manage subscription as teacher', + recommendedPlanId: 'gdevelop_education', }, filter: 'education', }) diff --git a/newIDE/app/src/MainFrame/RouterContext.js b/newIDE/app/src/MainFrame/RouterContext.js index b2eaee277fbb..d4ecd979c299 100644 --- a/newIDE/app/src/MainFrame/RouterContext.js +++ b/newIDE/app/src/MainFrame/RouterContext.js @@ -22,7 +22,8 @@ type RouteKey = | 'game-template' | 'tutorial-id' | 'course-id' - | 'create-from-example'; + | 'create-from-example' + | 'recommended-plan-id'; export type RouteArguments = { [RouteKey]: string }; export type Router = {| diff --git a/newIDE/app/src/Profile/CurrentUsageDisplayer.js b/newIDE/app/src/Profile/CurrentUsageDisplayer.js index daf3fb799a98..57af03023cfb 100644 --- a/newIDE/app/src/Profile/CurrentUsageDisplayer.js +++ b/newIDE/app/src/Profile/CurrentUsageDisplayer.js @@ -100,7 +100,7 @@ const CurrentUsageDisplayer = ({ if (!quota || !subscription || !usagePrice) return ; const isFeatureLocked = quota.max === 0; - const hasSubscription = hasValidSubscriptionPlan(subscription); + const hasValidSubscription = hasValidSubscriptionPlan(subscription); const remainingBuilds = Math.max(quota.max - quota.current, 0); const usageRatio = `${quota.current}/${quota.max}`; const remainingMultipleMessage = @@ -132,7 +132,7 @@ const CurrentUsageDisplayer = ({ return ( - {hasSubscription ? ( + {hasValidSubscription ? ( !quota.limitReached ? (
{!isFeatureLocked ? ( @@ -217,6 +218,7 @@ const CurrentUsageDisplayer = ({ onPayWithCredits: onPurchaseBuildWithCredits, } } + recommendedPlanIdIfNoSubscription="gdevelop_silver" > {!isFeatureLocked ? ( diff --git a/newIDE/app/src/Profile/RedeemCodeDialog.js b/newIDE/app/src/Profile/RedeemCodeDialog.js index 2c1dee16e590..ff8405506909 100644 --- a/newIDE/app/src/Profile/RedeemCodeDialog.js +++ b/newIDE/app/src/Profile/RedeemCodeDialog.js @@ -133,6 +133,7 @@ export default function RedeemCodeDialog({ floatingLabelFixed errorText={getRedeemCodeErrorText(error)} autoFocus="desktop" + disabled={isLoading} /> {!subscription || !subscription.planId ? null : !!subscription.redemptionCodeValidUntil ? ( // No subscription, do not show a warning. diff --git a/newIDE/app/src/Profile/Subscription/GetSubscriptionCard.js b/newIDE/app/src/Profile/Subscription/GetSubscriptionCard.js index 8b777c3ddb46..9caa590264cc 100644 --- a/newIDE/app/src/Profile/Subscription/GetSubscriptionCard.js +++ b/newIDE/app/src/Profile/Subscription/GetSubscriptionCard.js @@ -12,6 +12,8 @@ import classes from './GetSubscriptionCard.module.css'; import Paper from '../../UI/Paper'; import CrownShining from '../../UI/CustomSvgIcons/CrownShining'; import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer'; +import AuthenticatedUserContext from '../AuthenticatedUserContext'; +import { hasValidSubscriptionPlan } from '../../Utils/GDevelopServices/Usage'; const styles = { paper: { @@ -42,6 +44,11 @@ type Props = {| onUpgrade?: () => void, forceColumnLayout?: boolean, filter?: 'individual' | 'team' | 'education', + recommendedPlanIdIfNoSubscription?: + | 'gdevelop_silver' + | 'gdevelop_gold' + | 'gdevelop_startup' + | 'gdevelop_education', |}; const GetSubscriptionCard = ({ @@ -53,7 +60,13 @@ const GetSubscriptionCard = ({ onUpgrade, forceColumnLayout, filter, + recommendedPlanIdIfNoSubscription, }: Props) => { + const { subscription } = React.useContext(AuthenticatedUserContext); + const actualPlanIdToRecommend = hasValidSubscriptionPlan(subscription) + ? // If the user already has a subscription, show the original subscription dialog. + undefined + : recommendedPlanIdIfNoSubscription; const { openSubscriptionDialog } = React.useContext( SubscriptionSuggestionContext ); @@ -93,6 +106,7 @@ const GetSubscriptionCard = ({ openSubscriptionDialog({ analyticsMetadata: { reason: subscriptionDialogOpeningReason, + recommendedPlanId: actualPlanIdToRecommend, }, filter, }); diff --git a/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/PromotionSubscriptionPlan.js b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/PromotionSubscriptionPlan.js new file mode 100644 index 000000000000..a36a5c6e060c --- /dev/null +++ b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/PromotionSubscriptionPlan.js @@ -0,0 +1,469 @@ +// @flow +import * as React from 'react'; +import { I18n } from '@lingui/react'; +import Text from '../../../UI/Text'; +import { Column, Line, Spacer } from '../../../UI/Grid'; +import { + type SubscriptionPlanWithPricingSystems, + type SubscriptionPlanPricingSystem, + EDUCATION_PLAN_MAX_SEATS, + EDUCATION_PLAN_MIN_SEATS, +} from '../../../Utils/GDevelopServices/Usage'; +import { selectMessageByLocale } from '../../../Utils/i18n/MessageByLocale'; +import { getPlanIcon } from '../PlanCard'; +import RedemptionCodeIcon from '../../../UI/CustomSvgIcons/RedemptionCode'; +import { + ColumnStackLayout, + LineStackLayout, + ResponsiveLineStackLayout, +} from '../../../UI/Layout'; +import { t, Trans } from '@lingui/macro'; +import Link from '../../../UI/Link'; +import Window from '../../../Utils/Window'; +import FlatButton from '../../../UI/FlatButton'; +import GDevelopThemeContext from '../../../UI/Theme/GDevelopThemeContext'; +import CheckCircleFilled from '../../../UI/CustomSvgIcons/CheckCircleFilled'; +import ShieldChecked from '../../../UI/CustomSvgIcons/ShieldChecked'; +import classes from './PromotionSubscriptionPlan.module.css'; +import Paper from '../../../UI/Paper'; +import RaisedButton from '../../../UI/RaisedButton'; +import LeftLoader from '../../../UI/LeftLoader'; +import { FormControlLabel, Radio, RadioGroup } from '@material-ui/core'; +import DiscountFlame from '../../../UI/HotMessage/DiscountFlame'; +import ThumbsUp from '../../../UI/CustomSvgIcons/ThumbsUp'; +import { useResponsiveWindowSize } from '../../../UI/Responsive/ResponsiveWindowMeasurer'; +import SemiControlledTextField from '../../../UI/SemiControlledTextField'; + +const styles = { + simpleSizeContainer: { + display: 'flex', + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + doubleSizeContainer: { + display: 'flex', + flex: 2, + justifyContent: 'center', + alignItems: 'center', + }, + tableRightItemContainer: { + width: 120, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, + bulletText: { flex: 1 }, + bulletIcon: { width: 20, height: 20 }, + paper: { flex: 1, zIndex: 2, padding: 16 }, + descriptionContainer: { minHeight: 70 }, // Keep height the same for 1 or 2 lines. + discountContainer: { + padding: '4px 8px', + borderRadius: 4, + display: 'flex', + alignItems: 'center', + }, + radioGroup: { flex: 1 }, + formControlLabel: { + borderRadius: 4, + // Override MUI margins + marginLeft: 0, + marginRight: 0, + cursor: 'default', + }, + discountedPrice: { textDecoration: 'line-through' }, +}; + +const formatPriceWithCurrency = (amountInCents: number, currency: string) => { + if (currency === 'USD') { + return `$${amountInCents / 100}`; + } + return `${amountInCents / 100}${currency === 'EUR' ? '€' : currency}`; +}; + +const getYearlyDiscountDisplayText = ( + monthlyPricingSystem: SubscriptionPlanPricingSystem, + yearlyPricingSystem: SubscriptionPlanPricingSystem +): string | null => { + return ( + '-' + + (( + 100 - + (yearlyPricingSystem.amountInCents / + (monthlyPricingSystem.amountInCents * 12)) * + 100 + ).toFixed(0) + + '%') + ); +}; + +const PromotionSubscriptionPlan = ({ + subscriptionPlanWithPricingSystems, + disabled, + onClickRedeemCode, + onClickChoosePlan, + seatsCount, + setSeatsCount, +}: {| + subscriptionPlanWithPricingSystems: SubscriptionPlanWithPricingSystems, + disabled?: boolean, + onClickRedeemCode: () => void, + onClickChoosePlan: ( + pricingSystem: SubscriptionPlanPricingSystem + ) => Promise, + seatsCount: number, + setSeatsCount: (seatsCount: number) => void, +|}) => { + const { windowSize } = useResponsiveWindowSize(); + const isLargeScreen = windowSize === 'large' || windowSize === 'xlarge'; + const gdevelopTheme = React.useContext(GDevelopThemeContext); + const [period, setPeriod] = React.useState<'year' | 'month'>('year'); + + const planIcon = getPlanIcon({ + subscriptionPlan: subscriptionPlanWithPricingSystems, + logoSize: 12, + }); + + const selectedPricingSystem = subscriptionPlanWithPricingSystems.pricingSystems.find( + pricingSystem => pricingSystem.period === period + ); + const yearlyPlanPrice = subscriptionPlanWithPricingSystems.pricingSystems.find( + price => price.period === 'year' + ); + const monthlyPlanPrice = subscriptionPlanWithPricingSystems.pricingSystems.find( + price => price.period === 'month' + ); + + if (!selectedPricingSystem || !yearlyPlanPrice || !monthlyPlanPrice) { + console.error( + 'No pricing system found for period', + period, + 'in', + subscriptionPlanWithPricingSystems + ); + return null; + } + + const yearlyPriceInCentsWithMonthlyPlan = Math.floor( + monthlyPlanPrice.amountInCents * 12 + ); + const yearlyDiscountDisplayText = getYearlyDiscountDisplayText( + monthlyPlanPrice, + yearlyPlanPrice + ); + + return ( + + {({ i18n }) => ( + +
+ +
+ +
+ + {selectMessageByLocale( + i18n, + subscriptionPlanWithPricingSystems.nameByLocale + )}{' '} + +
+
+
+ {subscriptionPlanWithPricingSystems.bulletPointsByLocale.map( + (bulletPointByLocale, index) => ( + +
+ + + {selectMessageByLocale(i18n, bulletPointByLocale)} + +
+ +
+
+
+
+ ) + )} + + + + + Compare all the advantages of the different plans in this{' '} + + Window.openExternalURL( + 'https://gdevelop.io/pricing#feature-comparison' + ) + } + > + big feature comparison table + + . + + + +
+
+
+ +
+ + + + {planIcon} + + + + {selectMessageByLocale( + i18n, + subscriptionPlanWithPricingSystems.nameByLocale + )} + + + + + +
+ {selectMessageByLocale( + i18n, + subscriptionPlanWithPricingSystems.descriptionByLocale + )} +
+
+ {subscriptionPlanWithPricingSystems.id === + 'gdevelop_education' && ( + + Number of seats} + commitOnBlur + type="number" + onChange={value => { + const newValue = parseInt(value); + setSeatsCount( + Math.min( + EDUCATION_PLAN_MAX_SEATS, + Math.max( + Number.isNaN(newValue) + ? EDUCATION_PLAN_MIN_SEATS + : newValue, + EDUCATION_PLAN_MIN_SEATS + ) + ) + ); + }} + min={EDUCATION_PLAN_MIN_SEATS} + max={EDUCATION_PLAN_MAX_SEATS} + step={1} + helperMarkdownText={i18n._( + t`As a teacher, you will use one seat in the plan so make sure to include yourself!` + )} + /> + + )} + + { + setPeriod(event.target.value); + }} + style={styles.radioGroup} + > + + } + label={ + + + + {!yearlyPlanPrice.isPerUser ? ( + + Yearly, + {formatPriceWithCurrency( + yearlyPlanPrice.amountInCents, + yearlyPlanPrice.currency + )} + + ) : ( + + Yearly, + {formatPriceWithCurrency( + yearlyPlanPrice.amountInCents, + yearlyPlanPrice.currency + )}{' '} + per seat + + )} + + + + Instead of{' '} + + {formatPriceWithCurrency( + yearlyPriceInCentsWithMonthlyPlan, + yearlyPlanPrice.currency + )} + + + + + + + + + + {yearlyDiscountDisplayText} + + + + + } + /> + + + } + label={ + + + + {!monthlyPlanPrice.isPerUser ? ( + + Monthly, + {formatPriceWithCurrency( + monthlyPlanPrice.amountInCents, + monthlyPlanPrice.currency + )} + + ) : ( + + Monthly, + {formatPriceWithCurrency( + monthlyPlanPrice.amountInCents, + monthlyPlanPrice.currency + )}{' '} + per seat + + )} + + + + } + /> + + + + + + Choose this plan + + + } + onClick={() => onClickChoosePlan(selectedPricingSystem)} + size="large" + fullWidth + /> + +
+
+
+ + + + Paypal secure + + + + Stripe secure + + + + Cancel anytime + + + } + label={Redeem a code} + key="redeem-code" + disabled={disabled} + onClick={onClickRedeemCode} + primary + /> +
+
+
+ )} +
+ ); +}; + +export default PromotionSubscriptionPlan; diff --git a/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/PromotionSubscriptionPlan.module.css b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/PromotionSubscriptionPlan.module.css new file mode 100644 index 000000000000..45c84f067131 --- /dev/null +++ b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/PromotionSubscriptionPlan.module.css @@ -0,0 +1,21 @@ +.choosePlanContainer { + position: relative; + overflow: hidden; + padding: 3px; + border-radius: 10px; + display: flex; + flex: 1; +} + +.choosePlanContainer::before { + content: ''; + display: block; + background: linear-gradient(90deg, var(--theme-premium-teal) 0%, var(--theme-premium-orange) 100%); + width: 100%; + height: 100%; + padding-bottom: 100%; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} \ No newline at end of file diff --git a/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/SubscriptionOptions.js b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/SubscriptionOptions.js new file mode 100644 index 000000000000..d1605721cee5 --- /dev/null +++ b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/SubscriptionOptions.js @@ -0,0 +1,178 @@ +// @flow +import * as React from 'react'; +import { I18n } from '@lingui/react'; +import ButtonBase from '@material-ui/core/ButtonBase'; +import { shouldValidate } from '../../../UI/KeyboardShortcuts/InteractionKeys'; +import Text from '../../../UI/Text'; +import { Column } from '../../../UI/Grid'; +import { type SubscriptionPlanWithPricingSystems } from '../../../Utils/GDevelopServices/Usage'; +import { selectMessageByLocale } from '../../../Utils/i18n/MessageByLocale'; +import { getPlanIcon } from '../PlanCard'; +import { LineStackLayout } from '../../../UI/Layout'; +import classes from './SubscriptionOptions.module.css'; +import classNames from 'classnames'; +import GDevelopThemeContext from '../../../UI/Theme/GDevelopThemeContext'; +import { Trans } from '@lingui/macro'; +import { useResponsiveWindowSize } from '../../../UI/Responsive/ResponsiveWindowMeasurer'; + +const styles = { + optionsContainer: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(8rem, 1fr))', + gridGap: '0.5rem', + margin: 2, + }, + buttonBase: { + height: '100%', + width: '100%', + borderRadius: 8, + padding: 8, + cursor: 'default', + overflow: 'hidden', + boxSizing: 'border-box', + alignItems: 'flex-start', + }, +}; + +type Props = {| + children: React.Node, + onClick: () => void, + selected: boolean, + recommended: boolean, + disabled?: boolean, +|}; + +const SubscriptionOptionButton = ({ + children, + onClick, + selected, + recommended, + disabled, +}: Props) => { + const { windowSize } = useResponsiveWindowSize(); + const isMobileOrMediumScreen = + windowSize === 'small' || windowSize === 'medium'; + const gdevelopTheme = React.useContext(GDevelopThemeContext); + return ( +
+ {recommended && ( +
+ + {isMobileOrMediumScreen ? ( + Recommended + ) : ( + Recommended for you + )} + +
+ )} +
+ ): void => { + if (shouldValidate(event) && !selected) { + onClick(); + } + }} + disableTouchRipple={selected} // Avoid ripple effect even if already selected. + disabled={disabled} + > + {children} + +
+
+ ); +}; + +const SubscriptionOptions = ({ + subscriptionPlansWithPricingSystems, + selectedPlanId, + recommendedPlanId, + onClick, + disabled, +}: {| + subscriptionPlansWithPricingSystems: SubscriptionPlanWithPricingSystems[], + selectedPlanId: string, + recommendedPlanId: string, + onClick: string => void, + disabled?: boolean, +|}) => { + return ( + + {({ i18n }) => ( +
+ {subscriptionPlansWithPricingSystems.map( + (subscriptionPlanWithPricingSystems, index) => { + const isSelected = + selectedPlanId === subscriptionPlanWithPricingSystems.id; + const isRecommended = + recommendedPlanId === subscriptionPlanWithPricingSystems.id; + return ( + + + onClick(subscriptionPlanWithPricingSystems.id) + } + selected={isSelected} + recommended={isRecommended} + disabled={disabled} + > + + + {getPlanIcon({ + subscriptionPlan: subscriptionPlanWithPricingSystems, + logoSize: 12, + })} + + {selectMessageByLocale( + i18n, + subscriptionPlanWithPricingSystems.nameByLocale + )} + + + + + {selectMessageByLocale( + i18n, + subscriptionPlanWithPricingSystems.descriptionByLocale + )} + + + + + + ); + } + )} +
+ )} +
+ ); +}; + +export default SubscriptionOptions; diff --git a/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/SubscriptionOptions.module.css b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/SubscriptionOptions.module.css new file mode 100644 index 000000000000..8d4e6baebe53 --- /dev/null +++ b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/SubscriptionOptions.module.css @@ -0,0 +1,45 @@ +.optionAndBadgeContainer { + position: relative; + height: 100%; +} + +.optionContainer { + position: relative; + overflow: hidden; + padding: 2px; + border-radius: 10px; + display: flex; + height: 100%; +} + +.optionContainer::before { + content: ''; + display: block; + background: var(--theme-text-disabled-color); + width: 100%; + height: 100%; + padding-bottom: 100%; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} + +.optionContainer:hover::before { + background: var(--theme-text-default-color); +} + +.optionContainer.selected::before { + background: linear-gradient(90deg, var(--theme-premium-teal) 0%, var(--theme-premium-orange) 100%); +} + +.recommendedBadge { + position: absolute; + top: -12px; + right: 10px; + background: var(--theme-message-hot-background-color); + color: var(--theme-message-hot-color); + padding: 2px 8px; + border-radius: 16px; + z-index: 2; +} \ No newline at end of file diff --git a/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/index.js b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/index.js new file mode 100644 index 000000000000..3a98b8f10e1e --- /dev/null +++ b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/index.js @@ -0,0 +1,238 @@ +// @flow +import { Trans } from '@lingui/macro'; +import { I18n } from '@lingui/react'; +import { type I18n as I18nType } from '@lingui/core'; +import * as React from 'react'; +import Dialog from '../../../UI/Dialog'; +import AuthenticatedUserContext from '../../AuthenticatedUserContext'; +import { + getRedirectToCheckoutUrl, + type SubscriptionPlanWithPricingSystems, + type SubscriptionPlanPricingSystem, +} from '../../../Utils/GDevelopServices/Usage'; +import { sendChoosePlanClicked } from '../../../Utils/Analytics/EventSender'; +import { type PurchasablePlanId } from '../SubscriptionSuggestionContext'; +import Window from '../../../Utils/Window'; +import Text from '../../../UI/Text'; +import { ColumnStackLayout, LineStackLayout } from '../../../UI/Layout'; +import RedeemCodeDialog from '../../RedeemCodeDialog'; +import PlaceholderLoader from '../../../UI/PlaceholderLoader'; +import { Column, Line } from '../../../UI/Grid'; +import SubscriptionOptions from './SubscriptionOptions'; +import PromotionSubscriptionPlan from './PromotionSubscriptionPlan'; +import { getPlanSpecificRequirements } from '../SubscriptionDialog'; +import AlertMessage from '../../../UI/AlertMessage'; + +type Props = {| + onClose: Function, + subscriptionPlansWithPricingSystems: ?(SubscriptionPlanWithPricingSystems[]), + recommendedPlanId: PurchasablePlanId, + onOpenPendingDialog: (open: boolean) => void, +|}; + +export default function PromotionSubscriptionDialog({ + onClose, + subscriptionPlansWithPricingSystems, + recommendedPlanId, + onOpenPendingDialog, +}: Props) { + const [isRefreshing, setIsRefreshing] = React.useState(false); + const [ + educationPlanSeatsCount, + setEducationPlanSeatsCount, + ] = React.useState(20); + const [redeemCodeDialogOpen, setRedeemCodeDialogOpen] = React.useState(false); + const authenticatedUser = React.useContext(AuthenticatedUserContext); + + const [ + availableRecommendedPlanId, + setAvailableRecommendedPlanId, + ] = React.useState(recommendedPlanId); + const [selectedPlanId, setSelectedPlanId] = React.useState(recommendedPlanId); + + const buyPlan = async ( + i18n: I18nType, + subscriptionPlanPricingSystem: SubscriptionPlanPricingSystem + ) => { + const { subscription, profile } = authenticatedUser; + if (!profile || !subscription) return; + if (subscription.planId) { + // User already has a subscription, this dialog should not be opened. + return; + } + + sendChoosePlanClicked({ + planId: subscriptionPlanPricingSystem.planId, + pricingSystemId: subscriptionPlanPricingSystem.id, + }); + onOpenPendingDialog(true); + const isEducationPlan = + subscriptionPlanPricingSystem && + subscriptionPlanPricingSystem.planId === 'gdevelop_education'; + const quantity = isEducationPlan ? educationPlanSeatsCount : undefined; + Window.openExternalURL( + getRedirectToCheckoutUrl({ + pricingSystemId: subscriptionPlanPricingSystem.id, + userId: profile.id, + userEmail: profile.email, + quantity, + }) + ); + }; + + const purchasablePlansWithPricingSystems = React.useMemo( + () => + subscriptionPlansWithPricingSystems + ? subscriptionPlansWithPricingSystems.filter( + subscriptionPlanWithPricingSystems => + // Hide free plan + subscriptionPlanWithPricingSystems.pricingSystems.length > 0 + ) + : null, + [subscriptionPlansWithPricingSystems] + ); + + // If the recommended plan is not available, select the first plan. + React.useEffect( + () => { + if (purchasablePlansWithPricingSystems) { + if ( + !purchasablePlansWithPricingSystems.find( + purchasablePlanWithPricingSystems => + purchasablePlanWithPricingSystems.id === recommendedPlanId + ) + ) { + const firstPlanId = purchasablePlansWithPricingSystems[0].id; + setSelectedPlanId(firstPlanId); + setAvailableRecommendedPlanId(firstPlanId); + } + } + }, + [purchasablePlansWithPricingSystems, recommendedPlanId] + ); + + const displayedPlan = React.useMemo( + () => + purchasablePlansWithPricingSystems + ? purchasablePlansWithPricingSystems.find( + purchasablePlanWithPricingSystems => + purchasablePlanWithPricingSystems.id === selectedPlanId + ) + : null, + [purchasablePlansWithPricingSystems, selectedPlanId] + ); + + if (!displayedPlan) { + return null; + } + + const isLoading = isRefreshing; + + return ( + + {({ i18n }) => ( + <> + Get GDevelop Premium} + subtitle={ + + Choose a subscription to enjoy the best of game creation. + + } + open + onRequestClose={onClose} + minHeight="lg" + maxWidth="lg" + flexColumnBody + topBackgroundSrc={'res/premium/premium_dialog_background.png'} + > + + {!purchasablePlansWithPricingSystems ? ( + + ) : ( + + setRedeemCodeDialogOpen(true) + } + subscriptionPlanWithPricingSystems={displayedPlan} + disabled={isLoading} + onClickChoosePlan={async pricingSystem => { + if (!authenticatedUser.authenticated) { + authenticatedUser.onOpenCreateAccountDialog(); + } else { + await buyPlan(i18n, pricingSystem); + } + }} + seatsCount={educationPlanSeatsCount} + setSeatsCount={setEducationPlanSeatsCount} + /> + + + )} + + + + ❤️ + + Support What You Love + + + + + The GDevelop project is open-source, powered by passion + and community. Your membership helps the GDevelop company + maintain servers, build new features, develop commercial + offerings and keep the open-source project thriving. Our + goal: make game development fast, fun and accessible to + all. + + + {getPlanSpecificRequirements( + i18n, + subscriptionPlansWithPricingSystems + ).map(planSpecificRequirements => ( + + {planSpecificRequirements} + + ))} + + + + + {redeemCodeDialogOpen && ( + { + setRedeemCodeDialogOpen(false); + + if (hasJustRedeemedCode) { + try { + setIsRefreshing(true); + await authenticatedUser.onRefreshSubscription(); + } finally { + setIsRefreshing(false); + onOpenPendingDialog(true); + } + } + }} + /> + )} + + )} + + ); +} diff --git a/newIDE/app/src/Profile/Subscription/SubscriptionChecker.js b/newIDE/app/src/Profile/Subscription/SubscriptionChecker.js index 3993f39d2a5c..ac96ba91e09b 100644 --- a/newIDE/app/src/Profile/Subscription/SubscriptionChecker.js +++ b/newIDE/app/src/Profile/Subscription/SubscriptionChecker.js @@ -121,6 +121,7 @@ const SubscriptionChecker = React.forwardRef< if (onChangeSubscription) onChangeSubscription(); setDialogOpen(false); }} + recommendedPlanIdIfNoSubscription="gdevelop_silver" > diff --git a/newIDE/app/src/Profile/Subscription/SubscriptionDetails.js b/newIDE/app/src/Profile/Subscription/SubscriptionDetails.js index 04f15edcf2d0..a25b563e34aa 100644 --- a/newIDE/app/src/Profile/Subscription/SubscriptionDetails.js +++ b/newIDE/app/src/Profile/Subscription/SubscriptionDetails.js @@ -457,6 +457,7 @@ const SubscriptionDetails = ({ Choose a subscription} subscriptionDialogOpeningReason="Consult profile" + recommendedPlanIdIfNoSubscription="gdevelop_silver" > diff --git a/newIDE/app/src/Profile/Subscription/SubscriptionDialog.js b/newIDE/app/src/Profile/Subscription/SubscriptionDialog.js index 2599485efdd0..5b507148c71b 100644 --- a/newIDE/app/src/Profile/Subscription/SubscriptionDialog.js +++ b/newIDE/app/src/Profile/Subscription/SubscriptionDialog.js @@ -22,18 +22,13 @@ import { } from '../../Utils/GDevelopServices/Usage'; import { showErrorBox } from '../../UI/Messages/MessageBox'; import { - sendSubscriptionDialogShown, sendChoosePlanClicked, sendCancelSubscriptionToChange, } from '../../Utils/Analytics/EventSender'; -import { - type SubscriptionAnalyticsMetadata, - type SubscriptionType, -} from './SubscriptionSuggestionContext'; -import SubscriptionPendingDialog from './SubscriptionPendingDialog'; +import { type SubscriptionType } from './SubscriptionSuggestionContext'; import Window from '../../Utils/Window'; import Text from '../../UI/Text'; -import { ColumnStackLayout } from '../../UI/Layout'; +import { ColumnStackLayout, LineStackLayout } from '../../UI/Layout'; import RedemptionCodeIcon from '../../UI/CustomSvgIcons/RedemptionCode'; import useAlertDialog from '../../UI/Alert/useAlertDialog'; import RedeemCodeDialog from '../RedeemCodeDialog'; @@ -184,7 +179,7 @@ const getMaximumYearlyDiscountOverPlans = ({ return maximumDiscount; }; -const getPlanSpecificRequirements = ( +export const getPlanSpecificRequirements = ( i18n: I18nType, subscriptionPlansWithPricingSystems: ?Array ): Array => { @@ -207,21 +202,19 @@ const getPlanSpecificRequirements = ( }; type Props = {| - open: boolean, onClose: Function, - analyticsMetadata: SubscriptionAnalyticsMetadata, subscriptionPlansWithPricingSystems: ?(SubscriptionPlanWithPricingSystems[]), userLegacySubscriptionPlanWithPricingSystem: ?SubscriptionPlanWithPricingSystems, filter: ?SubscriptionType, + onOpenPendingDialog: (open: boolean) => void, |}; export default function SubscriptionDialog({ - open, onClose, - analyticsMetadata, subscriptionPlansWithPricingSystems, userLegacySubscriptionPlanWithPricingSystem, filter, + onOpenPendingDialog, }: Props) { const [isChangingSubscription, setIsChangingSubscription] = React.useState( false @@ -230,10 +223,6 @@ export default function SubscriptionDialog({ educationPlanSeatsCount, setEducationPlanSeatsCount, ] = React.useState(20); - const [ - subscriptionPendingDialogOpen, - setSubscriptionPendingDialogOpen, - ] = React.useState(false); const [redeemCodeDialogOpen, setRedeemCodeDialogOpen] = React.useState(false); const authenticatedUser = React.useContext(AuthenticatedUserContext); const [period, setPeriod] = React.useState<'year' | 'month'>( @@ -248,15 +237,6 @@ export default function SubscriptionDialog({ false ); - React.useEffect( - () => { - if (open) { - sendSubscriptionDialogShown(analyticsMetadata); - } - }, - [open, analyticsMetadata] - ); - const buyUpdateOrCancelPlan = async ( i18n: I18nType, subscriptionPlanPricingSystem: SubscriptionPlanPricingSystem | null @@ -271,7 +251,7 @@ export default function SubscriptionDialog({ } // Subscribing from an account without a subscription if (!subscription.planId && subscriptionPlanPricingSystem) { - setSubscriptionPendingDialogOpen(true); + onOpenPendingDialog(true); const isEducationPlan = subscriptionPlanPricingSystem && subscriptionPlanPricingSystem.planId === 'gdevelop_education'; @@ -360,7 +340,7 @@ export default function SubscriptionDialog({ } // Then redirect as if a new subscription is being chosen. - setSubscriptionPendingDialogOpen(true); + onOpenPendingDialog(true); Window.openExternalURL( getRedirectToCheckoutUrl({ pricingSystemId: subscriptionPlanPricingSystem.id, @@ -467,7 +447,7 @@ export default function SubscriptionDialog({ onClick={() => setRedeemCodeDialogOpen(true)} />, ]} - open={open} + open > {isPlanValid && userSubscriptionPlanWithPricingSystems && ( @@ -787,9 +767,12 @@ export default function SubscriptionDialog({
- - ❤️ Support What You Love - + + ❤️ + + Support What You Love + + The GDevelop project is open-source, powered by passion and @@ -883,15 +866,6 @@ export default function SubscriptionDialog({ )} - {subscriptionPendingDialogOpen && ( - { - setSubscriptionPendingDialogOpen(false); - authenticatedUser.onRefreshSubscription(); - }} - /> - )} {redeemCodeDialogOpen && ( void, authenticatedUser: AuthenticatedUser, + onSuccess: () => void, |}; export default function SubscriptionPendingDialog({ onClose, authenticatedUser, + onSuccess, }: Props) { const userPlanIdAtOpening = React.useRef( !!authenticatedUser.subscription @@ -90,6 +92,15 @@ export default function SubscriptionPendingDialog({ [onClose, onEdit, discordUsername] ); + React.useEffect( + () => { + if (hasUserPlanChanged) { + onSuccess(); + } + }, + [hasUserPlanChanged, onSuccess] + ); + return ( {({ i18n }) => ( diff --git a/newIDE/app/src/Profile/Subscription/SubscriptionSuggestionContext.js b/newIDE/app/src/Profile/Subscription/SubscriptionSuggestionContext.js index b84e1c745dda..ae4366dffa58 100644 --- a/newIDE/app/src/Profile/Subscription/SubscriptionSuggestionContext.js +++ b/newIDE/app/src/Profile/Subscription/SubscriptionSuggestionContext.js @@ -2,7 +2,10 @@ import { t } from '@lingui/macro'; import * as React from 'react'; import SubscriptionDialog from './SubscriptionDialog'; -import { type SubscriptionDialogDisplayReason } from '../../Utils/Analytics/EventSender'; +import { + sendSubscriptionDialogShown, + type SubscriptionDialogDisplayReason, +} from '../../Utils/Analytics/EventSender'; import { isNativeMobileApp } from '../../Utils/Platform'; import { hasMobileAppStoreSubscriptionPlan, @@ -13,14 +16,22 @@ import useAlertDialog from '../../UI/Alert/useAlertDialog'; import useSubscriptionPlans, { getAvailableSubscriptionPlansWithPrices, } from '../../Utils/UseSubscriptionPlans'; +import PromotionSubscriptionDialog from './PromotionSubscriptionDialog'; +import SubscriptionPendingDialog from './SubscriptionPendingDialog'; + +export type SubscriptionType = 'individual' | 'team' | 'education'; +export type PurchasablePlanId = + | 'gdevelop_silver' + | 'gdevelop_gold' + | 'gdevelop_startup' + | 'gdevelop_education'; export type SubscriptionAnalyticsMetadata = {| reason: SubscriptionDialogDisplayReason, + recommendedPlanId?: PurchasablePlanId, preStep?: 'subscriptionChecker', |}; -export type SubscriptionType = 'individual' | 'team' | 'education'; - type SubscriptionSuggestionState = {| /** * Call this when a subscription or subscription upgrade is required. @@ -46,10 +57,10 @@ export const SubscriptionSuggestionProvider = ({ children, simulateMobileApp, }: SubscriptionSuggestionProviderProps) => { - const [analyticsMetadata, setAnalyticsMetadata] = React.useState(null); + const [ + analyticsMetadata, + setAnalyticsMetadata, + ] = React.useState(null); const [filter, setFilter] = React.useState< 'individual' | 'team' | 'education' | null >(null); @@ -59,6 +70,10 @@ export const SubscriptionSuggestionProvider = ({ includeLegacy: true, authenticatedUser, }); + const [ + subscriptionPendingDialogOpen, + setSubscriptionPendingDialogOpen, + ] = React.useState(false); const closeSubscriptionDialog = () => setAnalyticsMetadata(null); @@ -132,23 +147,58 @@ export const SubscriptionSuggestionProvider = ({ [subscriptionPlansWithPricingSystems, authenticatedUser.subscription] ); + // When the analyticsMetadata is set, a dialog is shown so we can send an event. + React.useEffect( + () => { + if (analyticsMetadata) { + sendSubscriptionDialogShown(analyticsMetadata); + } + }, + [analyticsMetadata] + ); + return ( {children} - {analyticsMetadata && ( - { + setSubscriptionPendingDialogOpen(false); + authenticatedUser.onRefreshSubscription(); + }} + onSuccess={closeSubscriptionDialog} /> )} + {analyticsMetadata ? ( + !hasValidSubscriptionPlan(authenticatedUser.subscription) && + analyticsMetadata.recommendedPlanId ? ( + + setSubscriptionPendingDialogOpen(open) + } + /> + ) : ( + + setSubscriptionPendingDialogOpen(open) + } + /> + ) + ) : null} ); }; diff --git a/newIDE/app/src/ProjectCreation/AIPromptField.js b/newIDE/app/src/ProjectCreation/AIPromptField.js index d5c137331cd3..9038e85bc1c7 100644 --- a/newIDE/app/src/ProjectCreation/AIPromptField.js +++ b/newIDE/app/src/ProjectCreation/AIPromptField.js @@ -177,7 +177,10 @@ const AIPromptField = ({ )} /> {authenticated && !hasUsagesAvailable && ( - + diff --git a/newIDE/app/src/ProjectManager/LoadingScreenEditor.js b/newIDE/app/src/ProjectManager/LoadingScreenEditor.js index 9b0e129a3035..16e66fa4feee 100644 --- a/newIDE/app/src/ProjectManager/LoadingScreenEditor.js +++ b/newIDE/app/src/ProjectManager/LoadingScreenEditor.js @@ -68,7 +68,7 @@ export const LoadingScreenEditor = ({ const subscriptionChecker = React.useRef(null); const authenticatedUser = React.useContext(AuthenticatedUserContext); const forceUpdate = useForceUpdate(); - const shouldDisplayGetSubscriptionCard = !hasValidSubscriptionPlan( + const hasValidSubscription = hasValidSubscriptionPlan( authenticatedUser.subscription ); @@ -234,8 +234,11 @@ export const LoadingScreenEditor = ({ - {shouldDisplayGetSubscriptionCard && ( - + {!hasValidSubscription && ( + Get a silver or gold subscription to disable GDevelop diff --git a/newIDE/app/src/UI/CustomSvgIcons/ShieldChecked.js b/newIDE/app/src/UI/CustomSvgIcons/ShieldChecked.js new file mode 100644 index 000000000000..67f052f1bb96 --- /dev/null +++ b/newIDE/app/src/UI/CustomSvgIcons/ShieldChecked.js @@ -0,0 +1,20 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default React.memo(props => ( + + + +)); diff --git a/newIDE/app/src/UI/CustomSvgIcons/ThumbsUp.js b/newIDE/app/src/UI/CustomSvgIcons/ThumbsUp.js new file mode 100644 index 000000000000..6db32dde734b --- /dev/null +++ b/newIDE/app/src/UI/CustomSvgIcons/ThumbsUp.js @@ -0,0 +1,20 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default React.memo(props => ( + + + +)); diff --git a/newIDE/app/src/UI/Dialog.js b/newIDE/app/src/UI/Dialog.js index 48639034822c..cd9cda08e1b4 100644 --- a/newIDE/app/src/UI/Dialog.js +++ b/newIDE/app/src/UI/Dialog.js @@ -14,7 +14,7 @@ import { shouldCloseOrCancel, shouldSubmit, } from './KeyboardShortcuts/InteractionKeys'; -import { LineStackLayout } from './Layout'; +import { ColumnStackLayout, LineStackLayout } from './Layout'; import RaisedButton from './RaisedButton'; import Text from './Text'; import Cross from './CustomSvgIcons/Cross'; @@ -99,6 +99,16 @@ const styles = { minHeightForFullHeightModal: 'calc(100% - 64px)', minHeightForSmallHeightModal: 'min(100% - 64px, 350px)', minHeightForLargeHeightModal: 'min(100% - 64px, 800px)', + topBackground: { + position: 'absolute', + top: 0, + zIndex: -1, + left: dialogPaddingX, + right: dialogPaddingX, + height: '100%', + backgroundSize: 'contain', + backgroundRepeat: 'no-repeat', + }, }; const useDangerousStylesForDialog = (dangerLevel?: 'warning' | 'danger') => @@ -149,11 +159,14 @@ const useStylesForDialogContent = ({ type DialogProps = {| open?: boolean, title: React.Node, + subtitle?: React.Node, fixedContent?: React.Node, actions?: Array, secondaryActions?: Array, dangerLevel?: 'warning' | 'danger', + topBackgroundSrc?: string, + /** * Callback called when the dialog is asking to be closed * (either by Escape key or a click outside, according to preferences). @@ -220,6 +233,7 @@ const Dialog = ({ maxWidth, minHeight, title, + subtitle, fixedContent, children, flexColumnBody, @@ -231,6 +245,7 @@ const Dialog = ({ fullscreen, actionsFullWidthOnMobile, forceScrollVisible, + topBackgroundSrc, }: DialogProps) => { const preferences = React.useContext(PreferencesContext); const gdevelopTheme = React.useContext(GDevelopThemeContext); @@ -342,6 +357,14 @@ const Dialog = ({ const softKeyboardBottomOffset = useSoftKeyboardBottomOffset(); + const paperMinHeight = fullHeight + ? styles.minHeightForFullHeightModal + : minHeight === 'lg' + ? styles.minHeightForLargeHeightModal + : minHeight === 'sm' + ? styles.minHeightForSmallHeightModal + : undefined; + return ( + {topBackgroundSrc && ( +
+ )}
)} - - {title} - + + + {title} + + {subtitle && {subtitle}} + diff --git a/newIDE/app/src/UI/RaisedButton.js b/newIDE/app/src/UI/RaisedButton.js index 8c2737ff2f69..6b461fa5056d 100644 --- a/newIDE/app/src/UI/RaisedButton.js +++ b/newIDE/app/src/UI/RaisedButton.js @@ -12,7 +12,7 @@ export type RaisedButtonPropsWithoutOnClick = {| label?: React.Node, primary?: boolean, color?: 'primary' | 'success' | 'danger' | 'premium', - size?: 'medium', + size?: 'medium' | 'large', disabled?: boolean, keyboardFocused?: boolean, fullWidth?: boolean, diff --git a/newIDE/app/src/UI/User/UserChip.js b/newIDE/app/src/UI/User/UserChip.js index 3edf01bd1b30..4236f95878c7 100644 --- a/newIDE/app/src/UI/User/UserChip.js +++ b/newIDE/app/src/UI/User/UserChip.js @@ -31,6 +31,7 @@ const GetPremiumButton = () => { openSubscriptionDialog({ analyticsMetadata: { reason: 'Account get premium', + recommendedPlanId: 'gdevelop_silver', }, }); }} diff --git a/newIDE/app/src/Utils/Analytics/EventSender.js b/newIDE/app/src/Utils/Analytics/EventSender.js index 1900192d24be..97556f9d80ae 100644 --- a/newIDE/app/src/Utils/Analytics/EventSender.js +++ b/newIDE/app/src/Utils/Analytics/EventSender.js @@ -10,6 +10,7 @@ import { import { getIDEVersion, getIDEVersionWithHash } from '../../Version'; import { loadPreferencesFromLocalStorage } from '../../MainFrame/Preferences/PreferencesProvider'; import { getBrowserLanguageOrLocale } from '../Language'; +import { type SubscriptionAnalyticsMetadata } from '../../Profile/Subscription/SubscriptionSuggestionContext'; import optionalRequire from '../OptionalRequire'; import Window from '../Window'; const electron = optionalRequire('electron'); @@ -377,10 +378,9 @@ export type SubscriptionDialogDisplayReason = | 'Unlock course chapter' | 'Account get premium'; -export const sendSubscriptionDialogShown = (metadata: {| - reason: SubscriptionDialogDisplayReason, - preStep?: 'subscriptionChecker', -|}) => { +export const sendSubscriptionDialogShown = ( + metadata: SubscriptionAnalyticsMetadata +) => { recordEvent('subscription-dialog-shown', metadata); }; diff --git a/newIDE/app/src/Utils/UseOpenInitialDialog.js b/newIDE/app/src/Utils/UseOpenInitialDialog.js index 0acbdb913067..e64e00c5d579 100644 --- a/newIDE/app/src/Utils/UseOpenInitialDialog.js +++ b/newIDE/app/src/Utils/UseOpenInitialDialog.js @@ -1,7 +1,10 @@ // @flow import * as React from 'react'; import RouterContext from '../MainFrame/RouterContext'; -import { SubscriptionSuggestionContext } from '../Profile/Subscription/SubscriptionSuggestionContext'; +import { + SubscriptionSuggestionContext, + type PurchasablePlanId, +} from '../Profile/Subscription/SubscriptionSuggestionContext'; import { FLING_GAME_IN_APP_TUTORIAL_ID } from './GDevelopServices/InAppTutorial'; import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext'; @@ -34,10 +37,27 @@ const useOpenInitialDialog = ({ () => { switch (routeArguments['initial-dialog']) { case 'subscription': + let recommendedPlanId = routeArguments['recommended-plan-id']; + if ( + ![ + 'gdevelop_silver', + 'gdevelop_gold', + 'gdevelop_education', + 'gdevelop_startup', + ].includes(recommendedPlanId) + ) { + recommendedPlanId = 'gdevelop_silver'; + } + // $FlowFixMe - we know that recommendedPlanId is a valid PurchasablePlanId + const verifiedPlanId: PurchasablePlanId = recommendedPlanId; + openSubscriptionDialog({ - analyticsMetadata: { reason: 'Landing dialog at opening' }, + analyticsMetadata: { + reason: 'Landing dialog at opening', + recommendedPlanId: verifiedPlanId, + }, }); - removeRouteArguments(['initial-dialog']); + removeRouteArguments(['initial-dialog', 'recommended-plan-id']); break; case 'signup': // Add timeout to give time to the app to sign in with Firebase diff --git a/newIDE/app/src/VersionHistory/UseVersionHistory.js b/newIDE/app/src/VersionHistory/UseVersionHistory.js index 73c2e29d6f01..d927ac256450 100644 --- a/newIDE/app/src/VersionHistory/UseVersionHistory.js +++ b/newIDE/app/src/VersionHistory/UseVersionHistory.js @@ -454,6 +454,7 @@ const useVersionHistory = ({ subscriptionDialogOpeningReason="Version history" forceColumnLayout filter="team" + recommendedPlanIdIfNoSubscription="gdevelop_startup" > diff --git a/newIDE/app/src/stories/componentStories/Profile/Subscription/PromotionSubscriptionDialog.stories.js b/newIDE/app/src/stories/componentStories/Profile/Subscription/PromotionSubscriptionDialog.stories.js new file mode 100644 index 000000000000..4b56e7518908 --- /dev/null +++ b/newIDE/app/src/stories/componentStories/Profile/Subscription/PromotionSubscriptionDialog.stories.js @@ -0,0 +1,140 @@ +// @flow +import * as React from 'react'; +import { action } from '@storybook/addon-actions'; + +import paperDecorator from '../../../PaperDecorator'; +import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserContext'; +import { + fakeAuthenticatedUserWithNoSubscription, + fakeNotAuthenticatedUser, + subscriptionPlansWithPricingSystems, +} from '../../../../fixtures/GDevelopServicesTestData'; +import PromotionSubscriptionDialog from '../../../../Profile/Subscription/PromotionSubscriptionDialog'; +import AlertProvider from '../../../../UI/Alert/AlertProvider'; +import { getAvailableSubscriptionPlansWithPrices } from '../../../../Utils/UseSubscriptionPlans'; + +export default { + title: 'Subscription/PromotionSubscriptionDialog', + component: PromotionSubscriptionDialog, + decorators: [paperDecorator], +}; + +const availableSubscriptionPlansWithPrices = getAvailableSubscriptionPlansWithPrices( + subscriptionPlansWithPricingSystems +); + +export const NotAuthenticatedSilverRecommended = () => { + return ( + + + action('on close')()} + recommendedPlanId="gdevelop_silver" + onOpenPendingDialog={() => action('on open pending dialog')()} + /> + + + ); +}; + +export const AuthenticatedSilverRecommended = () => { + return ( + + + action('on close')()} + recommendedPlanId="gdevelop_silver" + onOpenPendingDialog={() => action('on open pending dialog')()} + /> + + + ); +}; + +export const GoldRecommended = () => { + return ( + + + action('on close')()} + recommendedPlanId="gdevelop_gold" + onOpenPendingDialog={() => action('on open pending dialog')()} + /> + + + ); +}; + +export const ProRecommended = () => { + return ( + + + action('on close')()} + recommendedPlanId="gdevelop_startup" + onOpenPendingDialog={() => action('on open pending dialog')()} + /> + + + ); +}; + +export const EducationRecommended = () => { + return ( + + + action('on close')()} + recommendedPlanId="gdevelop_education" + onOpenPendingDialog={() => action('on open pending dialog')()} + /> + + + ); +}; + +export const WithoutSilverButRecommended = () => { + const availableSubscriptionPlansWithPrices = getAvailableSubscriptionPlansWithPrices( + subscriptionPlansWithPricingSystems + ); + const subscritionPlansWithoutSilver = availableSubscriptionPlansWithPrices.filter( + plan => plan.id !== 'gdevelop_silver' + ); + return ( + + + action('on close')()} + recommendedPlanId="gdevelop_silver" + onOpenPendingDialog={() => action('on open pending dialog')()} + /> + + + ); +}; diff --git a/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionDialog.stories.js b/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionDialog.stories.js index 19d4d1b6b6a7..0614b724a0fb 100644 --- a/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionDialog.stories.js +++ b/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionDialog.stories.js @@ -172,7 +172,6 @@ export const Default = ({ action('on close')()} - analyticsMetadata={{ reason: 'Debugger' }} filter={filter === 'none' ? undefined : filter} + onOpenPendingDialog={() => action('on open pending dialog')()} /> diff --git a/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionPendingDialog.stories.js b/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionPendingDialog.stories.js index 6363ebd5b7e0..9931335a7c80 100644 --- a/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionPendingDialog.stories.js +++ b/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionPendingDialog.stories.js @@ -21,6 +21,7 @@ export const DefaultNoSubscription = () => ( ); @@ -28,6 +29,7 @@ export const AuthenticatedUserWithSubscriptionAndDiscordUsernameAlreadyFilled = ); @@ -43,6 +45,7 @@ export const AuthenticatedUserWithSilverSubscriptionButWithoutDiscordUsername = profile: fakeProfileWithoutDiscordUsername, }} onClose={action('on close')} + onSuccess={action('on success')} /> ); @@ -53,5 +56,6 @@ export const AuthenticatedUserWithStartupSubscriptionButWithoutDiscordUsername = profile: fakeProfileWithoutDiscordUsername, }} onClose={action('on close')} + onSuccess={action('on success')} /> ); diff --git a/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionSuggestionContext.stories.js b/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionSuggestionContext.stories.js index 12887487a0f0..fd3958c1768b 100644 --- a/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionSuggestionContext.stories.js +++ b/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionSuggestionContext.stories.js @@ -31,6 +31,7 @@ const SubscriptionDialogTestOpener = ({ label }: {| label: string |}) => { openSubscriptionDialog({ analyticsMetadata: { reason: 'Cloud Project limit reached', + recommendedPlanId: 'gdevelop_silver', }, }); }, From 4cb35d808b2d055768046b9dad586f7ea0b8735e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pasteau?= <4895034+ClementPasteau@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:04:50 +0100 Subject: [PATCH 2/7] Use full features summary for sub --- .../PromotionSubscriptionPlan.js | 87 ++++++++++++------- .../app/src/Utils/GDevelopServices/Usage.js | 66 +++++++++++++- 2 files changed, 123 insertions(+), 30 deletions(-) diff --git a/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/PromotionSubscriptionPlan.js b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/PromotionSubscriptionPlan.js index a36a5c6e060c..f4c0cb81a606 100644 --- a/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/PromotionSubscriptionPlan.js +++ b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/PromotionSubscriptionPlan.js @@ -2,12 +2,13 @@ import * as React from 'react'; import { I18n } from '@lingui/react'; import Text from '../../../UI/Text'; -import { Column, Line, Spacer } from '../../../UI/Grid'; +import { Column, LargeSpacer, Line, Spacer } from '../../../UI/Grid'; import { type SubscriptionPlanWithPricingSystems, type SubscriptionPlanPricingSystem, EDUCATION_PLAN_MAX_SEATS, EDUCATION_PLAN_MIN_SEATS, + getSummarizedSubscriptionPlanFeatures, } from '../../../Utils/GDevelopServices/Usage'; import { selectMessageByLocale } from '../../../Utils/i18n/MessageByLocale'; import { getPlanIcon } from '../PlanCard'; @@ -47,6 +48,10 @@ const styles = { justifyContent: 'center', alignItems: 'center', }, + summarizeFeatureRow: { + paddingTop: 4, + paddingBottom: 4, + }, tableRightItemContainer: { width: 120, display: 'flex', @@ -72,6 +77,13 @@ const styles = { cursor: 'default', }, discountedPrice: { textDecoration: 'line-through' }, + unlimitedContainer: { + padding: '4px 8px', + borderRadius: 4, + display: 'flex', + alignItems: 'center', + color: 'black', + }, }; const formatPriceWithCurrency = (amountInCents: number, currency: string) => { @@ -163,7 +175,7 @@ const PromotionSubscriptionPlan = ({ : styles.simpleSizeContainer } > - +
- {subscriptionPlanWithPricingSystems.bulletPointsByLocale.map( - (bulletPointByLocale, index) => ( - -
( + +
+ - - - {selectMessageByLocale(i18n, bulletPointByLocale)} - -
+ + {summarizedFeature.displayedFeatureName} + +
+ {summarizedFeature.enabled ? ( -
- -
- - ) - )} - + ) : summarizedFeature.unlimited ? ( +
+ + ∞ Unlimited + +
+ ) : ( + {summarizedFeature.description} + )} +
+ +
+
+ ))} + @@ -230,7 +259,7 @@ const PromotionSubscriptionPlan = ({ -
+
@@ -239,7 +268,7 @@ const PromotionSubscriptionPlan = ({ {planIcon} - + {selectMessageByLocale( diff --git a/newIDE/app/src/Utils/GDevelopServices/Usage.js b/newIDE/app/src/Utils/GDevelopServices/Usage.js index 33b84232af8d..9a9113229bad 100644 --- a/newIDE/app/src/Utils/GDevelopServices/Usage.js +++ b/newIDE/app/src/Utils/GDevelopServices/Usage.js @@ -1,8 +1,12 @@ // @flow import axios from 'axios'; +import { type I18n as I18nType } from '@lingui/core'; import { GDevelopUsageApi } from './ApiConfigs'; import { type MessageDescriptor } from '../i18n/MessageDescriptor.flow'; -import { type MessageByLocale } from '../i18n/MessageByLocale'; +import { + selectMessageByLocale, + type MessageByLocale, +} from '../i18n/MessageByLocale'; import { extractGDevelopApiErrorStatusAndCode } from './Errors'; import { isNativeMobileApp } from '../Platform'; @@ -181,6 +185,7 @@ export type SubscriptionPlan = {| unlimited?: true, upcoming?: true, trialLike?: true, + displayInSummary?: true, |}>, pillarNamesPerLocale: { [key: string]: MessageByLocale }, @@ -200,6 +205,17 @@ export interface UserEarningsBalance { updatedAt: number; } +export type SummarizedPlanFeature = {| + displayedFeatureName: string, + description?: string, + tooltip?: string, + enabled?: 'yes' | 'no', + unlimited?: true, + upcoming?: true, + trialLike?: true, + displayInSummary?: true, +|}; + export const EDUCATION_PLAN_MIN_SEATS = 5; export const EDUCATION_PLAN_MAX_SEATS = 300; export const apiClient = axios.create({ @@ -602,3 +618,51 @@ export const shouldHideClassroomTab = (limits: ?Limits) => export const canUseCloudProjectHistory = (limits: ?Limits): boolean => limits && limits.capabilities.versionHistory.enabled ? true : false; + +export function getSummarizedSubscriptionPlanFeatures( + i18n: I18nType, + plan: SubscriptionPlanWithPricingSystems +): SummarizedPlanFeature[] { + const summarizedFeatures: SummarizedPlanFeature[] = []; + + plan.fullFeatures.forEach(feature => { + let planFeature: ?SummarizedPlanFeature = null; + + const featureNameByLocale = plan.featureNamesByLocale[feature.featureName]; + if (!featureNameByLocale) { + // Couldn't find the feature name. + return; + } + if (!feature.displayInSummary) { + // Don't display this feature in the summary. + return; + } + + planFeature = { + displayedFeatureName: selectMessageByLocale(i18n, featureNameByLocale), + }; + + if (feature.enabled) { + planFeature.enabled = feature.enabled; + } + if (feature.unlimited) { + planFeature.unlimited = feature.unlimited; + } + if (feature.upcoming) { + planFeature.upcoming = feature.upcoming; + } + if (feature.trialLike) { + planFeature.trialLike = feature.trialLike; + } + if (feature.descriptionByLocale) { + planFeature.description = selectMessageByLocale( + i18n, + feature.descriptionByLocale + ); + } + + summarizedFeatures.push(planFeature); + }); + + return summarizedFeatures; +} From 18a2e8b32cbae3925fa14c3c6b223124239190ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pasteau?= <4895034+ClementPasteau@users.noreply.github.com> Date: Thu, 16 Jan 2025 16:12:50 +0100 Subject: [PATCH 3/7] Show loader if logging in --- .../Profile/Subscription/PromotionSubscriptionDialog/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/index.js b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/index.js index 3a98b8f10e1e..53478a54a4e4 100644 --- a/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/index.js +++ b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/index.js @@ -147,7 +147,8 @@ export default function PromotionSubscriptionDialog({ topBackgroundSrc={'res/premium/premium_dialog_background.png'} > - {!purchasablePlansWithPricingSystems ? ( + {!purchasablePlansWithPricingSystems || + authenticatedUser.loginState === 'loggingIn' ? ( ) : ( From d51bd157b5d9e1d42b222bdfb828e3b02c1824c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pasteau?= <4895034+ClementPasteau@users.noreply.github.com> Date: Thu, 16 Jan 2025 16:25:41 +0100 Subject: [PATCH 4/7] Remove type --- .../PromotionSubscriptionDialog/index.js | 3 +-- .../SubscriptionSuggestionContext.js | 14 +++++------- newIDE/app/src/Utils/UseOpenInitialDialog.js | 22 ++++--------------- 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/index.js b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/index.js index 53478a54a4e4..8f24d3de69b6 100644 --- a/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/index.js +++ b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/index.js @@ -11,7 +11,6 @@ import { type SubscriptionPlanPricingSystem, } from '../../../Utils/GDevelopServices/Usage'; import { sendChoosePlanClicked } from '../../../Utils/Analytics/EventSender'; -import { type PurchasablePlanId } from '../SubscriptionSuggestionContext'; import Window from '../../../Utils/Window'; import Text from '../../../UI/Text'; import { ColumnStackLayout, LineStackLayout } from '../../../UI/Layout'; @@ -26,7 +25,7 @@ import AlertMessage from '../../../UI/AlertMessage'; type Props = {| onClose: Function, subscriptionPlansWithPricingSystems: ?(SubscriptionPlanWithPricingSystems[]), - recommendedPlanId: PurchasablePlanId, + recommendedPlanId: string, onOpenPendingDialog: (open: boolean) => void, |}; diff --git a/newIDE/app/src/Profile/Subscription/SubscriptionSuggestionContext.js b/newIDE/app/src/Profile/Subscription/SubscriptionSuggestionContext.js index ae4366dffa58..21f714f9d3ec 100644 --- a/newIDE/app/src/Profile/Subscription/SubscriptionSuggestionContext.js +++ b/newIDE/app/src/Profile/Subscription/SubscriptionSuggestionContext.js @@ -18,17 +18,13 @@ import useSubscriptionPlans, { } from '../../Utils/UseSubscriptionPlans'; import PromotionSubscriptionDialog from './PromotionSubscriptionDialog'; import SubscriptionPendingDialog from './SubscriptionPendingDialog'; +import LoaderModal from '../../UI/LoaderModal'; export type SubscriptionType = 'individual' | 'team' | 'education'; -export type PurchasablePlanId = - | 'gdevelop_silver' - | 'gdevelop_gold' - | 'gdevelop_startup' - | 'gdevelop_education'; export type SubscriptionAnalyticsMetadata = {| reason: SubscriptionDialogDisplayReason, - recommendedPlanId?: PurchasablePlanId, + recommendedPlanId?: string, preStep?: 'subscriptionChecker', |}; @@ -171,8 +167,10 @@ export const SubscriptionSuggestionProvider = ({ /> )} {analyticsMetadata ? ( - !hasValidSubscriptionPlan(authenticatedUser.subscription) && - analyticsMetadata.recommendedPlanId ? ( + authenticatedUser.loginState === 'loggingIn' ? ( + + ) : !hasValidSubscriptionPlan(authenticatedUser.subscription) && + analyticsMetadata.recommendedPlanId ? ( { switch (routeArguments['initial-dialog']) { case 'subscription': - let recommendedPlanId = routeArguments['recommended-plan-id']; - if ( - ![ - 'gdevelop_silver', - 'gdevelop_gold', - 'gdevelop_education', - 'gdevelop_startup', - ].includes(recommendedPlanId) - ) { - recommendedPlanId = 'gdevelop_silver'; - } - // $FlowFixMe - we know that recommendedPlanId is a valid PurchasablePlanId - const verifiedPlanId: PurchasablePlanId = recommendedPlanId; + let recommendedPlanId = + routeArguments['recommended-plan-id'] || 'gdevelop_silver'; openSubscriptionDialog({ analyticsMetadata: { reason: 'Landing dialog at opening', - recommendedPlanId: verifiedPlanId, + recommendedPlanId, }, }); removeRouteArguments(['initial-dialog', 'recommended-plan-id']); From f3cae7709a57047a789b686a3af4d3fa0a3cb5c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pasteau?= <4895034+ClementPasteau@users.noreply.github.com> Date: Thu, 16 Jan 2025 17:30:55 +0100 Subject: [PATCH 5/7] Use real data for storybook --- .../GDevelopServicesTestData/index.js | 2031 ----------------- .../RecommendationList.stories.js | 161 +- .../PromotionSubscriptionDialog.stories.js | 103 +- .../SubscriptionDetails.stories.js | 17 +- .../SubscriptionDialog.stories.js | 30 +- 5 files changed, 186 insertions(+), 2156 deletions(-) diff --git a/newIDE/app/src/fixtures/GDevelopServicesTestData/index.js b/newIDE/app/src/fixtures/GDevelopServicesTestData/index.js index a749442f611f..8166e7f6f670 100644 --- a/newIDE/app/src/fixtures/GDevelopServicesTestData/index.js +++ b/newIDE/app/src/fixtures/GDevelopServicesTestData/index.js @@ -3,7 +3,6 @@ import { type Usages, type Subscription, type Limits, - type SubscriptionPlanWithPricingSystems, } from '../../Utils/GDevelopServices/Usage'; import { User as FirebaseUser } from 'firebase/auth'; import { type Profile } from '../../Utils/GDevelopServices/Authentication'; @@ -313,2036 +312,6 @@ export const noSubscription: Subscription = { pricingSystemId: null, }; -export const subscriptionPlansWithPricingSystems: SubscriptionPlanWithPricingSystems[] = [ - { - id: 'free', - isLegacy: false, - nameByLocale: { - en: 'Free', - }, - targetAudiences: ['CASUAL'], - pillarNamesPerLocale: { - 'ASSET-STORE': { - en: 'Asset Store', - }, - CREATION: { - en: 'Creation', - }, - MARKETING: { - en: 'Marketing & Promotion', - }, - PRO: { - en: 'Professional features', - }, - PUBLISHING: { - en: 'Publishing', - }, - SOCIAL: { - en: 'Social & Online', - }, - SUPPORT: { - en: 'Community & Support', - }, - MONETIZATION: { - en: 'Monetization', - }, - }, - featureNamesByLocale: { - ENGINE_AND_EDITOR: { - en: 'Open-source (MIT) editor and game engine', - }, - DEBUGGER_AND_LIVE_PREVIEW: { - en: 'Debugger and live preview', - }, - CLOUD_PROJECTS: { - en: 'Cloud projects', - }, - AI_PROTOTYPING: { - en: 'Game prototypes with AI', - }, - MOBILE_APP: { - en: 'Mobile app access', - }, - MANUAL_EXPORT: { - en: 'Export (any platform) manually with devtools', - }, - PUBLISH_GD_GAMES: { - en: 'Publish your game on gd.games', - }, - PUBLISH_DESKTOP: { - en: 'Publish for desktop (Windows, macOS, Linux)', - }, - PUBLISH_ANDROID: { - en: 'Publish your game on Google Play (Android)', - }, - PUBLISH_IOS: { - en: 'Publish your game on iOS (App Store)', - }, - REMOVE_MANDATORY_LOGO: { - en: 'Remove GDevelop logo/watermark', - }, - VERSION_HISTORY: { - en: 'Version history', - }, - COLLABORATORS: { - en: 'Collaborator (project sharing)', - }, - INVOICES: { - en: 'Invoices for your company', - }, - LEADERBOARDS: { - en: 'Leaderboards for games', - }, - LEADERBOARDS_STYLING: { - en: 'Leaderboards styling', - }, - ANALYTICS: { - en: 'Analytics on game usage', - }, - FEEDBACKS: { - en: 'Player feedbacks', - }, - MULTIPLAYER: { - en: 'Multiplayer', - }, - EMBED_GD_GAMES: { - en: 'Embed game on any website in one click', - }, - DEDICATED_SUPPORT_CHANNEL: { - en: 'Dedicated professional support channel', - }, - DEDICATED_COMMUNITY_CHANNEL: { - en: 'Dedicated community channel', - }, - FREE_CREDITS: { - en: 'Free credits every month', - }, - FEATURING_TIER1: { - en: 'Featuring on gd.games', - }, - FEATURING_TIER2: { - en: 'Featuring on social/newsletter', - }, - FEATURING_TIER3: { - en: 'Featuring inside GDevelop', - }, - GET_FREE_ASSET_PACKS: { - en: 'Use thousands of free asset packs', - }, - CLAIM_PREMIUM_ASSET_PACKS: { - en: 'Claim a premium asset pack or template', - }, - BUY_PREMIUM_ASSET_PACKS: { - en: 'Buy premium asset packs', - }, - ADMOB_ADS: { - en: 'Show ads in games (Admob for Android)', - }, - COMMUNITY_IAP: { - en: 'In-App Purchases (community extension)', - }, - SELL_GAMES: { - en: 'Sell your games (Steam, App Stores...)', - }, - SELL_ON_ASSET_STORE: { - en: 'Sell your assets or templates on GDevelop Asset Store', - }, - }, - descriptionByLocale: { - en: 'Create your first game 100% free.', - }, - bulletPointsByLocale: [ - { - en: 'Fully featured, open-source game engine', - }, - { - en: 'Publish to Android/Desktop (once per day)', - }, - { - en: 'Try GDevelop.io online services for free', - }, - { - en: 'Thousands of free assets on the Asset Store', - }, - ], - fullFeatures: [ - { - featureName: 'ENGINE_AND_EDITOR', - pillarName: 'CREATION', - enabled: 'yes', - }, - { - featureName: 'DEBUGGER_AND_LIVE_PREVIEW', - pillarName: 'CREATION', - descriptionByLocale: { - en: 'Nag screen', - }, - trialLike: true, - }, - { - featureName: 'CLOUD_PROJECTS', - pillarName: 'CREATION', - descriptionByLocale: { - en: '3', - }, - tooltipByLocale: { - en: - '10 for early bird accounts, created and verified before Feb 2024.', - }, - }, - { - featureName: 'MOBILE_APP', - pillarName: 'CREATION', - enabled: 'no', - }, - { - featureName: 'AI_PROTOTYPING', - pillarName: 'CREATION', - descriptionByLocale: { - en: '1 per day', - }, - trialLike: true, - tooltipByLocale: { - en: - '2 per day for early bird accounts, created and verified before Feb 2024.', - }, - }, - { - featureName: 'PUBLISH_GD_GAMES', - pillarName: 'PUBLISHING', - unlimited: true, - }, - { - featureName: 'PUBLISH_DESKTOP', - pillarName: 'PUBLISHING', - descriptionByLocale: { - en: '1 per day', - }, - tooltipByLocale: { - en: - '2 per day for early bird accounts, created and verified before Feb 2024.', - }, - trialLike: true, - }, - { - featureName: 'PUBLISH_ANDROID', - pillarName: 'PUBLISHING', - descriptionByLocale: { - en: '1 per day', - }, - tooltipByLocale: { - en: - '2 per day for early bird accounts, created and verified before Feb 2024.', - }, - trialLike: true, - }, - { - featureName: 'PUBLISH_IOS', - pillarName: 'PUBLISHING', - enabled: 'no', - }, - { - featureName: 'MANUAL_EXPORT', - pillarName: 'PUBLISHING', - enabled: 'yes', - }, - { - featureName: 'REMOVE_MANDATORY_LOGO', - pillarName: 'PUBLISHING', - enabled: 'no', - }, - { - featureName: 'VERSION_HISTORY', - pillarName: 'PRO', - enabled: 'no', - }, - { - featureName: 'COLLABORATORS', - pillarName: 'PRO', - enabled: 'no', - }, - { - featureName: 'INVOICES', - pillarName: 'PRO', - enabled: 'no', - }, - { - featureName: 'FEEDBACKS', - pillarName: 'SOCIAL', - descriptionByLocale: { - en: '3 per game', - }, - }, - { - featureName: 'LEADERBOARDS', - pillarName: 'SOCIAL', - descriptionByLocale: { - en: '1 per game', - }, - }, - { - featureName: 'LEADERBOARDS_STYLING', - pillarName: 'SOCIAL', - enabled: 'no', - }, - { - featureName: 'ANALYTICS', - pillarName: 'SOCIAL', - enabled: 'no', - }, - { - featureName: 'MULTIPLAYER', - pillarName: 'SOCIAL', - enabled: 'no', - upcoming: true, - }, - { - featureName: 'EMBED_GD_GAMES', - upcoming: true, - pillarName: 'SOCIAL', - enabled: 'no', - }, - { - featureName: 'DEDICATED_COMMUNITY_CHANNEL', - pillarName: 'SUPPORT', - enabled: 'no', - }, - { - featureName: 'DEDICATED_SUPPORT_CHANNEL', - pillarName: 'SUPPORT', - enabled: 'no', - }, - { - featureName: 'FREE_CREDITS', - pillarName: 'MARKETING', - enabled: 'no', - }, - { - featureName: 'FEATURING_TIER1', - pillarName: 'MARKETING', - descriptionByLocale: { - en: 'Buy with credits', - }, - }, - { - featureName: 'FEATURING_TIER2', - pillarName: 'MARKETING', - descriptionByLocale: { - en: 'Buy with credits', - }, - }, - { - featureName: 'FEATURING_TIER3', - pillarName: 'MARKETING', - descriptionByLocale: { - en: 'Buy with credits', - }, - }, - { - featureName: 'GET_FREE_ASSET_PACKS', - pillarName: 'ASSET-STORE', - enabled: 'yes', - }, - { - featureName: 'BUY_PREMIUM_ASSET_PACKS', - pillarName: 'ASSET-STORE', - enabled: 'yes', - }, - { - featureName: 'CLAIM_PREMIUM_ASSET_PACKS', - pillarName: 'ASSET-STORE', - enabled: 'no', - }, - { - featureName: 'ADMOB_ADS', - pillarName: 'MONETIZATION', - enabled: 'yes', - }, - { - featureName: 'COMMUNITY_IAP', - pillarName: 'MONETIZATION', - enabled: 'yes', - }, - { - featureName: 'SELL_GAMES', - pillarName: 'MONETIZATION', - enabled: 'yes', - }, - { - featureName: 'SELL_ON_ASSET_STORE', - pillarName: 'MONETIZATION', - descriptionByLocale: { - en: 'Yes - get in touch', - }, - }, - ], - pricingSystems: [], - }, - { - id: 'gdevelop_silver', - isLegacy: false, - nameByLocale: { - en: 'Silver', - }, - targetAudiences: ['CASUAL'], - pillarNamesPerLocale: { - 'ASSET-STORE': { - en: 'Asset Store', - }, - CREATION: { - en: 'Creation', - }, - MARKETING: { - en: 'Marketing & Promotion', - }, - PRO: { - en: 'Professional features', - }, - PUBLISHING: { - en: 'Publishing', - }, - SOCIAL: { - en: 'Social & Online', - }, - SUPPORT: { - en: 'Community & Support', - }, - MONETIZATION: { - en: 'Monetization', - }, - }, - featureNamesByLocale: { - ENGINE_AND_EDITOR: { - en: 'Open-source (MIT) editor and game engine', - }, - DEBUGGER_AND_LIVE_PREVIEW: { - en: 'Debugger and live preview', - }, - CLOUD_PROJECTS: { - en: 'Cloud projects', - }, - AI_PROTOTYPING: { - en: 'Game prototypes with AI', - }, - MOBILE_APP: { - en: 'Mobile app access', - }, - MANUAL_EXPORT: { - en: 'Export (any platform) manually with devtools', - }, - PUBLISH_GD_GAMES: { - en: 'Publish your game on gd.games', - }, - PUBLISH_DESKTOP: { - en: 'Publish for desktop (Windows, macOS, Linux)', - }, - PUBLISH_ANDROID: { - en: 'Publish your game on Google Play (Android)', - }, - PUBLISH_IOS: { - en: 'Publish your game on iOS (App Store)', - }, - REMOVE_MANDATORY_LOGO: { - en: 'Remove GDevelop logo/watermark', - }, - VERSION_HISTORY: { - en: 'Version history', - }, - COLLABORATORS: { - en: 'Collaborator (project sharing)', - }, - INVOICES: { - en: 'Invoices for your company', - }, - LEADERBOARDS: { - en: 'Leaderboards for games', - }, - LEADERBOARDS_STYLING: { - en: 'Leaderboards styling', - }, - ANALYTICS: { - en: 'Analytics on game usage', - }, - FEEDBACKS: { - en: 'Player feedbacks', - }, - MULTIPLAYER: { - en: 'Multiplayer', - }, - EMBED_GD_GAMES: { - en: 'Embed game on any website in one click', - }, - DEDICATED_SUPPORT_CHANNEL: { - en: 'Dedicated professional support channel', - }, - DEDICATED_COMMUNITY_CHANNEL: { - en: 'Dedicated community channel', - }, - FREE_CREDITS: { - en: 'Free credits every month', - }, - FEATURING_TIER1: { - en: 'Featuring on gd.games', - }, - FEATURING_TIER2: { - en: 'Featuring on social/newsletter', - }, - FEATURING_TIER3: { - en: 'Featuring inside GDevelop', - }, - GET_FREE_ASSET_PACKS: { - en: 'Use thousands of free asset packs', - }, - CLAIM_PREMIUM_ASSET_PACKS: { - en: 'Claim a premium asset pack or template', - }, - BUY_PREMIUM_ASSET_PACKS: { - en: 'Buy premium asset packs', - }, - ADMOB_ADS: { - en: 'Show ads in games (Admob for Android)', - }, - COMMUNITY_IAP: { - en: 'In-App Purchases (community extension)', - }, - SELL_GAMES: { - en: 'Sell your games (Steam, App Stores...)', - }, - SELL_ON_ASSET_STORE: { - en: 'Sell your assets or templates on GDevelop Asset Store', - }, - }, - descriptionByLocale: { - en: 'Publish your first games on Android.', - }, - specificRequirementByLocale: { - en: - "If you're using your GDevelop account for a company having more than $50,000 USD revenue per year, you must use the Pro membership.", - }, - bulletPointsByLocale: [ - { - en: 'Publish on Google Play, desktop and on the web', - }, - { - en: 'Access GDevelop mobile app (iOS/Android)', - }, - { - en: 'Analytics to follow your game virality', - }, - { - en: '100 credits per month', - }, - ], - fullFeatures: [ - { - featureName: 'ENGINE_AND_EDITOR', - pillarName: 'CREATION', - enabled: 'yes', - }, - { - featureName: 'DEBUGGER_AND_LIVE_PREVIEW', - pillarName: 'CREATION', - enabled: 'yes', - }, - { - featureName: 'CLOUD_PROJECTS', - pillarName: 'CREATION', - descriptionByLocale: { - en: '50', - }, - }, - { - featureName: 'MOBILE_APP', - pillarName: 'CREATION', - enabled: 'yes', - }, - { - featureName: 'AI_PROTOTYPING', - pillarName: 'CREATION', - descriptionByLocale: { - en: '10 per day', - }, - }, - { - featureName: 'PUBLISH_GD_GAMES', - pillarName: 'PUBLISHING', - unlimited: true, - }, - { - featureName: 'PUBLISH_DESKTOP', - pillarName: 'PUBLISHING', - descriptionByLocale: { - en: '10 per day', - }, - }, - { - featureName: 'PUBLISH_ANDROID', - pillarName: 'PUBLISHING', - descriptionByLocale: { - en: '10 per day', - }, - }, - { - featureName: 'PUBLISH_IOS', - pillarName: 'PUBLISHING', - enabled: 'no', - }, - { - featureName: 'MANUAL_EXPORT', - pillarName: 'PUBLISHING', - enabled: 'yes', - }, - { - featureName: 'REMOVE_MANDATORY_LOGO', - pillarName: 'PUBLISHING', - enabled: 'yes', - }, - { - featureName: 'VERSION_HISTORY', - pillarName: 'PRO', - enabled: 'no', - }, - { - featureName: 'COLLABORATORS', - pillarName: 'PRO', - enabled: 'no', - }, - { - featureName: 'INVOICES', - pillarName: 'PRO', - enabled: 'no', - }, - { - featureName: 'FEEDBACKS', - pillarName: 'SOCIAL', - descriptionByLocale: { - en: '10 per game', - }, - }, - { - featureName: 'LEADERBOARDS', - pillarName: 'SOCIAL', - descriptionByLocale: { - en: '1 per game', - }, - }, - { - featureName: 'LEADERBOARDS_STYLING', - pillarName: 'SOCIAL', - enabled: 'no', - }, - { - featureName: 'ANALYTICS', - pillarName: 'SOCIAL', - enabled: 'yes', - }, - { - featureName: 'MULTIPLAYER', - pillarName: 'SOCIAL', - enabled: 'no', - upcoming: true, - }, - { - featureName: 'EMBED_GD_GAMES', - upcoming: true, - pillarName: 'SOCIAL', - enabled: 'no', - }, - { - featureName: 'DEDICATED_COMMUNITY_CHANNEL', - pillarName: 'SUPPORT', - enabled: 'no', - }, - { - featureName: 'DEDICATED_SUPPORT_CHANNEL', - pillarName: 'SUPPORT', - enabled: 'no', - }, - { - featureName: 'FREE_CREDITS', - pillarName: 'MARKETING', - descriptionByLocale: { - en: '100 per month', - }, - }, - { - featureName: 'FEATURING_TIER1', - pillarName: 'MARKETING', - descriptionByLocale: { - en: 'Buy with credits', - }, - }, - { - featureName: 'FEATURING_TIER2', - pillarName: 'MARKETING', - descriptionByLocale: { - en: 'Buy with credits', - }, - }, - { - featureName: 'FEATURING_TIER3', - pillarName: 'MARKETING', - descriptionByLocale: { - en: 'Buy with credits', - }, - }, - { - featureName: 'GET_FREE_ASSET_PACKS', - pillarName: 'ASSET-STORE', - enabled: 'yes', - }, - { - featureName: 'BUY_PREMIUM_ASSET_PACKS', - pillarName: 'ASSET-STORE', - enabled: 'yes', - }, - { - featureName: 'CLAIM_PREMIUM_ASSET_PACKS', - pillarName: 'ASSET-STORE', - enabled: 'no', - }, - { - featureName: 'ADMOB_ADS', - pillarName: 'MONETIZATION', - enabled: 'yes', - }, - { - featureName: 'COMMUNITY_IAP', - pillarName: 'MONETIZATION', - enabled: 'yes', - }, - { - featureName: 'SELL_GAMES', - pillarName: 'MONETIZATION', - enabled: 'yes', - }, - { - featureName: 'SELL_ON_ASSET_STORE', - pillarName: 'MONETIZATION', - descriptionByLocale: { - en: 'Yes - get in touch', - }, - }, - ], - pricingSystems: [ - { - id: 'silver_1month_499EUR', - status: 'active', - planId: 'gdevelop_silver', - period: 'month', - periodCount: 1, - currency: 'EUR', - amountInCents: 499, - region: 'eurozone', - }, - { - id: 'silver_1year_3599EUR', - status: 'active', - planId: 'gdevelop_silver', - period: 'year', - periodCount: 1, - currency: 'EUR', - amountInCents: 3599, - region: 'eurozone', - }, - ], - }, - { - id: 'gdevelop_gold', - isLegacy: false, - nameByLocale: { - en: 'Gold', - }, - targetAudiences: ['CASUAL'], - pillarNamesPerLocale: { - 'ASSET-STORE': { - en: 'Asset Store', - }, - CREATION: { - en: 'Creation', - }, - MARKETING: { - en: 'Marketing & Promotion', - }, - PRO: { - en: 'Professional features', - }, - PUBLISHING: { - en: 'Publishing', - }, - SOCIAL: { - en: 'Social & Online', - }, - SUPPORT: { - en: 'Community & Support', - }, - MONETIZATION: { - en: 'Monetization', - }, - }, - featureNamesByLocale: { - ENGINE_AND_EDITOR: { - en: 'Open-source (MIT) editor and game engine', - }, - DEBUGGER_AND_LIVE_PREVIEW: { - en: 'Debugger and live preview', - }, - CLOUD_PROJECTS: { - en: 'Cloud projects', - }, - AI_PROTOTYPING: { - en: 'Game prototypes with AI', - }, - MOBILE_APP: { - en: 'Mobile app access', - }, - MANUAL_EXPORT: { - en: 'Export (any platform) manually with devtools', - }, - PUBLISH_GD_GAMES: { - en: 'Publish your game on gd.games', - }, - PUBLISH_DESKTOP: { - en: 'Publish for desktop (Windows, macOS, Linux)', - }, - PUBLISH_ANDROID: { - en: 'Publish your game on Google Play (Android)', - }, - PUBLISH_IOS: { - en: 'Publish your game on iOS (App Store)', - }, - REMOVE_MANDATORY_LOGO: { - en: 'Remove GDevelop logo/watermark', - }, - VERSION_HISTORY: { - en: 'Version history', - }, - COLLABORATORS: { - en: 'Collaborator (project sharing)', - }, - INVOICES: { - en: 'Invoices for your company', - }, - LEADERBOARDS: { - en: 'Leaderboards for games', - }, - LEADERBOARDS_STYLING: { - en: 'Leaderboards styling', - }, - ANALYTICS: { - en: 'Analytics on game usage', - }, - FEEDBACKS: { - en: 'Player feedbacks', - }, - MULTIPLAYER: { - en: 'Multiplayer', - }, - EMBED_GD_GAMES: { - en: 'Embed game on any website in one click', - }, - DEDICATED_SUPPORT_CHANNEL: { - en: 'Dedicated professional support channel', - }, - DEDICATED_COMMUNITY_CHANNEL: { - en: 'Dedicated community channel', - }, - FREE_CREDITS: { - en: 'Free credits every month', - }, - FEATURING_TIER1: { - en: 'Featuring on gd.games', - }, - FEATURING_TIER2: { - en: 'Featuring on social/newsletter', - }, - FEATURING_TIER3: { - en: 'Featuring inside GDevelop', - }, - GET_FREE_ASSET_PACKS: { - en: 'Use thousands of free asset packs', - }, - CLAIM_PREMIUM_ASSET_PACKS: { - en: 'Claim a premium asset pack or template', - }, - BUY_PREMIUM_ASSET_PACKS: { - en: 'Buy premium asset packs', - }, - ADMOB_ADS: { - en: 'Show ads in games (Admob for Android)', - }, - COMMUNITY_IAP: { - en: 'In-App Purchases (community extension)', - }, - SELL_GAMES: { - en: 'Sell your games (Steam, App Stores...)', - }, - SELL_ON_ASSET_STORE: { - en: 'Sell your assets or templates on GDevelop Asset Store', - }, - }, - descriptionByLocale: { - en: 'Full creation and marketing features. Publish anywhere.', - }, - specificRequirementByLocale: { - en: - "If you're using your GDevelop account for a company having more than $50,000 USD revenue per year, you must use the Pro membership.", - }, - bulletPointsByLocale: [ - { - en: 'Publish your game on iOS', - }, - { - en: 'Dedicated channel on Discord', - }, - { - en: 'Unlimited leaderboards and player feedbacks', - }, - { - en: '300 credits per month, to promote your game or use in asset store', - }, - { - en: '1 free asset pack to claim per month', - }, - ], - fullFeatures: [ - { - featureName: 'ENGINE_AND_EDITOR', - pillarName: 'CREATION', - enabled: 'yes', - }, - { - featureName: 'DEBUGGER_AND_LIVE_PREVIEW', - pillarName: 'CREATION', - enabled: 'yes', - }, - { - featureName: 'CLOUD_PROJECTS', - pillarName: 'CREATION', - descriptionByLocale: { - en: '100', - }, - }, - { - featureName: 'MOBILE_APP', - pillarName: 'CREATION', - enabled: 'yes', - }, - { - featureName: 'AI_PROTOTYPING', - pillarName: 'CREATION', - descriptionByLocale: { - en: '100 per day', - }, - }, - { - featureName: 'PUBLISH_GD_GAMES', - pillarName: 'PUBLISHING', - unlimited: true, - }, - { - featureName: 'PUBLISH_DESKTOP', - pillarName: 'PUBLISHING', - descriptionByLocale: { - en: '100 per day', - }, - }, - { - featureName: 'PUBLISH_ANDROID', - pillarName: 'PUBLISHING', - descriptionByLocale: { - en: '100 per day', - }, - }, - { - featureName: 'PUBLISH_IOS', - pillarName: 'PUBLISHING', - descriptionByLocale: { - en: '5 per month', - }, - }, - { - featureName: 'MANUAL_EXPORT', - pillarName: 'PUBLISHING', - enabled: 'yes', - }, - { - featureName: 'REMOVE_MANDATORY_LOGO', - pillarName: 'PUBLISHING', - enabled: 'yes', - }, - { - featureName: 'VERSION_HISTORY', - pillarName: 'PRO', - enabled: 'no', - }, - { - featureName: 'COLLABORATORS', - pillarName: 'PRO', - enabled: 'no', - }, - { - featureName: 'INVOICES', - pillarName: 'PRO', - enabled: 'no', - }, - { - featureName: 'FEEDBACKS', - pillarName: 'SOCIAL', - unlimited: true, - }, - { - featureName: 'LEADERBOARDS', - pillarName: 'SOCIAL', - unlimited: true, - }, - { - featureName: 'LEADERBOARDS_STYLING', - pillarName: 'SOCIAL', - descriptionByLocale: { - en: 'Custom colors', - }, - }, - { - featureName: 'ANALYTICS', - pillarName: 'SOCIAL', - enabled: 'yes', - }, - { - featureName: 'MULTIPLAYER', - pillarName: 'SOCIAL', - enabled: 'yes', - upcoming: true, - }, - { - featureName: 'EMBED_GD_GAMES', - upcoming: true, - pillarName: 'SOCIAL', - enabled: 'no', - }, - { - featureName: 'DEDICATED_COMMUNITY_CHANNEL', - pillarName: 'SUPPORT', - enabled: 'yes', - }, - { - featureName: 'DEDICATED_SUPPORT_CHANNEL', - pillarName: 'SUPPORT', - enabled: 'no', - }, - { - featureName: 'FREE_CREDITS', - pillarName: 'MARKETING', - descriptionByLocale: { - en: '300 per month', - }, - }, - { - featureName: 'FEATURING_TIER1', - pillarName: 'MARKETING', - descriptionByLocale: { - en: 'Buy with credits', - }, - }, - { - featureName: 'FEATURING_TIER2', - pillarName: 'MARKETING', - descriptionByLocale: { - en: 'Buy with credits', - }, - }, - { - featureName: 'FEATURING_TIER3', - pillarName: 'MARKETING', - descriptionByLocale: { - en: 'Buy with credits', - }, - }, - { - featureName: 'GET_FREE_ASSET_PACKS', - pillarName: 'ASSET-STORE', - enabled: 'yes', - }, - { - featureName: 'BUY_PREMIUM_ASSET_PACKS', - pillarName: 'ASSET-STORE', - enabled: 'yes', - }, - { - featureName: 'CLAIM_PREMIUM_ASSET_PACKS', - pillarName: 'ASSET-STORE', - descriptionByLocale: { - en: '1 per month', - }, - }, - { - featureName: 'ADMOB_ADS', - pillarName: 'MONETIZATION', - enabled: 'yes', - }, - { - featureName: 'COMMUNITY_IAP', - pillarName: 'MONETIZATION', - enabled: 'yes', - }, - { - featureName: 'SELL_GAMES', - pillarName: 'MONETIZATION', - enabled: 'yes', - }, - { - featureName: 'SELL_ON_ASSET_STORE', - pillarName: 'MONETIZATION', - descriptionByLocale: { - en: 'Yes - get in touch', - }, - }, - ], - pricingSystems: [ - { - id: 'gold_1month_999EUR', - planId: 'gdevelop_gold', - status: 'active', - period: 'month', - periodCount: 1, - currency: 'EUR', - amountInCents: 999, - region: 'eurozone', - }, - { - id: 'gold_1year_7199EUR', - planId: 'gdevelop_gold', - status: 'active', - period: 'year', - periodCount: 1, - currency: 'EUR', - amountInCents: 7199, - region: 'eurozone', - }, - ], - }, - { - id: 'gdevelop_startup', - isLegacy: false, - nameByLocale: { - en: 'Pro', - }, - targetAudiences: ['PRO'], - pillarNamesPerLocale: { - 'ASSET-STORE': { - en: 'Asset Store', - }, - CREATION: { - en: 'Creation', - }, - MARKETING: { - en: 'Marketing & Promotion', - }, - PRO: { - en: 'Professional features', - }, - PUBLISHING: { - en: 'Publishing', - }, - SOCIAL: { - en: 'Social & Online', - }, - SUPPORT: { - en: 'Community & Support', - }, - MONETIZATION: { - en: 'Monetization', - }, - }, - featureNamesByLocale: { - ENGINE_AND_EDITOR: { - en: 'Open-source (MIT) editor and game engine', - }, - DEBUGGER_AND_LIVE_PREVIEW: { - en: 'Debugger and live preview', - }, - CLOUD_PROJECTS: { - en: 'Cloud projects', - }, - AI_PROTOTYPING: { - en: 'Game prototypes with AI', - }, - MOBILE_APP: { - en: 'Mobile app access', - }, - MANUAL_EXPORT: { - en: 'Export (any platform) manually with devtools', - }, - PUBLISH_GD_GAMES: { - en: 'Publish your game on gd.games', - }, - PUBLISH_DESKTOP: { - en: 'Publish for desktop (Windows, macOS, Linux)', - }, - PUBLISH_ANDROID: { - en: 'Publish your game on Google Play (Android)', - }, - PUBLISH_IOS: { - en: 'Publish your game on iOS (App Store)', - }, - REMOVE_MANDATORY_LOGO: { - en: 'Remove GDevelop logo/watermark', - }, - VERSION_HISTORY: { - en: 'Version history', - }, - COLLABORATORS: { - en: 'Collaborator (project sharing)', - }, - INVOICES: { - en: 'Invoices for your company', - }, - LEADERBOARDS: { - en: 'Leaderboards for games', - }, - LEADERBOARDS_STYLING: { - en: 'Leaderboards styling', - }, - ANALYTICS: { - en: 'Analytics on game usage', - }, - FEEDBACKS: { - en: 'Player feedbacks', - }, - MULTIPLAYER: { - en: 'Multiplayer', - }, - EMBED_GD_GAMES: { - en: 'Embed game on any website in one click', - }, - DEDICATED_SUPPORT_CHANNEL: { - en: 'Dedicated professional support channel', - }, - DEDICATED_COMMUNITY_CHANNEL: { - en: 'Dedicated community channel', - }, - FREE_CREDITS: { - en: 'Free credits every month', - }, - FEATURING_TIER1: { - en: 'Featuring on gd.games', - }, - FEATURING_TIER2: { - en: 'Featuring on social/newsletter', - }, - FEATURING_TIER3: { - en: 'Featuring inside GDevelop', - }, - GET_FREE_ASSET_PACKS: { - en: 'Use thousands of free asset packs', - }, - CLAIM_PREMIUM_ASSET_PACKS: { - en: 'Claim a premium asset pack or template', - }, - BUY_PREMIUM_ASSET_PACKS: { - en: 'Buy premium asset packs', - }, - ADMOB_ADS: { - en: 'Show ads in games (Admob for Android)', - }, - COMMUNITY_IAP: { - en: 'In-App Purchases (community extension)', - }, - SELL_GAMES: { - en: 'Sell your games (Steam, App Stores...)', - }, - SELL_ON_ASSET_STORE: { - en: 'Sell your assets or templates on GDevelop Asset Store', - }, - }, - descriptionByLocale: { - en: 'Full professional features.', - }, - bulletPointsByLocale: [ - { - en: 'Dedicated support channel on Discord for pros', - }, - { - en: 'Collaboration: share projects with teammates', - }, - { - en: 'Version history for projects', - }, - { - en: 'Unlimited cloud projects', - }, - { - en: 'Access to the upcoming pro marketplace', - }, - { - en: 'Billing/invoices available', - }, - ], - fullFeatures: [ - { - featureName: 'ENGINE_AND_EDITOR', - pillarName: 'CREATION', - enabled: 'yes', - }, - { - featureName: 'DEBUGGER_AND_LIVE_PREVIEW', - pillarName: 'CREATION', - enabled: 'yes', - }, - { - featureName: 'CLOUD_PROJECTS', - pillarName: 'CREATION', - unlimited: true, - }, - { - featureName: 'MOBILE_APP', - pillarName: 'CREATION', - enabled: 'yes', - }, - { - featureName: 'AI_PROTOTYPING', - pillarName: 'CREATION', - unlimited: true, - }, - { - featureName: 'PUBLISH_GD_GAMES', - pillarName: 'PUBLISHING', - unlimited: true, - }, - { - featureName: 'PUBLISH_DESKTOP', - pillarName: 'PUBLISHING', - unlimited: true, - }, - { - featureName: 'PUBLISH_ANDROID', - pillarName: 'PUBLISHING', - unlimited: true, - }, - { - featureName: 'PUBLISH_IOS', - pillarName: 'PUBLISHING', - descriptionByLocale: { - en: '15 per month', - }, - }, - { - featureName: 'MANUAL_EXPORT', - pillarName: 'PUBLISHING', - enabled: 'yes', - }, - { - featureName: 'REMOVE_MANDATORY_LOGO', - pillarName: 'PUBLISHING', - enabled: 'yes', - }, - { - featureName: 'VERSION_HISTORY', - pillarName: 'PRO', - enabled: 'yes', - }, - { - featureName: 'COLLABORATORS', - pillarName: 'PRO', - enabled: 'yes', - }, - { - featureName: 'INVOICES', - pillarName: 'PRO', - enabled: 'yes', - }, - { - featureName: 'FEEDBACKS', - pillarName: 'SOCIAL', - unlimited: true, - }, - { - featureName: 'LEADERBOARDS', - pillarName: 'SOCIAL', - unlimited: true, - }, - { - featureName: 'LEADERBOARDS_STYLING', - pillarName: 'SOCIAL', - descriptionByLocale: { - en: 'Custom CSS/styling', - }, - }, - { - featureName: 'ANALYTICS', - pillarName: 'SOCIAL', - enabled: 'yes', - }, - { - featureName: 'MULTIPLAYER', - pillarName: 'SOCIAL', - enabled: 'yes', - upcoming: true, - }, - { - featureName: 'EMBED_GD_GAMES', - upcoming: true, - pillarName: 'SOCIAL', - enabled: 'yes', - }, - { - featureName: 'DEDICATED_COMMUNITY_CHANNEL', - pillarName: 'SUPPORT', - enabled: 'yes', - }, - { - featureName: 'DEDICATED_SUPPORT_CHANNEL', - pillarName: 'SUPPORT', - enabled: 'yes', - }, - { - featureName: 'FREE_CREDITS', - pillarName: 'MARKETING', - descriptionByLocale: { - en: '1000 per month', - }, - }, - { - featureName: 'FEATURING_TIER1', - pillarName: 'MARKETING', - descriptionByLocale: { - en: 'Buy with credits', - }, - }, - { - featureName: 'FEATURING_TIER2', - pillarName: 'MARKETING', - descriptionByLocale: { - en: 'Buy with credits', - }, - }, - { - featureName: 'FEATURING_TIER3', - pillarName: 'MARKETING', - descriptionByLocale: { - en: 'Buy with credits', - }, - }, - { - featureName: 'GET_FREE_ASSET_PACKS', - pillarName: 'ASSET-STORE', - enabled: 'yes', - }, - { - featureName: 'BUY_PREMIUM_ASSET_PACKS', - pillarName: 'ASSET-STORE', - enabled: 'yes', - }, - { - featureName: 'CLAIM_PREMIUM_ASSET_PACKS', - pillarName: 'ASSET-STORE', - descriptionByLocale: { - en: '1 per month', - }, - }, - { - featureName: 'ADMOB_ADS', - pillarName: 'MONETIZATION', - enabled: 'yes', - }, - { - featureName: 'COMMUNITY_IAP', - pillarName: 'MONETIZATION', - enabled: 'yes', - }, - { - featureName: 'SELL_GAMES', - pillarName: 'MONETIZATION', - enabled: 'yes', - }, - { - featureName: 'SELL_ON_ASSET_STORE', - pillarName: 'MONETIZATION', - descriptionByLocale: { - en: 'Yes - get in touch', - }, - }, - ], - pricingSystems: [ - { - id: 'startup_1month_3000EUR', - planId: 'gdevelop_startup', - status: 'active', - period: 'month', - periodCount: 1, - currency: 'EUR', - amountInCents: 3000, - region: 'eurozone', - }, - { - id: 'startup_1year_30900EUR', - planId: 'gdevelop_startup', - status: 'active', - period: 'year', - periodCount: 1, - currency: 'EUR', - amountInCents: 30900, - region: 'eurozone', - }, - ], - }, - { - id: 'gdevelop_education', - isLegacy: false, - nameByLocale: { - en: 'Education', - }, - targetAudiences: ['EDUCATION'], - pillarNamesPerLocale: { - 'ASSET-STORE': { - en: 'Asset Store', - }, - CREATION: { - en: 'Creation', - }, - MARKETING: { - en: 'Marketing & Promotion', - }, - PRO: { - en: 'Professional features', - }, - PUBLISHING: { - en: 'Publishing', - }, - SOCIAL: { - en: 'Social & Online', - }, - SUPPORT: { - en: 'Community & Support', - }, - MONETIZATION: { - en: 'Monetization', - }, - }, - featureNamesByLocale: { - ENGINE_AND_EDITOR: { - en: 'Open-source (MIT) editor and game engine', - }, - DEBUGGER_AND_LIVE_PREVIEW: { - en: 'Debugger and live preview', - }, - CLOUD_PROJECTS: { - en: 'Cloud projects', - }, - AI_PROTOTYPING: { - en: 'Game prototypes with AI', - }, - MOBILE_APP: { - en: 'Mobile app access', - }, - MANUAL_EXPORT: { - en: 'Export (any platform) manually with devtools', - }, - PUBLISH_GD_GAMES: { - en: 'Publish your game on gd.games', - }, - PUBLISH_DESKTOP: { - en: 'Publish for desktop (Windows, macOS, Linux)', - }, - PUBLISH_ANDROID: { - en: 'Publish your game on Google Play (Android)', - }, - PUBLISH_IOS: { - en: 'Publish your game on iOS (App Store)', - }, - REMOVE_MANDATORY_LOGO: { - en: 'Remove GDevelop logo/watermark', - }, - VERSION_HISTORY: { - en: 'Version history', - }, - COLLABORATORS: { - en: 'Collaborator (project sharing)', - }, - INVOICES: { - en: 'Invoices for your company', - }, - LEADERBOARDS: { - en: 'Leaderboards for games', - }, - LEADERBOARDS_STYLING: { - en: 'Leaderboards styling', - }, - ANALYTICS: { - en: 'Analytics on game usage', - }, - FEEDBACKS: { - en: 'Player feedbacks', - }, - MULTIPLAYER: { - en: 'Multiplayer', - }, - EMBED_GD_GAMES: { - en: 'Embed game on any website in one click', - }, - DEDICATED_SUPPORT_CHANNEL: { - en: 'Dedicated professional support channel', - }, - DEDICATED_COMMUNITY_CHANNEL: { - en: 'Dedicated community channel', - }, - FREE_CREDITS: { - en: 'Free credits every month', - }, - FEATURING_TIER1: { - en: 'Featuring on gd.games', - }, - FEATURING_TIER2: { - en: 'Featuring on social/newsletter', - }, - FEATURING_TIER3: { - en: 'Featuring inside GDevelop', - }, - GET_FREE_ASSET_PACKS: { - en: 'Use thousands of free asset packs', - }, - CLAIM_PREMIUM_ASSET_PACKS: { - en: 'Claim a premium asset pack or template', - }, - BUY_PREMIUM_ASSET_PACKS: { - en: 'Buy premium asset packs', - }, - ADMOB_ADS: { - en: 'Show ads in games (Admob for Android)', - }, - COMMUNITY_IAP: { - en: 'In-App Purchases (community extension)', - }, - SELL_GAMES: { - en: 'Sell your games (Steam, App Stores...)', - }, - SELL_ON_ASSET_STORE: { - en: 'Sell your assets or templates on GDevelop Asset Store', - }, - }, - descriptionByLocale: { - en: 'Teach logic, coding and game creation.', - }, - bulletPointsByLocale: [ - { - en: 'Full anonymous accounts for students', - 'ar-SA': 'سرية الطلاب.', - 'de-DE': 'Anonymität der Schüler.', - 'es-ES': 'Anonimato de los estudiantes.', - 'fr-FR': 'Anonymat des étudiants.', - 'it-IT': 'Anonimato degli studenti.', - 'ja-JP': '学生の匿名性。', - 'ko-KR': '학생 익명성.', - 'pl-PL': 'Anonimowość uczniów.', - 'pt-BR': 'Anonimato dos alunos.', - 'ru-RU': 'Анонимность студентов.', - 'sl-SI': 'Anonimnost študentov.', - 'uk-UA': 'Анонімність студентів.', - 'zh-CN': '学生匿名性。', - }, - { - en: 'NDPA or custom privacy agreements', - }, - { - en: 'Organize students per classroom', - 'ar-SA': 'تنظيم الطلاب حسب الصف.', - 'de-DE': 'Schüler pro Klassenzimmer organisieren.', - 'es-ES': 'Organizar estudiantes por aulas.', - 'fr-FR': 'Organiser les étudiants par classe.', - 'it-IT': 'Organizza gli studenti per classe.', - 'ja-JP': 'クラスごとに学生を組織化。', - 'ko-KR': '학생을 강의실별로 정리하십시오.', - 'pl-PL': 'Organizuj uczniów według sal lekcyjnych.', - 'pt-BR': 'Organize os alunos por sala de aula.', - 'ru-RU': 'Организация студентов по классам.', - 'sl-SI': 'Organizacija študentov po učilnicah.', - 'uk-UA': 'Організація студентів за класами.', - 'zh-CN': '根据教室组织学生。', - }, - { - en: 'Access students projects', - 'ar-SA': 'الوصول إلى مشاريع الطلاب الخاصة بك', - 'de-DE': 'Zugriff auf die Projekte Ihrer Schüler.', - 'es-ES': 'Acceda a los proyectos de sus estudiantes.', - 'fr-FR': 'Accédez aux projets de vos étudiants.', - 'it-IT': 'Accedi ai progetti dei tuoi studenti.', - 'ja-JP': '生徒のプロジェクトにアクセス。', - 'ko-KR': '학생 프로젝트에 액세스하십시오.', - 'pl-PL': 'Dostęp do projektów uczniów.', - 'pt-BR': 'Acesse os projetos dos seus alunos.', - 'ru-RU': 'Доступ к проектам ваших студентов.', - 'sl-SI': 'Dostop do projektov vaših študentov.', - 'uk-UA': 'Доступ до проектів ваших студентів.', - 'zh-CN': '访问您的学生项目。', - }, - { - en: 'Version history for all projects', - }, - { - en: 'Publishing to any platform', - }, - { - en: 'GDevelop mobile app full access', - }, - ], - fullFeatures: [], - pricingSystems: [ - { - id: 'education_1month_299EUR', - planId: 'gdevelop_education', - status: 'active', - period: 'month', - periodCount: 1, - currency: 'EUR', - amountInCents: 299, - isPerUser: true, - region: 'eurozone', - }, - { - id: 'education_1year_2999EUR', - planId: 'gdevelop_education', - status: 'active', - period: 'year', - periodCount: 1, - currency: 'EUR', - amountInCents: 2999, - isPerUser: true, - region: 'eurozone', - }, - ], - }, - { - id: 'gdevelop_indie', - isLegacy: true, - nameByLocale: { - en: 'Silver (Legacy)', - }, - targetAudiences: ['CASUAL'], - pillarNamesPerLocale: { - 'ASSET-STORE': { - en: 'Asset Store', - }, - CREATION: { - en: 'Creation', - }, - MARKETING: { - en: 'Marketing & Promotion', - }, - PRO: { - en: 'Professional features', - }, - PUBLISHING: { - en: 'Publishing', - }, - SOCIAL: { - en: 'Social & Online', - }, - SUPPORT: { - en: 'Community & Support', - }, - MONETIZATION: { - en: 'Monetization', - }, - }, - featureNamesByLocale: { - ENGINE_AND_EDITOR: { - en: 'Open-source (MIT) editor and game engine', - }, - DEBUGGER_AND_LIVE_PREVIEW: { - en: 'Debugger and live preview', - }, - CLOUD_PROJECTS: { - en: 'Cloud projects', - }, - AI_PROTOTYPING: { - en: 'Game prototypes with AI', - }, - MOBILE_APP: { - en: 'Mobile app access', - }, - MANUAL_EXPORT: { - en: 'Export (any platform) manually with devtools', - }, - PUBLISH_GD_GAMES: { - en: 'Publish your game on gd.games', - }, - PUBLISH_DESKTOP: { - en: 'Publish for desktop (Windows, macOS, Linux)', - }, - PUBLISH_ANDROID: { - en: 'Publish your game on Google Play (Android)', - }, - PUBLISH_IOS: { - en: 'Publish your game on iOS (App Store)', - }, - REMOVE_MANDATORY_LOGO: { - en: 'Remove GDevelop logo/watermark', - }, - VERSION_HISTORY: { - en: 'Version history', - }, - COLLABORATORS: { - en: 'Collaborator (project sharing)', - }, - INVOICES: { - en: 'Invoices for your company', - }, - LEADERBOARDS: { - en: 'Leaderboards for games', - }, - LEADERBOARDS_STYLING: { - en: 'Leaderboards styling', - }, - ANALYTICS: { - en: 'Analytics on game usage', - }, - FEEDBACKS: { - en: 'Player feedbacks', - }, - MULTIPLAYER: { - en: 'Multiplayer', - }, - EMBED_GD_GAMES: { - en: 'Embed game on any website in one click', - }, - DEDICATED_SUPPORT_CHANNEL: { - en: 'Dedicated professional support channel', - }, - DEDICATED_COMMUNITY_CHANNEL: { - en: 'Dedicated community channel', - }, - FREE_CREDITS: { - en: 'Free credits every month', - }, - FEATURING_TIER1: { - en: 'Featuring on gd.games', - }, - FEATURING_TIER2: { - en: 'Featuring on social/newsletter', - }, - FEATURING_TIER3: { - en: 'Featuring inside GDevelop', - }, - GET_FREE_ASSET_PACKS: { - en: 'Use thousands of free asset packs', - }, - CLAIM_PREMIUM_ASSET_PACKS: { - en: 'Claim a premium asset pack or template', - }, - BUY_PREMIUM_ASSET_PACKS: { - en: 'Buy premium asset packs', - }, - ADMOB_ADS: { - en: 'Show ads in games (Admob for Android)', - }, - COMMUNITY_IAP: { - en: 'In-App Purchases (community extension)', - }, - SELL_GAMES: { - en: 'Sell your games (Steam, App Stores...)', - }, - SELL_ON_ASSET_STORE: { - en: 'Sell your assets or templates on GDevelop Asset Store', - }, - }, - descriptionByLocale: { - en: 'Build more and faster', - }, - bulletPointsByLocale: [ - { - en: - '50 cloud projects with 250MB of resources per project and 3-month version history.', - }, - { - en: '10 packagings per day for Android and for desktop.', - }, - { - en: 'Unlimited leaderboards and unlimited player feedback responses.', - }, - ], - fullFeatures: [], - pricingSystems: [ - { - id: 'indie_1month', - planId: 'gdevelop_indie', - status: 'active', - period: 'month', - periodCount: 1, - currency: 'EUR', - amountInCents: 200, - region: 'default', - }, - ], - }, - { - id: 'gdevelop_pro', - isLegacy: true, - nameByLocale: { - en: 'Gold (Legacy)', - }, - targetAudiences: ['CASUAL'], - pillarNamesPerLocale: { - 'ASSET-STORE': { - en: 'Asset Store', - }, - CREATION: { - en: 'Creation', - }, - MARKETING: { - en: 'Marketing & Promotion', - }, - PRO: { - en: 'Professional features', - }, - PUBLISHING: { - en: 'Publishing', - }, - SOCIAL: { - en: 'Social & Online', - }, - SUPPORT: { - en: 'Community & Support', - }, - MONETIZATION: { - en: 'Monetization', - }, - }, - featureNamesByLocale: { - ENGINE_AND_EDITOR: { - en: 'Open-source (MIT) editor and game engine', - }, - DEBUGGER_AND_LIVE_PREVIEW: { - en: 'Debugger and live preview', - }, - CLOUD_PROJECTS: { - en: 'Cloud projects', - }, - AI_PROTOTYPING: { - en: 'Game prototypes with AI', - }, - MOBILE_APP: { - en: 'Mobile app access', - }, - MANUAL_EXPORT: { - en: 'Export (any platform) manually with devtools', - }, - PUBLISH_GD_GAMES: { - en: 'Publish your game on gd.games', - }, - PUBLISH_DESKTOP: { - en: 'Publish for desktop (Windows, macOS, Linux)', - }, - PUBLISH_ANDROID: { - en: 'Publish your game on Google Play (Android)', - }, - PUBLISH_IOS: { - en: 'Publish your game on iOS (App Store)', - }, - REMOVE_MANDATORY_LOGO: { - en: 'Remove GDevelop logo/watermark', - }, - VERSION_HISTORY: { - en: 'Version history', - }, - COLLABORATORS: { - en: 'Collaborator (project sharing)', - }, - INVOICES: { - en: 'Invoices for your company', - }, - LEADERBOARDS: { - en: 'Leaderboards for games', - }, - LEADERBOARDS_STYLING: { - en: 'Leaderboards styling', - }, - ANALYTICS: { - en: 'Analytics on game usage', - }, - FEEDBACKS: { - en: 'Player feedbacks', - }, - MULTIPLAYER: { - en: 'Multiplayer', - }, - EMBED_GD_GAMES: { - en: 'Embed game on any website in one click', - }, - DEDICATED_SUPPORT_CHANNEL: { - en: 'Dedicated professional support channel', - }, - DEDICATED_COMMUNITY_CHANNEL: { - en: 'Dedicated community channel', - }, - FREE_CREDITS: { - en: 'Free credits every month', - }, - FEATURING_TIER1: { - en: 'Featuring on gd.games', - }, - FEATURING_TIER2: { - en: 'Featuring on social/newsletter', - }, - FEATURING_TIER3: { - en: 'Featuring inside GDevelop', - }, - GET_FREE_ASSET_PACKS: { - en: 'Use thousands of free asset packs', - }, - CLAIM_PREMIUM_ASSET_PACKS: { - en: 'Claim a premium asset pack or template', - }, - BUY_PREMIUM_ASSET_PACKS: { - en: 'Buy premium asset packs', - }, - ADMOB_ADS: { - en: 'Show ads in games (Admob for Android)', - }, - COMMUNITY_IAP: { - en: 'In-App Purchases (community extension)', - }, - SELL_GAMES: { - en: 'Sell your games (Steam, App Stores...)', - }, - SELL_ON_ASSET_STORE: { - en: 'Sell your assets or templates on GDevelop Asset Store', - }, - }, - descriptionByLocale: { - en: 'Experimented creators, ambitious games', - }, - bulletPointsByLocale: [ - { - en: - '100 cloud projects with 500MB of resources per project and one-year version history.', - }, - { - en: '70 packagings per day for Android and for desktop.', - }, - { - en: 'Unlimited leaderboards and unlimited player feedback responses.', - }, - { - en: - 'Immerse your players by removing GDevelop logo when the game loads.', - }, - ], - fullFeatures: [], - pricingSystems: [ - { - id: 'pro_1month', - planId: 'gdevelop_pro', - status: 'active', - period: 'month', - periodCount: 1, - currency: 'EUR', - amountInCents: 700, - region: 'default', - }, - ], - }, -]; - const prices = { 'cordova-build': { priceInCredits: 50, diff --git a/newIDE/app/src/stories/componentStories/HomePage/GetStartedSection/RecommendationList.stories.js b/newIDE/app/src/stories/componentStories/HomePage/GetStartedSection/RecommendationList.stories.js index d9570713dfe8..5a7d7bbd5837 100644 --- a/newIDE/app/src/stories/componentStories/HomePage/GetStartedSection/RecommendationList.stories.js +++ b/newIDE/app/src/stories/componentStories/HomePage/GetStartedSection/RecommendationList.stories.js @@ -2,17 +2,17 @@ import * as React from 'react'; import { action } from '@storybook/addon-actions'; import paperDecorator from '../../../PaperDecorator'; -import { - fakeAuthenticatedUserWithGitHubStarBadge, - fakeAuthenticatedUserWithNoSubscription, - subscriptionPlansWithPricingSystems, -} from '../../../../fixtures/GDevelopServicesTestData'; +import { fakeAuthenticatedUserWithNoSubscription } from '../../../../fixtures/GDevelopServicesTestData'; import RecommendationList from '../../../../MainFrame/EditorContainers/HomePage/GetStartedSection/RecommendationList'; import PreferencesContext, { initialPreferences, } from '../../../../MainFrame/Preferences/PreferencesContext'; import inAppTutorialDecorator from '../../../InAppTutorialDecorator'; import { TutorialStateProvider } from '../../../../Tutorial/TutorialContext'; +import useSubscriptionPlans, { + getAvailableSubscriptionPlansWithPrices, +} from '../../../../Utils/UseSubscriptionPlans'; +import LoaderModal from '../../../../UI/LoaderModal'; export default { title: 'HomePage/GetStartedSectionSection/RecommendationList', @@ -20,78 +20,85 @@ export default { decorators: [paperDecorator, inAppTutorialDecorator], }; -export const Default = () => ( - - - true} - onCreateProjectFromExample={action('onCreateProjectFromExample')} - /> - - -); +export const Default = () => { + const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({ + includeLegacy: true, + authenticatedUser: fakeAuthenticatedUserWithNoSubscription, + }); + return subscriptionPlansWithPricingSystems ? ( + + + true} + onCreateProjectFromExample={action('onCreateProjectFromExample')} + /> + + + ) : ( + + ); +}; + +export const WithSurvey = () => { + const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({ + includeLegacy: true, + authenticatedUser: fakeAuthenticatedUserWithNoSubscription, + }); -export const WithGitHubStarAlreadyMade = () => ( - - - true} - onCreateProjectFromExample={action('onCreateProjectFromExample')} - /> - - -); + return subscriptionPlansWithPricingSystems ? ( + + + true} + onCreateProjectFromExample={action('onCreateProjectFromExample')} + /> + + + ) : ( + + ); +}; -export const WithSurvey = () => ( - - - true} - onCreateProjectFromExample={action('onCreateProjectFromExample')} - /> - - -); +export const WithSurveyAlreadyFilled = () => { + const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({ + includeLegacy: true, + authenticatedUser: fakeAuthenticatedUserWithNoSubscription, + }); -export const WithSurveyAlreadyFilled = () => ( - - - true} - onCreateProjectFromExample={action('onCreateProjectFromExample')} - /> - - -); + return subscriptionPlansWithPricingSystems ? ( + + + true} + onCreateProjectFromExample={action('onCreateProjectFromExample')} + /> + + + ) : ( + + ); +}; diff --git a/newIDE/app/src/stories/componentStories/Profile/Subscription/PromotionSubscriptionDialog.stories.js b/newIDE/app/src/stories/componentStories/Profile/Subscription/PromotionSubscriptionDialog.stories.js index 4b56e7518908..fd412afc8653 100644 --- a/newIDE/app/src/stories/componentStories/Profile/Subscription/PromotionSubscriptionDialog.stories.js +++ b/newIDE/app/src/stories/componentStories/Profile/Subscription/PromotionSubscriptionDialog.stories.js @@ -7,11 +7,13 @@ import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserConte import { fakeAuthenticatedUserWithNoSubscription, fakeNotAuthenticatedUser, - subscriptionPlansWithPricingSystems, } from '../../../../fixtures/GDevelopServicesTestData'; import PromotionSubscriptionDialog from '../../../../Profile/Subscription/PromotionSubscriptionDialog'; import AlertProvider from '../../../../UI/Alert/AlertProvider'; -import { getAvailableSubscriptionPlansWithPrices } from '../../../../Utils/UseSubscriptionPlans'; +import useSubscriptionPlans, { + getAvailableSubscriptionPlansWithPrices, +} from '../../../../Utils/UseSubscriptionPlans'; +import LoaderModal from '../../../../UI/LoaderModal'; export default { title: 'Subscription/PromotionSubscriptionDialog', @@ -19,122 +21,155 @@ export default { decorators: [paperDecorator], }; -const availableSubscriptionPlansWithPrices = getAvailableSubscriptionPlansWithPrices( - subscriptionPlansWithPricingSystems -); - export const NotAuthenticatedSilverRecommended = () => { - return ( + const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({ + includeLegacy: true, + authenticatedUser: fakeNotAuthenticatedUser, + }); + return subscriptionPlansWithPricingSystems ? ( action('on close')()} recommendedPlanId="gdevelop_silver" onOpenPendingDialog={() => action('on open pending dialog')()} /> + ) : ( + ); }; export const AuthenticatedSilverRecommended = () => { - return ( + const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({ + includeLegacy: true, + authenticatedUser: fakeAuthenticatedUserWithNoSubscription, + }); + return subscriptionPlansWithPricingSystems ? ( action('on close')()} recommendedPlanId="gdevelop_silver" onOpenPendingDialog={() => action('on open pending dialog')()} /> + ) : ( + ); }; export const GoldRecommended = () => { - return ( + const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({ + includeLegacy: true, + authenticatedUser: fakeAuthenticatedUserWithNoSubscription, + }); + return subscriptionPlansWithPricingSystems ? ( action('on close')()} recommendedPlanId="gdevelop_gold" onOpenPendingDialog={() => action('on open pending dialog')()} /> + ) : ( + ); }; export const ProRecommended = () => { - return ( + const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({ + includeLegacy: true, + authenticatedUser: fakeAuthenticatedUserWithNoSubscription, + }); + return subscriptionPlansWithPricingSystems ? ( action('on close')()} recommendedPlanId="gdevelop_startup" onOpenPendingDialog={() => action('on open pending dialog')()} /> + ) : ( + ); }; export const EducationRecommended = () => { - return ( + const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({ + includeLegacy: true, + authenticatedUser: fakeAuthenticatedUserWithNoSubscription, + }); + return subscriptionPlansWithPricingSystems ? ( action('on close')()} recommendedPlanId="gdevelop_education" onOpenPendingDialog={() => action('on open pending dialog')()} /> + ) : ( + ); }; export const WithoutSilverButRecommended = () => { - const availableSubscriptionPlansWithPrices = getAvailableSubscriptionPlansWithPrices( - subscriptionPlansWithPricingSystems - ); - const subscritionPlansWithoutSilver = availableSubscriptionPlansWithPrices.filter( - plan => plan.id !== 'gdevelop_silver' - ); - return ( + const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({ + includeLegacy: true, + authenticatedUser: fakeAuthenticatedUserWithNoSubscription, + }); + const subscritionPlansWithoutSilver = subscriptionPlansWithPricingSystems + ? subscriptionPlansWithPricingSystems.filter( + plan => plan.id !== 'gdevelop_silver' + ) + : null; + return subscritionPlansWithoutSilver ? ( action('on close')()} recommendedPlanId="gdevelop_silver" onOpenPendingDialog={() => action('on open pending dialog')()} /> + ) : ( + ); }; diff --git a/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionDetails.stories.js b/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionDetails.stories.js index fd8d2a3990b2..a113306dcc27 100644 --- a/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionDetails.stories.js +++ b/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionDetails.stories.js @@ -4,7 +4,6 @@ import { action } from '@storybook/addon-actions'; import paperDecorator from '../../../PaperDecorator'; import { - subscriptionPlansWithPricingSystems, fakeNotAuthenticatedUser, fakeAuthenticatedUserLoggingIn, fakeAuthenticatedUserWithNoSubscription, @@ -21,6 +20,10 @@ import { import subscriptionSuggestionDecorator from '../../../SubscriptionSuggestionDecorator'; import SubscriptionDetails from '../../../../Profile/Subscription/SubscriptionDetails'; import AlertProvider from '../../../../UI/Alert/AlertProvider'; +import useSubscriptionPlans, { + getAvailableSubscriptionPlansWithPrices, +} from '../../../../Utils/UseSubscriptionPlans'; +import LoaderModal from '../../../../UI/LoaderModal'; export default { title: 'Subscription/SubscriptionDetails', @@ -181,18 +184,24 @@ export const Default = ({ } const { subscription: userSubscription } = authenticatedUser; + const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({ + includeLegacy: true, + authenticatedUser, + }); - return ( + return subscriptionPlansWithPricingSystems ? ( + ) : ( + ); }; diff --git a/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionDialog.stories.js b/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionDialog.stories.js index 0614b724a0fb..ee00e56f9e98 100644 --- a/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionDialog.stories.js +++ b/newIDE/app/src/stories/componentStories/Profile/Subscription/SubscriptionDialog.stories.js @@ -14,11 +14,13 @@ import { fakeAuthenticatedUserWithLegacyProSubscription, fakeAuthenticatedUserWithEducationPlan, fakeStartupAuthenticatedUser, - subscriptionPlansWithPricingSystems, } from '../../../../fixtures/GDevelopServicesTestData'; import SubscriptionDialog from '../../../../Profile/Subscription/SubscriptionDialog'; import AlertProvider from '../../../../UI/Alert/AlertProvider'; -import { getAvailableSubscriptionPlansWithPrices } from '../../../../Utils/UseSubscriptionPlans'; +import useSubscriptionPlans, { + getAvailableSubscriptionPlansWithPrices, +} from '../../../../Utils/UseSubscriptionPlans'; +import LoaderModal from '../../../../UI/LoaderModal'; export default { title: 'Subscription/SubscriptionDialog', @@ -159,16 +161,22 @@ export const Default = ({ } } + const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({ + includeLegacy: true, + authenticatedUser, + }); + const { subscription: userSubscription } = authenticatedUser; - const userLegacySubscriptionPlanWithPricingSystem = userSubscription - ? subscriptionPlansWithPricingSystems.find( - planWithPricingSystem => - planWithPricingSystem.id === userSubscription.planId && - planWithPricingSystem.isLegacy - ) - : null; + const userLegacySubscriptionPlanWithPricingSystem = + userSubscription && subscriptionPlansWithPricingSystems + ? subscriptionPlansWithPricingSystems.find( + planWithPricingSystem => + planWithPricingSystem.id === userSubscription.planId && + planWithPricingSystem.isLegacy + ) + : null; - return ( + return subscriptionPlansWithPricingSystems ? ( + ) : ( + ); }; From 88b9f6ed8291d56691cbefe32c9bfec3b7260ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pasteau?= <4895034+ClementPasteau@users.noreply.github.com> Date: Fri, 17 Jan 2025 10:24:52 +0100 Subject: [PATCH 6/7] Improve design and handle features-not-enabled --- .../PromotionSubscriptionPlan.js | 5 +++- .../SubscriptionOptions.js | 24 +++++++++---------- .../SubscriptionOptions.module.css | 7 +++++- .../PromotionSubscriptionDialog.stories.js | 6 ++--- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/PromotionSubscriptionPlan.js b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/PromotionSubscriptionPlan.js index f4c0cb81a606..d684fa31ef88 100644 --- a/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/PromotionSubscriptionPlan.js +++ b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/PromotionSubscriptionPlan.js @@ -34,6 +34,7 @@ import DiscountFlame from '../../../UI/HotMessage/DiscountFlame'; import ThumbsUp from '../../../UI/CustomSvgIcons/ThumbsUp'; import { useResponsiveWindowSize } from '../../../UI/Responsive/ResponsiveWindowMeasurer'; import SemiControlledTextField from '../../../UI/SemiControlledTextField'; +import CircledClose from '../../../UI/CustomSvgIcons/CircledClose'; const styles = { simpleSizeContainer: { @@ -214,13 +215,15 @@ const PromotionSubscriptionPlan = ({ {summarizedFeature.displayedFeatureName}
- {summarizedFeature.enabled ? ( + {summarizedFeature.enabled === 'yes' ? ( + ) : summarizedFeature.enabled === 'no' ? ( + ) : summarizedFeature.unlimited ? (
{ - const { windowSize } = useResponsiveWindowSize(); - const isMobileOrMediumScreen = - windowSize === 'small' || windowSize === 'medium'; + const { windowSize, isMobile } = useResponsiveWindowSize(); + const isMobileOrMediumScreen = isMobile || windowSize === 'medium'; const gdevelopTheme = React.useContext(GDevelopThemeContext); return (
{recommended && ( -
+
{isMobileOrMediumScreen ? ( Recommended @@ -113,7 +111,7 @@ const SubscriptionOptions = ({ return ( {({ i18n }) => ( -
+ {subscriptionPlansWithPricingSystems.map( (subscriptionPlanWithPricingSystems, index) => { const isSelected = @@ -169,7 +167,7 @@ const SubscriptionOptions = ({ ); } )} -
+ )}
); diff --git a/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/SubscriptionOptions.module.css b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/SubscriptionOptions.module.css index 8d4e6baebe53..4bf5b34ce978 100644 --- a/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/SubscriptionOptions.module.css +++ b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/SubscriptionOptions.module.css @@ -1,6 +1,8 @@ .optionAndBadgeContainer { position: relative; height: 100%; + padding-top: 4px; + padding-bottom: 4px; } .optionContainer { @@ -35,11 +37,14 @@ .recommendedBadge { position: absolute; - top: -12px; + top: -8px; right: 10px; background: var(--theme-message-hot-background-color); color: var(--theme-message-hot-color); padding: 2px 8px; border-radius: 16px; z-index: 2; +} +.recommendedBadge.mobile { + top: -5px; } \ No newline at end of file diff --git a/newIDE/app/src/stories/componentStories/Profile/Subscription/PromotionSubscriptionDialog.stories.js b/newIDE/app/src/stories/componentStories/Profile/Subscription/PromotionSubscriptionDialog.stories.js index fd412afc8653..8e62e3ba5eb9 100644 --- a/newIDE/app/src/stories/componentStories/Profile/Subscription/PromotionSubscriptionDialog.stories.js +++ b/newIDE/app/src/stories/componentStories/Profile/Subscription/PromotionSubscriptionDialog.stories.js @@ -149,19 +149,19 @@ export const WithoutSilverButRecommended = () => { includeLegacy: true, authenticatedUser: fakeAuthenticatedUserWithNoSubscription, }); - const subscritionPlansWithoutSilver = subscriptionPlansWithPricingSystems + const subscriptionPlansWithoutSilver = subscriptionPlansWithPricingSystems ? subscriptionPlansWithPricingSystems.filter( plan => plan.id !== 'gdevelop_silver' ) : null; - return subscritionPlansWithoutSilver ? ( + return subscriptionPlansWithoutSilver ? ( action('on close')()} recommendedPlanId="gdevelop_silver" From c4b1157abca224b175f99e99809f92b936abdb1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pasteau?= <4895034+ClementPasteau@users.noreply.github.com> Date: Fri, 17 Jan 2025 10:31:18 +0100 Subject: [PATCH 7/7] Slighlty fix spacing --- .../PromotionSubscriptionDialog/index.js | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/index.js b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/index.js index 8f24d3de69b6..bf06462abc4b 100644 --- a/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/index.js +++ b/newIDE/app/src/Profile/Subscription/PromotionSubscriptionDialog/index.js @@ -181,23 +181,25 @@ export default function PromotionSubscriptionDialog({ )} - - - ❤️ - - Support What You Love + + + + ❤️ + + Support What You Love + + + + + The GDevelop project is open-source, powered by passion + and community. Your membership helps the GDevelop + company maintain servers, build new features, develop + commercial offerings and keep the open-source project + thriving. Our goal: make game development fast, fun and + accessible to all. + - - - - The GDevelop project is open-source, powered by passion - and community. Your membership helps the GDevelop company - maintain servers, build new features, develop commercial - offerings and keep the open-source project thriving. Our - goal: make game development fast, fun and accessible to - all. - - + {getPlanSpecificRequirements( i18n, subscriptionPlansWithPricingSystems @@ -209,7 +211,7 @@ export default function PromotionSubscriptionDialog({ {planSpecificRequirements} ))} - +