Compare commits

...

1 Commits

Author SHA1 Message Date
Florian Rival
cb2fad173c Lazily load subscription plans to reduce requests at startup 2025-06-03 13:57:32 +02:00
16 changed files with 302 additions and 467 deletions

View File

@@ -1,185 +0,0 @@
// @flow
import * as React from 'react';
import { t, Trans } from '@lingui/macro';
import { I18n as I18nType } from '@lingui/core';
import { useResponsiveWindowSize } from '../../../../UI/Responsive/ResponsiveWindowMeasurer';
import GDevelopThemeContext from '../../../../UI/Theme/GDevelopThemeContext';
import { type SubscriptionPlanWithPricingSystems } from '../../../../Utils/GDevelopServices/Usage';
import { Column, Line } from '../../../../UI/Grid';
import Paper from '../../../../UI/Paper';
import {
ColumnStackLayout,
ResponsiveLineStackLayout,
} from '../../../../UI/Layout';
import Text from '../../../../UI/Text';
import CheckCircle from '../../../../UI/CustomSvgIcons/CheckCircle';
import RaisedButton from '../../../../UI/RaisedButton';
import Window from '../../../../Utils/Window';
import { selectMessageByLocale } from '../../../../Utils/i18n/MessageByLocale';
const styles = {
bulletIcon: { width: 20, height: 20, marginRight: 10 },
bulletText: { flex: 1 },
planRecommendationThumbnail: { maxWidth: 350, flex: 1 },
planRecommendationContainer: { borderRadius: 8, maxWidth: 850, padding: 8 },
};
const planImages = {
individual: {
path: 'res/plan-individual.svg',
alt: t`Red hero taking care of their diamond`,
},
education: {
path: 'res/plan-education.svg',
alt: t`Red hero sharing knowledge with pink cloud students`,
},
professional: {
path: 'res/plan-professional.svg',
alt: t`Red and Green heroes running side by side carrying their diamonds`,
},
};
const planDetailsById = {
silver: {
title: <Trans>GDevelop's Silver plan</Trans>,
description: (
<Trans>Unlock GDevelop's features to build more and faster.</Trans>
),
image: planImages.individual,
link: 'https://gdevelop.io/pricing/individual',
},
gold: {
title: <Trans>GDevelop's Gold plan</Trans>,
description: (
<Trans>Unlock GDevelop's features to build more and faster.</Trans>
),
image: planImages.individual,
link: 'https://gdevelop.io/pricing/individual',
},
education: {
title: <Trans>GDevelop's Education plan</Trans>,
description: (
<Trans>
For universities, extra curricular classes and summer camps.
</Trans>
),
image: planImages.education,
link: 'https://gdevelop.io/pricing/education',
},
startup: {
title: <Trans>GDevelop's Startup plan</Trans>,
description: (
<Trans>
Get the most out of GDevelop and get your games out in no time.
</Trans>
),
image: planImages.professional,
link: 'https://gdevelop.io/pricing/business',
},
business: {
title: <Trans>GDevelop's Business plan</Trans>,
description: (
<Trans>
Get the most out of GDevelop and get your games out in no time.
</Trans>
),
image: planImages.professional,
link: 'https://gdevelop.io/pricing/business',
},
};
const PlanRecommendationRow = ({
recommendationPlanId,
subscriptionPlansWithPricingSystems,
i18n,
}: {|
recommendationPlanId: string,
subscriptionPlansWithPricingSystems: SubscriptionPlanWithPricingSystems[],
i18n: I18nType,
|}) => {
const gdevelopTheme = React.useContext(GDevelopThemeContext);
const { isMobile } = useResponsiveWindowSize();
const planToUse =
recommendationPlanId === 'silver'
? 'gdevelop_silver'
: recommendationPlanId === 'gold'
? 'gdevelop_gold'
: recommendationPlanId === 'education'
? 'gdevelop_education'
: recommendationPlanId === 'startup' ||
recommendationPlanId === 'business'
? 'gdevelop_startup'
: null;
if (!planToUse) return null;
const plan = subscriptionPlansWithPricingSystems.find(
plan => plan.id === planToUse
);
if (!plan) return null;
const planDetails = planDetailsById[recommendationPlanId];
return (
<Line justifyContent="center">
<Paper
background="dark"
style={{
...styles.planRecommendationContainer,
border: `1px solid ${gdevelopTheme.palette.secondary}`,
}}
>
<ResponsiveLineStackLayout noColumnMargin noMargin>
<img
src={planDetails.image.path}
alt={i18n._(planDetails.image.alt)}
style={styles.planRecommendationThumbnail}
/>
<Line expand>
<ColumnStackLayout>
<Text
noMargin
align={isMobile ? 'center' : 'left'}
size="section-title"
>
{planDetails.title}
</Text>
<Text align={isMobile ? 'center' : 'left'}>
{planDetails.description}
</Text>
<div style={{ padding: `0 20px` }}>
<ColumnStackLayout noMargin>
{plan.bulletPointsByLocale.map(
(bulletPointByLocale, index) => (
<Column key={index} expand noMargin>
<Line noMargin alignItems="center">
<CheckCircle
style={{
...styles.bulletIcon,
color: gdevelopTheme.message.valid,
}}
/>
<Text style={styles.bulletText} size="body2" noMargin>
{selectMessageByLocale(i18n, bulletPointByLocale)}
</Text>
</Line>
</Column>
)
)}
</ColumnStackLayout>
</div>
<Column noMargin>
<RaisedButton
primary
label={<Trans>Learn More</Trans>}
onClick={() => Window.openExternalURL(planDetails.link)}
/>
</Column>
</ColumnStackLayout>
</Line>
</ResponsiveLineStackLayout>
</Paper>
</Line>
);
};
export default PlanRecommendationRow;

View File

@@ -8,7 +8,6 @@ import { makeStyles } from '@material-ui/styles';
import GridList from '@material-ui/core/GridList';
import GridListTile from '@material-ui/core/GridListTile';
import { type AuthenticatedUser } from '../../../../Profile/AuthenticatedUserContext';
import { type Subscription } from '../../../../Utils/GDevelopServices/Usage';
import { TutorialContext } from '../../../../Tutorial/TutorialContext';
import { SectionRow } from '../SectionContainer';
import GuidedLessons from '../InAppTutorials/GuidedLessons';
@@ -21,16 +20,11 @@ import {
import Text from '../../../../UI/Text';
import { Column, Line, Spacer } from '../../../../UI/Grid';
import { type Tutorial } from '../../../../Utils/GDevelopServices/Tutorial';
import { type SubscriptionPlanWithPricingSystems } from '../../../../Utils/GDevelopServices/Usage';
import { CardWidget } from '../CardWidget';
import Window from '../../../../Utils/Window';
import { ColumnStackLayout } from '../../../../UI/Layout';
import {
type GuidedLessonsRecommendation,
type PlanRecommendation,
} from '../../../../Utils/GDevelopServices/User';
import { type GuidedLessonsRecommendation } from '../../../../Utils/GDevelopServices/User';
import PreferencesContext from '../../../Preferences/PreferencesContext';
import PlanRecommendationRow from './PlanRecommendationRow';
import { SurveyCard } from './SurveyCard';
import PlaceholderLoader from '../../../../UI/PlaceholderLoader';
import PromotionsSlideshow from '../../../../Promotions/PromotionsSlideshow';
@@ -109,26 +103,6 @@ const getTutorialsLimitsFromWidth = (
}
};
const isPlanRecommendationRelevant = (
subscription: Subscription,
planRecommendation: PlanRecommendation
): boolean => {
// Don't recommend plans to holders of education plan.
if (subscription.planId === 'gdevelop_education') return false;
const relevantPlans =
subscription.planId === 'gdevelop_silver' ||
subscription.planId === 'gdevelop_indie'
? ['gold', 'startup', 'business', 'education']
: subscription.planId === 'gdevelop_gold' ||
subscription.planId === 'gdevelop_pro'
? ['startup', 'business', 'education']
: subscription.planId === 'gdevelop_startup'
? ['business']
: [];
return relevantPlans.includes(planRecommendation.id);
};
type TextTutorialsRowProps = {|
tutorials: Array<Tutorial>,
i18n: I18nType,
@@ -186,7 +160,6 @@ const TextTutorialsRow = ({ tutorials, i18n }: TextTutorialsRowProps) => {
type Props = {|
authenticatedUser: AuthenticatedUser,
selectInAppTutorial: (tutorialId: string) => void,
subscriptionPlansWithPricingSystems: ?(SubscriptionPlanWithPricingSystems[]),
onStartSurvey: null | (() => void),
hasFilledSurveyAlready: boolean,
onOpenProfile: () => void,
@@ -202,14 +175,13 @@ type Props = {|
const RecommendationList = ({
authenticatedUser,
selectInAppTutorial,
subscriptionPlansWithPricingSystems,
onStartSurvey,
hasFilledSurveyAlready,
onOpenProfile,
onCreateProjectFromExample,
askToCloseProject,
}: Props) => {
const { recommendations, subscription, limits } = authenticatedUser;
const { recommendations, limits } = authenticatedUser;
const { tutorials } = React.useContext(TutorialContext);
const {
getTutorialProgress,
@@ -251,11 +223,6 @@ const RecommendationList = ({
? guidedLessonsRecommendation.lessonsIds
: null;
// $FlowIgnore
const planRecommendation: ?PlanRecommendation = recommendations.find(
recommendation => recommendation.type === 'plan'
);
const getInAppTutorialPartProgress = ({
tutorialId,
}: {
@@ -373,32 +340,6 @@ const RecommendationList = ({
</SectionRow>
);
}
if (planRecommendation) {
const shouldDisplayPlanRecommendation =
limits &&
!(
limits.capabilities.classrooms &&
limits.capabilities.classrooms.hideUpgradeNotice
) &&
(!subscription ||
isPlanRecommendationRelevant(subscription, planRecommendation));
if (
shouldDisplayPlanRecommendation &&
subscriptionPlansWithPricingSystems
) {
items.push(
<SectionRow key="plan">
<PlanRecommendationRow
recommendationPlanId={planRecommendation.id}
subscriptionPlansWithPricingSystems={
subscriptionPlansWithPricingSystems
}
i18n={i18n}
/>
</SectionRow>
);
}
}
return (
<>

View File

@@ -23,7 +23,6 @@ import PreferencesContext from '../../../Preferences/PreferencesContext';
import RecommendationList from './RecommendationList';
import ErrorBoundary from '../../../../UI/ErrorBoundary';
import { delay } from '../../../../Utils/Delay';
import { type SubscriptionPlanWithPricingSystems } from '../../../../Utils/GDevelopServices/Usage';
import Checkbox from '../../../../UI/Checkbox';
import { sendUserSurveyCompleted } from '../../../../Utils/Analytics/EventSender';
import { type NewProjectSetup } from '../../../../ProjectCreation/NewProjectSetupDialog';
@@ -60,7 +59,6 @@ type Props = {|
onUserSurveyStarted: () => void,
onUserSurveyHidden: () => void,
selectInAppTutorial: (tutorialId: string) => void,
subscriptionPlansWithPricingSystems: ?(SubscriptionPlanWithPricingSystems[]),
onOpenProfile: () => void,
onCreateProjectFromExample: (
exampleShortHeader: ExampleShortHeader,
@@ -75,7 +73,6 @@ const GetStartedSection = ({
selectInAppTutorial,
onUserSurveyStarted,
onUserSurveyHidden,
subscriptionPlansWithPricingSystems,
onOpenProfile,
onCreateProjectFromExample,
askToCloseProject,
@@ -247,9 +244,6 @@ const GetStartedSection = ({
<RecommendationList
authenticatedUser={authenticatedUser}
selectInAppTutorial={selectInAppTutorial}
subscriptionPlansWithPricingSystems={
subscriptionPlansWithPricingSystems
}
onOpenProfile={onOpenProfile}
onStartSurvey={
profile

View File

@@ -273,7 +273,7 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
const { openSubscriptionDialog } = React.useContext(
SubscriptionSuggestionContext
);
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
const { getSubscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
});
const [selectedUserIds, setSelectedUserIds] = React.useState<string[]>([]);
@@ -374,6 +374,7 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
[selectedUserIds, members]
);
const subscriptionPlansWithPricingSystems = getSubscriptionPlansWithPricingSystems();
const isLoading = !subscriptionPlansWithPricingSystems;
const availableSeats = getAvailableSeats();

View File

@@ -28,7 +28,6 @@ import { useResponsiveWindowSize } from '../../../UI/Responsive/ResponsiveWindow
import { type PrivateGameTemplateListingData } from '../../../Utils/GDevelopServices/Shop';
import { PrivateGameTemplateStoreContext } from '../../../AssetStore/PrivateGameTemplates/PrivateGameTemplateStoreContext';
import PreferencesContext from '../../Preferences/PreferencesContext';
import useSubscriptionPlans from '../../../Utils/UseSubscriptionPlans';
import { incrementGetStartedSectionViewCount } from '../../../Utils/Analytics/LocalStats';
import {
sendUserSurveyHidden,
@@ -292,9 +291,6 @@ export const HomePage = React.memo<Props>(
: games.find(game => game.id === openedGameId),
[games, openedGameId]
);
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: false,
});
// Open the store and a pack or game template if asked to do so, either at
// app opening, either when the route changes (when clicking on an announcement
@@ -592,9 +588,6 @@ export const HomePage = React.memo<Props>(
selectInAppTutorial={selectInAppTutorial}
onUserSurveyStarted={onUserSurveyStarted}
onUserSurveyHidden={onUserSurveyHidden}
subscriptionPlansWithPricingSystems={
subscriptionPlansWithPricingSystems
}
onOpenProfile={onOpenProfile}
onCreateProjectFromExample={onCreateProjectFromExample}
askToCloseProject={askToCloseProject}

View File

@@ -32,7 +32,7 @@ const ProfileDialog = ({ onClose }: Props) => {
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const badgesSeenNotificationTimeoutRef = React.useRef<?TimeoutID>(null);
const badgesSeenNotificationSentRef = React.useRef<boolean>(false);
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
const { getSubscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
authenticatedUser,
});
@@ -184,9 +184,7 @@ const ProfileDialog = ({ onClose }: Props) => {
{isStudentAccount ? null : (
<SubscriptionDetails
subscription={authenticatedUser.subscription}
subscriptionPlansWithPricingSystems={
subscriptionPlansWithPricingSystems
}
subscriptionPlansWithPricingSystems={getSubscriptionPlansWithPricingSystems()}
onManageSubscription={onManageSubscription}
isManageSubscriptionLoading={isManageSubscriptionLoading}
/>

View File

@@ -24,14 +24,16 @@ import AlertMessage from '../../../UI/AlertMessage';
type Props = {|
onClose: Function,
subscriptionPlansWithPricingSystems: ?(SubscriptionPlanWithPricingSystems[]),
getAvailableSubscriptionPlansWithPrices: () =>
| SubscriptionPlanWithPricingSystems[]
| null,
recommendedPlanId: string,
onOpenPendingDialog: (open: boolean) => void,
|};
export default function PromotionSubscriptionDialog({
onClose,
subscriptionPlansWithPricingSystems,
getAvailableSubscriptionPlansWithPrices,
recommendedPlanId,
onOpenPendingDialog,
}: Props) {
@@ -79,6 +81,8 @@ export default function PromotionSubscriptionDialog({
);
};
const subscriptionPlansWithPricingSystems = getAvailableSubscriptionPlansWithPrices();
const purchasablePlansWithPricingSystems = React.useMemo(
() =>
subscriptionPlansWithPricingSystems

View File

@@ -203,16 +203,18 @@ export const getPlanSpecificRequirements = (
type Props = {|
onClose: Function,
subscriptionPlansWithPricingSystems: ?(SubscriptionPlanWithPricingSystems[]),
userLegacySubscriptionPlanWithPricingSystem: ?SubscriptionPlanWithPricingSystems,
getAvailableSubscriptionPlansWithPrices: () =>
| SubscriptionPlanWithPricingSystems[]
| null,
getUserLegacySubscriptionPlanWithPricingSystem: () => SubscriptionPlanWithPricingSystems | null,
filter: ?SubscriptionType,
onOpenPendingDialog: (open: boolean) => void,
|};
export default function SubscriptionDialog({
onClose,
subscriptionPlansWithPricingSystems,
userLegacySubscriptionPlanWithPricingSystem,
getAvailableSubscriptionPlansWithPrices,
getUserLegacySubscriptionPlanWithPricingSystem,
filter,
onOpenPendingDialog,
}: Props) {
@@ -225,6 +227,8 @@ export default function SubscriptionDialog({
] = React.useState<number>(20);
const [redeemCodeDialogOpen, setRedeemCodeDialogOpen] = React.useState(false);
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const subscriptionPlansWithPricingSystems = getAvailableSubscriptionPlansWithPrices();
const userLegacySubscriptionPlanWithPricingSystem = getUserLegacySubscriptionPlanWithPricingSystem();
const [period, setPeriod] = React.useState<'year' | 'month'>(
getSubscriptionPricingSystemPeriod(
authenticatedUser.subscription,

View File

@@ -14,11 +14,12 @@ import {
import AuthenticatedUserContext from '../AuthenticatedUserContext';
import useAlertDialog from '../../UI/Alert/useAlertDialog';
import useSubscriptionPlans, {
getAvailableSubscriptionPlansWithPrices,
filterAvailableSubscriptionPlansWithPrices,
} from '../../Utils/UseSubscriptionPlans';
import PromotionSubscriptionDialog from './PromotionSubscriptionDialog';
import SubscriptionPendingDialog from './SubscriptionPendingDialog';
import LoaderModal from '../../UI/LoaderModal';
import { useLazyMemo } from '../../Utils/UseLazyMemo';
export type SubscriptionType = 'individual' | 'team' | 'education';
@@ -62,7 +63,7 @@ export const SubscriptionSuggestionProvider = ({
>(null);
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const { showAlert } = useAlertDialog();
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
const { getSubscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
authenticatedUser,
});
@@ -101,46 +102,57 @@ export const SubscriptionSuggestionProvider = ({
openSubscriptionDialog,
]);
const availableSubscriptionPlansWithPrices = React.useMemo(
() =>
subscriptionPlansWithPricingSystems
? getAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems
)
: null,
[subscriptionPlansWithPricingSystems]
const getAvailableSubscriptionPlansWithPrices = useLazyMemo(
React.useCallback(
() => {
const subscriptionPlansWithPricingSystems = getSubscriptionPlansWithPricingSystems();
return subscriptionPlansWithPricingSystems
? filterAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems
)
: null;
},
[getSubscriptionPlansWithPricingSystems]
)
);
const userLegacySubscriptionPlanWithPricingSystem = React.useMemo(
() => {
if (
!authenticatedUser.subscription ||
!authenticatedUser.subscription.planId ||
!authenticatedUser.subscription.pricingSystemId ||
!subscriptionPlansWithPricingSystems
) {
return null;
}
const {
planId: userPlanId,
pricingSystemId: userPricingSystemId,
} = authenticatedUser.subscription;
const userPlanWithPricingSystems = subscriptionPlansWithPricingSystems.find(
planWithPricingSystems => planWithPricingSystems.id === userPlanId
);
if (!userPlanWithPricingSystems || !userPlanWithPricingSystems.isLegacy) {
return null;
}
const userPricingSystem = userPlanWithPricingSystems.pricingSystems.find(
pricingSystem => pricingSystem.id === userPricingSystemId
);
if (!userPricingSystem) return null;
return {
...userPlanWithPricingSystems,
pricingSystems: [userPricingSystem],
};
},
[subscriptionPlansWithPricingSystems, authenticatedUser.subscription]
const getUserLegacySubscriptionPlanWithPricingSystem = useLazyMemo(
React.useCallback(
() => {
const subscriptionPlansWithPricingSystems = getSubscriptionPlansWithPricingSystems();
if (
!authenticatedUser.subscription ||
!authenticatedUser.subscription.planId ||
!authenticatedUser.subscription.pricingSystemId ||
!subscriptionPlansWithPricingSystems
) {
return null;
}
const {
planId: userPlanId,
pricingSystemId: userPricingSystemId,
} = authenticatedUser.subscription;
const userPlanWithPricingSystems = subscriptionPlansWithPricingSystems.find(
planWithPricingSystems => planWithPricingSystems.id === userPlanId
);
if (
!userPlanWithPricingSystems ||
!userPlanWithPricingSystems.isLegacy
) {
return null;
}
const userPricingSystem = userPlanWithPricingSystems.pricingSystems.find(
pricingSystem => pricingSystem.id === userPricingSystemId
);
if (!userPricingSystem) return null;
return {
...userPlanWithPricingSystems,
pricingSystems: [userPricingSystem],
};
},
[getSubscriptionPlansWithPricingSystems, authenticatedUser.subscription]
)
);
// When the analyticsMetadata is set, a dialog is shown so we can send an event.
@@ -172,8 +184,8 @@ export const SubscriptionSuggestionProvider = ({
) : !hasValidSubscriptionPlan(authenticatedUser.subscription) &&
analyticsMetadata.recommendedPlanId ? (
<PromotionSubscriptionDialog
subscriptionPlansWithPricingSystems={
availableSubscriptionPlansWithPrices
getAvailableSubscriptionPlansWithPrices={
getAvailableSubscriptionPlansWithPrices
}
onClose={closeSubscriptionDialog}
recommendedPlanId={analyticsMetadata.recommendedPlanId}
@@ -183,11 +195,11 @@ export const SubscriptionSuggestionProvider = ({
/>
) : (
<SubscriptionDialog
subscriptionPlansWithPricingSystems={
availableSubscriptionPlansWithPrices
getAvailableSubscriptionPlansWithPrices={
getAvailableSubscriptionPlansWithPrices
}
userLegacySubscriptionPlanWithPricingSystem={
userLegacySubscriptionPlanWithPricingSystem
getUserLegacySubscriptionPlanWithPricingSystem={
getUserLegacySubscriptionPlanWithPricingSystem
}
onClose={closeSubscriptionDialog}
filter={filter}

View File

@@ -0,0 +1,78 @@
// @flow
import * as React from 'react';
import { useStableUpToDateRef } from './UseStableUpToDateCallback';
/**
* Expose a getter that returns the value computed by the function, which
* is then memoized as long as the function dependencies are stable.
*
* The function should be a React.useCallback - so that it is stable
* and only recomputed when the dependencies change.
*
* During the computation, the previous value or null is returned.
*/
export const useAsyncLazyMemo = <T>(fn: () => Promise<T>): (() => T | null) => {
const [value, setValue] = React.useState(null);
const [requestedVersion, setRequestedVersion] = React.useState(0);
const valueVersion = React.useRef(0);
const isComputingForVersion = React.useRef<{ [version: number]: boolean }>(
{}
);
const requestedVersionRef = useStableUpToDateRef(requestedVersion);
// If the function changes, it means that the value is no longer valid.
// We increment the requested version.
React.useEffect(
() => {
setRequestedVersion(requestedVersion => {
return requestedVersion + 1;
});
},
[fn]
);
// If the function changes, we need to recompute the value.
// If the value changed, the getter is also invalidated so it can be called again by components
// depending on it.
const getter = React.useCallback(
() => {
// Recompute the value if its version is older than the requested version
// or if it's not even set.
const shouldCompute =
valueVersion.current < requestedVersion || value === null;
if (shouldCompute && !isComputingForVersion.current[requestedVersion]) {
isComputingForVersion.current[requestedVersion] = true;
fn()
.then(result => {
// Update the version of the value only if the computation is still
// for the same version that is requested.
if (requestedVersion === requestedVersionRef.current) {
setValue(result);
valueVersion.current = requestedVersion;
}
})
.finally(() => {
// We're done computing the value for this version.
delete isComputingForVersion.current[requestedVersion];
});
}
return value;
},
[value, fn, requestedVersion, requestedVersionRef]
);
return getter;
};
/**
* Expose a getter that returns the value computed by the function, which
* is then memoized as long as the function dependencies are stable.
*
* The function should be a React.useCallback - so that it is stable
* and only recomputed when the dependencies change.
*/
export const useLazyMemo = <T>(fn: () => T): (() => T | null) => {
return useAsyncLazyMemo(React.useCallback(() => Promise.resolve(fn()), [fn]));
};

View File

@@ -9,6 +9,7 @@ import {
type SubscriptionPlanPricingSystem,
} from './GDevelopServices/Usage';
import { type AuthenticatedUser } from '../Profile/AuthenticatedUserContext';
import { useAsyncLazyMemo } from './UseLazyMemo';
const mergeSubscriptionPlansWithPrices = (
subscriptionPlans: SubscriptionPlan[],
@@ -35,7 +36,7 @@ const mergeSubscriptionPlansWithPrices = (
.filter(Boolean);
};
export const getAvailableSubscriptionPlansWithPrices = (
export const filterAvailableSubscriptionPlansWithPrices = (
subscriptionPlansWithPricingSystems: SubscriptionPlanWithPricingSystems[]
): SubscriptionPlanWithPricingSystems[] => {
const nonLegacyPlans = subscriptionPlansWithPricingSystems.filter(
@@ -61,10 +62,6 @@ type Props = {|
* Hook to access subscription plans across the app.
*/
const useSubscriptionPlans = ({ includeLegacy, authenticatedUser }: Props) => {
const [
subscriptionPlansWithPricingSystems,
setSubscriptionPlansWithPrices,
] = React.useState<?(SubscriptionPlanWithPricingSystems[])>(null);
const userId =
authenticatedUser && authenticatedUser.profile
? authenticatedUser.profile.id
@@ -74,7 +71,12 @@ const useSubscriptionPlans = ({ includeLegacy, authenticatedUser }: Props) => {
: null;
const fetchSubscriptionPlansAndPrices = React.useCallback(
async () => {
async (): Promise<SubscriptionPlanWithPricingSystems[]> => {
console.info(
`Fetching subscription plans and pricing systems (includeLegacy=${
includeLegacy ? 'true' : 'false'
}, userId=${userId || 'null'})...`
);
const results = await Promise.all([
listSubscriptionPlans({
includeLegacy,
@@ -87,21 +89,23 @@ const useSubscriptionPlans = ({ includeLegacy, authenticatedUser }: Props) => {
userId,
}),
]);
setSubscriptionPlansWithPrices(
mergeSubscriptionPlansWithPrices(results[0], results[1])
console.info(
`Subscription plans and pricing systems (includeLegacy=${
includeLegacy ? 'true' : 'false'
}, userId=${userId || 'null'}) fetched.`
);
return mergeSubscriptionPlansWithPrices(results[0], results[1]);
},
[includeLegacy, getAuthorizationHeader, userId]
);
React.useEffect(
() => {
fetchSubscriptionPlansAndPrices();
},
[fetchSubscriptionPlansAndPrices]
const getSubscriptionPlansWithPricingSystems = useAsyncLazyMemo(
fetchSubscriptionPlansAndPrices
);
return { subscriptionPlansWithPricingSystems };
return {
getSubscriptionPlansWithPricingSystems,
};
};
export default useSubscriptionPlans;

View File

@@ -9,10 +9,6 @@ import PreferencesContext, {
} 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',
@@ -21,48 +17,29 @@ export default {
};
export const Default = () => {
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
authenticatedUser: fakeAuthenticatedUserWithNoSubscription,
});
return subscriptionPlansWithPricingSystems ? (
<PreferencesContext.Provider value={initialPreferences}>
<TutorialStateProvider>
<RecommendationList
onOpenProfile={action('onOpenProfile')}
authenticatedUser={fakeAuthenticatedUserWithNoSubscription}
selectInAppTutorial={action('selectInAppTutorial')}
subscriptionPlansWithPricingSystems={getAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems
)}
onStartSurvey={null}
hasFilledSurveyAlready={false}
askToCloseProject={async () => true}
onCreateProjectFromExample={action('onCreateProjectFromExample')}
/>
</TutorialStateProvider>
</PreferencesContext.Provider>
) : (
<LoaderModal show />
);
<PreferencesContext.Provider value={initialPreferences}>
<TutorialStateProvider>
<RecommendationList
onOpenProfile={action('onOpenProfile')}
authenticatedUser={fakeAuthenticatedUserWithNoSubscription}
selectInAppTutorial={action('selectInAppTutorial')}
onStartSurvey={null}
hasFilledSurveyAlready={false}
askToCloseProject={async () => true}
onCreateProjectFromExample={action('onCreateProjectFromExample')}
/>
</TutorialStateProvider>
</PreferencesContext.Provider>;
};
export const WithSurvey = () => {
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
authenticatedUser: fakeAuthenticatedUserWithNoSubscription,
});
return subscriptionPlansWithPricingSystems ? (
return (
<PreferencesContext.Provider value={initialPreferences}>
<TutorialStateProvider>
<RecommendationList
onOpenProfile={action('onOpenProfile')}
authenticatedUser={fakeAuthenticatedUserWithNoSubscription}
selectInAppTutorial={action('selectInAppTutorial')}
subscriptionPlansWithPricingSystems={getAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems
)}
onStartSurvey={action('onStartSurvey')}
hasFilledSurveyAlready={false}
askToCloseProject={async () => true}
@@ -70,27 +47,17 @@ export const WithSurvey = () => {
/>
</TutorialStateProvider>
</PreferencesContext.Provider>
) : (
<LoaderModal show />
);
};
export const WithSurveyAlreadyFilled = () => {
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
authenticatedUser: fakeAuthenticatedUserWithNoSubscription,
});
return subscriptionPlansWithPricingSystems ? (
return (
<PreferencesContext.Provider value={initialPreferences}>
<TutorialStateProvider>
<RecommendationList
onOpenProfile={action('onOpenProfile')}
authenticatedUser={fakeAuthenticatedUserWithNoSubscription}
selectInAppTutorial={action('selectInAppTutorial')}
subscriptionPlansWithPricingSystems={getAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems
)}
onStartSurvey={action('onStartSurvey')}
hasFilledSurveyAlready={true}
askToCloseProject={async () => true}
@@ -98,7 +65,5 @@ export const WithSurveyAlreadyFilled = () => {
/>
</TutorialStateProvider>
</PreferencesContext.Provider>
) : (
<LoaderModal show />
);
};

View File

@@ -10,10 +10,7 @@ import {
} from '../../../../fixtures/GDevelopServicesTestData';
import PromotionSubscriptionDialog from '../../../../Profile/Subscription/PromotionSubscriptionDialog';
import AlertProvider from '../../../../UI/Alert/AlertProvider';
import useSubscriptionPlans, {
getAvailableSubscriptionPlansWithPrices,
} from '../../../../Utils/UseSubscriptionPlans';
import LoaderModal from '../../../../UI/LoaderModal';
import { useGetAvailableSubscriptionPlansWithPrices } from './Utils';
export default {
title: 'Subscription/PromotionSubscriptionDialog',
@@ -22,154 +19,141 @@ export default {
};
export const NotAuthenticatedSilverRecommended = () => {
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
authenticatedUser: fakeNotAuthenticatedUser,
});
return subscriptionPlansWithPricingSystems ? (
const getAvailableSubscriptionPlansWithPrices = useGetAvailableSubscriptionPlansWithPrices(
{
authenticatedUser: fakeNotAuthenticatedUser,
}
);
return (
<AlertProvider>
<AuthenticatedUserContext.Provider value={fakeNotAuthenticatedUser}>
<PromotionSubscriptionDialog
subscriptionPlansWithPricingSystems={getAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems
)}
getAvailableSubscriptionPlansWithPrices={
getAvailableSubscriptionPlansWithPrices
}
onClose={() => action('on close')()}
recommendedPlanId="gdevelop_silver"
onOpenPendingDialog={() => action('on open pending dialog')()}
/>
</AuthenticatedUserContext.Provider>
</AlertProvider>
) : (
<LoaderModal show />
);
};
export const AuthenticatedSilverRecommended = () => {
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
authenticatedUser: fakeAuthenticatedUserWithNoSubscription,
});
return subscriptionPlansWithPricingSystems ? (
const getAvailableSubscriptionPlansWithPrices = useGetAvailableSubscriptionPlansWithPrices(
{
authenticatedUser: fakeNotAuthenticatedUser,
}
);
return (
<AlertProvider>
<AuthenticatedUserContext.Provider
value={fakeAuthenticatedUserWithNoSubscription}
>
<PromotionSubscriptionDialog
subscriptionPlansWithPricingSystems={getAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems
)}
getAvailableSubscriptionPlansWithPrices={
getAvailableSubscriptionPlansWithPrices
}
onClose={() => action('on close')()}
recommendedPlanId="gdevelop_silver"
onOpenPendingDialog={() => action('on open pending dialog')()}
/>
</AuthenticatedUserContext.Provider>
</AlertProvider>
) : (
<LoaderModal show />
);
};
export const GoldRecommended = () => {
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
authenticatedUser: fakeAuthenticatedUserWithNoSubscription,
});
return subscriptionPlansWithPricingSystems ? (
const getAvailableSubscriptionPlansWithPrices = useGetAvailableSubscriptionPlansWithPrices(
{
authenticatedUser: fakeNotAuthenticatedUser,
}
);
return (
<AlertProvider>
<AuthenticatedUserContext.Provider
value={fakeAuthenticatedUserWithNoSubscription}
>
<PromotionSubscriptionDialog
subscriptionPlansWithPricingSystems={getAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems
)}
getAvailableSubscriptionPlansWithPrices={
getAvailableSubscriptionPlansWithPrices
}
onClose={() => action('on close')()}
recommendedPlanId="gdevelop_gold"
onOpenPendingDialog={() => action('on open pending dialog')()}
/>
</AuthenticatedUserContext.Provider>
</AlertProvider>
) : (
<LoaderModal show />
);
};
export const ProRecommended = () => {
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
authenticatedUser: fakeAuthenticatedUserWithNoSubscription,
});
return subscriptionPlansWithPricingSystems ? (
const getAvailableSubscriptionPlansWithPrices = useGetAvailableSubscriptionPlansWithPrices(
{
authenticatedUser: fakeNotAuthenticatedUser,
}
);
return (
<AlertProvider>
<AuthenticatedUserContext.Provider
value={fakeAuthenticatedUserWithNoSubscription}
>
<PromotionSubscriptionDialog
subscriptionPlansWithPricingSystems={getAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems
)}
getAvailableSubscriptionPlansWithPrices={
getAvailableSubscriptionPlansWithPrices
}
onClose={() => action('on close')()}
recommendedPlanId="gdevelop_startup"
onOpenPendingDialog={() => action('on open pending dialog')()}
/>
</AuthenticatedUserContext.Provider>
</AlertProvider>
) : (
<LoaderModal show />
);
};
export const EducationRecommended = () => {
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
authenticatedUser: fakeAuthenticatedUserWithNoSubscription,
});
return subscriptionPlansWithPricingSystems ? (
const getAvailableSubscriptionPlansWithPrices = useGetAvailableSubscriptionPlansWithPrices(
{
authenticatedUser: fakeNotAuthenticatedUser,
}
);
return (
<AlertProvider>
<AuthenticatedUserContext.Provider
value={fakeAuthenticatedUserWithNoSubscription}
>
<PromotionSubscriptionDialog
subscriptionPlansWithPricingSystems={getAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems
)}
getAvailableSubscriptionPlansWithPrices={
getAvailableSubscriptionPlansWithPrices
}
onClose={() => action('on close')()}
recommendedPlanId="gdevelop_education"
onOpenPendingDialog={() => action('on open pending dialog')()}
/>
</AuthenticatedUserContext.Provider>
</AlertProvider>
) : (
<LoaderModal show />
);
};
export const WithoutSilverButRecommended = () => {
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
authenticatedUser: fakeAuthenticatedUserWithNoSubscription,
});
const subscriptionPlansWithoutSilver = subscriptionPlansWithPricingSystems
? subscriptionPlansWithPricingSystems.filter(
plan => plan.id !== 'gdevelop_silver'
)
: null;
return subscriptionPlansWithoutSilver ? (
const getAvailableSubscriptionPlansWithPrices = useGetAvailableSubscriptionPlansWithPrices(
{ authenticatedUser: fakeNotAuthenticatedUser, filterSilver: true }
);
return (
<AlertProvider>
<AuthenticatedUserContext.Provider
value={fakeAuthenticatedUserWithNoSubscription}
>
<PromotionSubscriptionDialog
subscriptionPlansWithPricingSystems={getAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithoutSilver
)}
getAvailableSubscriptionPlansWithPrices={
getAvailableSubscriptionPlansWithPrices
}
onClose={() => action('on close')()}
recommendedPlanId="gdevelop_silver"
onOpenPendingDialog={() => action('on open pending dialog')()}
/>
</AuthenticatedUserContext.Provider>
</AlertProvider>
) : (
<LoaderModal show />
);
};

View File

@@ -20,10 +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';
import useSubscriptionPlans, {
filterAvailableSubscriptionPlansWithPrices,
} from '../../../../Utils/UseSubscriptionPlans';
export default {
title: 'Subscription/SubscriptionDetails',
@@ -184,15 +184,16 @@ export const Default = ({
}
const { subscription: userSubscription } = authenticatedUser;
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
const { getSubscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
authenticatedUser,
});
const subscriptionPlansWithPricingSystems = getSubscriptionPlansWithPricingSystems();
return subscriptionPlansWithPricingSystems ? (
<AlertProvider>
<SubscriptionDetails
subscriptionPlansWithPricingSystems={getAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems={filterAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems
)}
subscription={userSubscription}

View File

@@ -18,7 +18,7 @@ import {
import SubscriptionDialog from '../../../../Profile/Subscription/SubscriptionDialog';
import AlertProvider from '../../../../UI/Alert/AlertProvider';
import useSubscriptionPlans, {
getAvailableSubscriptionPlansWithPrices,
filterAvailableSubscriptionPlansWithPrices,
} from '../../../../Utils/UseSubscriptionPlans';
import LoaderModal from '../../../../UI/LoaderModal';
@@ -161,29 +161,33 @@ export const Default = ({
}
}
const { subscriptionPlansWithPricingSystems } = useSubscriptionPlans({
const { getSubscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
authenticatedUser,
});
const subscriptionPlansWithPricingSystems = getSubscriptionPlansWithPricingSystems();
const { subscription: userSubscription } = authenticatedUser;
const userLegacySubscriptionPlanWithPricingSystem =
userSubscription && subscriptionPlansWithPricingSystems
? subscriptionPlansWithPricingSystems.find(
planWithPricingSystem =>
planWithPricingSystem.id === userSubscription.planId &&
planWithPricingSystem.isLegacy
)
: null;
(userSubscription &&
subscriptionPlansWithPricingSystems &&
subscriptionPlansWithPricingSystems.find(
planWithPricingSystem =>
planWithPricingSystem.id === userSubscription.planId &&
planWithPricingSystem.isLegacy
)) ||
null;
return subscriptionPlansWithPricingSystems ? (
<AlertProvider>
<AuthenticatedUserContext.Provider value={authenticatedUser}>
<SubscriptionDialog
subscriptionPlansWithPricingSystems={getAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems
)}
userLegacySubscriptionPlanWithPricingSystem={
getAvailableSubscriptionPlansWithPrices={() =>
filterAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems
)
}
getUserLegacySubscriptionPlanWithPricingSystem={() =>
userLegacySubscriptionPlanWithPricingSystem
}
onClose={() => action('on close')()}

View File

@@ -0,0 +1,37 @@
// @flow
import * as React from 'react';
import useSubscriptionPlans, {
filterAvailableSubscriptionPlansWithPrices,
} from '../../../../Utils/UseSubscriptionPlans';
import { useLazyMemo } from '../../../../Utils/UseLazyMemo';
import { type AuthenticatedUser } from '../../../../Profile/AuthenticatedUserContext';
export const useGetAvailableSubscriptionPlansWithPrices = ({
authenticatedUser,
filterSilver,
}: {
authenticatedUser: AuthenticatedUser,
filterSilver?: boolean,
}) => {
const { getSubscriptionPlansWithPricingSystems } = useSubscriptionPlans({
includeLegacy: true,
authenticatedUser,
});
const getAvailableSubscriptionPlansWithPrices = useLazyMemo(
React.useCallback(
() => {
const subscriptionPlansWithPricingSystems = getSubscriptionPlansWithPricingSystems();
if (!subscriptionPlansWithPricingSystems) return null;
return filterAvailableSubscriptionPlansWithPrices(
subscriptionPlansWithPricingSystems.filter(plan =>
filterSilver ? plan.id !== 'gdevelop_silver' : true
)
);
},
[getSubscriptionPlansWithPricingSystems, filterSilver]
)
);
return getAvailableSubscriptionPlansWithPrices;
};