mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Allow buying a bundle without an account (#7873)
Do not show in changelog
This commit is contained in:
@@ -88,8 +88,8 @@ type Props = {|
|
||||
getCourseCompletion: (courseId: string) => CourseCompletion | null,
|
||||
noPadding?: boolean,
|
||||
noActions?: boolean,
|
||||
simpleCheckout?: boolean,
|
||||
onPurchaseDone?: () => void,
|
||||
fastCheckout?: boolean,
|
||||
onCloseAfterPurchaseDone?: () => void,
|
||||
|};
|
||||
|
||||
const BundleInformationPage = ({
|
||||
@@ -104,8 +104,8 @@ const BundleInformationPage = ({
|
||||
getCourseCompletion,
|
||||
noPadding,
|
||||
noActions,
|
||||
simpleCheckout,
|
||||
onPurchaseDone,
|
||||
fastCheckout,
|
||||
onCloseAfterPurchaseDone,
|
||||
}: Props) => {
|
||||
const { windowSize, isLandscape, isMobile } = useResponsiveWindowSize();
|
||||
const { bundleListingDatas } = React.useContext(BundleStoreContext); // If archived, should use the one passed.
|
||||
@@ -317,8 +317,8 @@ const BundleInformationPage = ({
|
||||
bundleListingData={bundleListingData}
|
||||
bundle={bundle}
|
||||
i18n={i18n}
|
||||
simpleCheckout={simpleCheckout}
|
||||
onPurchaseDone={onPurchaseDone}
|
||||
fastCheckout={fastCheckout}
|
||||
onCloseAfterPurchaseDone={onCloseAfterPurchaseDone}
|
||||
/>
|
||||
<Line noMargin>
|
||||
<Text size="section-title">
|
||||
|
@@ -68,8 +68,8 @@ type Props = {|
|
||||
bundle: Bundle,
|
||||
simulateAppStoreProduct?: boolean,
|
||||
i18n: I18nType,
|
||||
simpleCheckout?: boolean,
|
||||
onPurchaseDone?: () => void,
|
||||
fastCheckout?: boolean,
|
||||
onCloseAfterPurchaseDone?: () => void,
|
||||
|};
|
||||
|
||||
const BundlePageHeader = ({
|
||||
@@ -77,8 +77,8 @@ const BundlePageHeader = ({
|
||||
bundleListingData,
|
||||
simulateAppStoreProduct,
|
||||
i18n,
|
||||
simpleCheckout,
|
||||
onPurchaseDone,
|
||||
fastCheckout,
|
||||
onCloseAfterPurchaseDone,
|
||||
}: Props) => {
|
||||
const { privateGameTemplateListingDatas } = React.useContext(
|
||||
PrivateGameTemplateStoreContext
|
||||
@@ -298,7 +298,7 @@ const BundlePageHeader = ({
|
||||
<ProductLimitedTimeOffer
|
||||
visibleUntil={bundleListingData.visibleUntil}
|
||||
/>
|
||||
) : estimatedSavingsFormatted ? (
|
||||
) : estimatedTotalPriceFormatted ? (
|
||||
<Column
|
||||
noMargin
|
||||
alignItems="flex-start"
|
||||
@@ -310,9 +310,8 @@ const BundlePageHeader = ({
|
||||
/>
|
||||
<Text color="secondary">
|
||||
<Trans>
|
||||
Get{' '}
|
||||
{estimatedSavingsFormatted.savingsPriceFormatted}{' '}
|
||||
worth of value for less!
|
||||
Get {estimatedTotalPriceFormatted} worth of value
|
||||
for less!
|
||||
</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
@@ -507,8 +506,8 @@ const BundlePageHeader = ({
|
||||
bundleListingData={purchasingBundleListingData}
|
||||
usageType="default"
|
||||
onClose={() => setPurchasingBundleListingData(null)}
|
||||
simpleCheckout={simpleCheckout}
|
||||
onPurchaseDone={onPurchaseDone}
|
||||
fastCheckout={fastCheckout}
|
||||
onCloseAfterPurchaseDone={onCloseAfterPurchaseDone}
|
||||
/>
|
||||
)}
|
||||
{isRedemptionCodesDialogOpen && (
|
||||
|
@@ -25,14 +25,15 @@ import {
|
||||
} from '../../Utils/AppStorePurchases';
|
||||
import { extractGDevelopApiErrorStatusAndCode } from '../../Utils/GDevelopServices/Errors';
|
||||
import PasswordPromptDialog from '../PasswordPromptDialog';
|
||||
import { getUserUUID } from '../../Utils/Analytics/UserUUID';
|
||||
|
||||
type Props = {|
|
||||
bundleListingData: BundleListingData,
|
||||
usageType: string,
|
||||
onClose: () => void,
|
||||
simulateAppStoreProduct?: boolean,
|
||||
simpleCheckout?: boolean,
|
||||
onPurchaseDone?: () => void,
|
||||
fastCheckout?: boolean,
|
||||
onCloseAfterPurchaseDone?: () => void,
|
||||
|};
|
||||
|
||||
const BundlePurchaseDialog = ({
|
||||
@@ -40,8 +41,8 @@ const BundlePurchaseDialog = ({
|
||||
usageType,
|
||||
onClose,
|
||||
simulateAppStoreProduct,
|
||||
simpleCheckout,
|
||||
onPurchaseDone,
|
||||
fastCheckout,
|
||||
onCloseAfterPurchaseDone,
|
||||
}: Props) => {
|
||||
const {
|
||||
profile,
|
||||
@@ -64,16 +65,22 @@ const BundlePurchaseDialog = ({
|
||||
] = React.useState<boolean>(false);
|
||||
const [password, setPassword] = React.useState<string>('');
|
||||
const { showAlert } = useAlertDialog();
|
||||
const [isOpeningUrl, setIsOpeningUrl] = React.useState(false);
|
||||
|
||||
const shouldUseOrSimulateAppStoreProduct =
|
||||
shouldUseAppStoreProduct() || simulateAppStoreProduct;
|
||||
|
||||
const willReceiveAnEmailForThePurchase = !profile && fastCheckout;
|
||||
|
||||
const onStartPurchase = async () => {
|
||||
if (!profile) return;
|
||||
setDisplayPasswordPrompt(false);
|
||||
|
||||
// Purchase with the App Store.
|
||||
if (shouldUseOrSimulateAppStoreProduct) {
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsPurchasing(true);
|
||||
await purchaseAppStoreProduct(bundleListingData.appStoreProductId);
|
||||
@@ -98,21 +105,35 @@ const BundlePurchaseDialog = ({
|
||||
// Purchase with web.
|
||||
try {
|
||||
setIsPurchasing(true);
|
||||
const checkoutUrl = simpleCheckout
|
||||
? getStripeCheckoutUrl({
|
||||
productId: bundleListingData.id,
|
||||
priceName: price.name,
|
||||
userId: profile.id,
|
||||
userEmail: profile.email,
|
||||
...(password ? { password } : undefined),
|
||||
})
|
||||
: getPurchaseCheckoutUrl({
|
||||
productId: bundleListingData.id,
|
||||
priceName: price.name,
|
||||
userId: profile.id,
|
||||
userEmail: profile.email,
|
||||
...(password ? { password } : undefined),
|
||||
});
|
||||
|
||||
if (fastCheckout) {
|
||||
const checkoutUrl = getStripeCheckoutUrl({
|
||||
productId: bundleListingData.id,
|
||||
priceName: price.name,
|
||||
userId: profile ? profile.id : undefined,
|
||||
userEmail: profile ? profile.email : undefined,
|
||||
userUuid: profile ? undefined : getUserUUID(),
|
||||
password: password || undefined,
|
||||
});
|
||||
// Mark the Url as opening if opening in the same tab, as it can take some time to load.
|
||||
setIsOpeningUrl(willReceiveAnEmailForThePurchase);
|
||||
Window.openExternalURL(checkoutUrl, {
|
||||
shouldOpenInSameTabIfPossible: willReceiveAnEmailForThePurchase,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Cannot continue without a profile if not doing fast checkout.
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
const checkoutUrl = getPurchaseCheckoutUrl({
|
||||
productId: bundleListingData.id,
|
||||
priceName: price.name,
|
||||
userId: profile.id,
|
||||
userEmail: profile.email,
|
||||
password: password || undefined,
|
||||
});
|
||||
Window.openExternalURL(checkoutUrl);
|
||||
} catch (error) {
|
||||
const extractedStatusAndCode = extractGDevelopApiErrorStatusAndCode(
|
||||
@@ -155,6 +176,16 @@ const BundlePurchaseDialog = ({
|
||||
[]
|
||||
);
|
||||
|
||||
const onCloseDialog = React.useCallback(
|
||||
() => {
|
||||
if (onCloseAfterPurchaseDone && purchaseSuccessful) {
|
||||
onCloseAfterPurchaseDone();
|
||||
}
|
||||
onClose();
|
||||
},
|
||||
[onCloseAfterPurchaseDone, purchaseSuccessful, onClose]
|
||||
);
|
||||
|
||||
// This effect will be triggered when the bundle purchases change,
|
||||
// to check if the user has just bought the product.
|
||||
React.useEffect(
|
||||
@@ -170,7 +201,6 @@ const BundlePurchaseDialog = ({
|
||||
// We found the purchase, the user has bought the bundle.
|
||||
// We do not close the dialog yet, as we need to trigger a refresh of the products received.
|
||||
await onPurchaseSuccessful();
|
||||
if (onPurchaseDone) onPurchaseDone();
|
||||
}
|
||||
};
|
||||
checkIfPurchaseIsDone();
|
||||
@@ -180,7 +210,7 @@ const BundlePurchaseDialog = ({
|
||||
bundlePurchases,
|
||||
bundleListingData,
|
||||
onPurchaseSuccessful,
|
||||
onPurchaseDone,
|
||||
onCloseAfterPurchaseDone,
|
||||
onRefreshBundlePurchases,
|
||||
]
|
||||
);
|
||||
@@ -189,7 +219,7 @@ const BundlePurchaseDialog = ({
|
||||
() => {
|
||||
onRefreshBundlePurchases();
|
||||
},
|
||||
isPurchasing ? 3900 : null
|
||||
isPurchasing && !willReceiveAnEmailForThePurchase ? 3900 : null
|
||||
);
|
||||
|
||||
// Listen to the received bundle, to know when a user has just logged in and the received bundles have been loaded.
|
||||
@@ -226,7 +256,7 @@ const BundlePurchaseDialog = ({
|
||||
setIsPurchasing(false);
|
||||
setPurchaseSuccessful(true);
|
||||
} else if (!purchaseSuccessful) {
|
||||
onClose();
|
||||
onCloseDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,106 +265,121 @@ const BundlePurchaseDialog = ({
|
||||
receivedBundles,
|
||||
bundleListingData,
|
||||
isPurchasing,
|
||||
onClose,
|
||||
onCloseDialog,
|
||||
isCheckingPurchasesAfterLogin,
|
||||
purchaseSuccessful,
|
||||
]
|
||||
);
|
||||
|
||||
const dialogContents = !profile
|
||||
? {
|
||||
subtitle: <Trans>Log-in to purchase this item</Trans>,
|
||||
content: (
|
||||
<CreateProfile
|
||||
onOpenLoginDialog={onOpenLoginDialog}
|
||||
onOpenCreateAccountDialog={onOpenCreateAccountDialog}
|
||||
message={
|
||||
<Trans>
|
||||
Bundles and their content will be linked to your user account
|
||||
and available for all your projects. Log-in or sign-up to
|
||||
purchase this bundle. (or restore your existing purchase).
|
||||
</Trans>
|
||||
}
|
||||
justifyContent="center"
|
||||
/>
|
||||
),
|
||||
}
|
||||
: purchaseSuccessful
|
||||
? {
|
||||
subtitle: <Trans>Your purchase has been processed!</Trans>,
|
||||
content: (
|
||||
<Line justifyContent="center" alignItems="center">
|
||||
<Text>
|
||||
<Trans>You can now go back to use your new bundle.</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
),
|
||||
}
|
||||
: isPurchasing
|
||||
? {
|
||||
subtitle: shouldUseOrSimulateAppStoreProduct ? (
|
||||
<Trans>Complete your purchase with the app store.</Trans>
|
||||
) : (
|
||||
<Trans>Complete your payment on the web browser</Trans>
|
||||
),
|
||||
content: shouldUseOrSimulateAppStoreProduct ? (
|
||||
<>
|
||||
<ColumnStackLayout justifyContent="center" alignItems="center">
|
||||
<CircularProgress size={40} />
|
||||
<Text>
|
||||
const dialogContents =
|
||||
!profile && !fastCheckout
|
||||
? {
|
||||
subtitle: <Trans>Log-in to purchase this item</Trans>,
|
||||
content: (
|
||||
<CreateProfile
|
||||
onOpenLoginDialog={onOpenLoginDialog}
|
||||
onOpenCreateAccountDialog={onOpenCreateAccountDialog}
|
||||
message={
|
||||
<Trans>
|
||||
The purchase will be linked to your account once done.
|
||||
Bundles and their content will be linked to your user account
|
||||
and available for all your projects. Log-in or sign-up to
|
||||
purchase this bundle. (or restore your existing purchase).
|
||||
</Trans>
|
||||
}
|
||||
justifyContent="center"
|
||||
/>
|
||||
),
|
||||
}
|
||||
: purchaseSuccessful
|
||||
? {
|
||||
subtitle: <Trans>Your purchase has been processed!</Trans>,
|
||||
content: (
|
||||
<Line justifyContent="center" alignItems="center">
|
||||
<Text>
|
||||
<Trans>You can now go back to use your new bundle.</Trans>
|
||||
</Text>
|
||||
</ColumnStackLayout>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
</Line>
|
||||
),
|
||||
}
|
||||
: isPurchasing
|
||||
? {
|
||||
subtitle: shouldUseOrSimulateAppStoreProduct ? (
|
||||
<Trans>Complete your purchase with the app store.</Trans>
|
||||
) : (
|
||||
<Trans>Complete your payment on the web browser</Trans>
|
||||
),
|
||||
content: shouldUseOrSimulateAppStoreProduct ? (
|
||||
<>
|
||||
<ColumnStackLayout justifyContent="center" alignItems="center">
|
||||
<CircularProgress size={40} />
|
||||
<Text>
|
||||
<Trans>
|
||||
The purchase will be linked to your account once done.
|
||||
</Trans>
|
||||
</Text>
|
||||
</ColumnStackLayout>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{!willReceiveAnEmailForThePurchase && (
|
||||
<Line justifyContent="center" alignItems="center">
|
||||
<CircularProgress size={20} />
|
||||
<Spacer />
|
||||
<Text>
|
||||
<Trans>Waiting for the purchase confirmation...</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
)}
|
||||
<Spacer />
|
||||
<Line justifyContent="center">
|
||||
<BackgroundText>
|
||||
{!willReceiveAnEmailForThePurchase ? (
|
||||
<Trans>
|
||||
Once you're done, come back to GDevelop and the bundle
|
||||
will be added to your account automatically.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Once you're done, you will receive an email confirmation
|
||||
so that you can link the bundle to your account.
|
||||
</Trans>
|
||||
)}
|
||||
</BackgroundText>
|
||||
</Line>
|
||||
</>
|
||||
),
|
||||
}
|
||||
: isCheckingPurchasesAfterLogin
|
||||
? {
|
||||
subtitle: <Trans>Loading your profile...</Trans>,
|
||||
content: (
|
||||
<Line justifyContent="center" alignItems="center">
|
||||
<CircularProgress size={20} />
|
||||
<Spacer />
|
||||
</Line>
|
||||
),
|
||||
}
|
||||
: {
|
||||
subtitle: profile ? (
|
||||
<Trans>
|
||||
The bundle {bundleListingData.name} will be linked to your account{' '}
|
||||
{profile.email}.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
The bundle {bundleListingData.name} will be sent to the email
|
||||
address provided in the checkout.
|
||||
</Trans>
|
||||
),
|
||||
content: shouldUseOrSimulateAppStoreProduct ? null : (
|
||||
<Line justifyContent="center" alignItems="center">
|
||||
<Text>
|
||||
<Trans>Waiting for the purchase confirmation...</Trans>
|
||||
<Trans>
|
||||
A new secure window will open to complete the purchase.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Spacer />
|
||||
<Line justifyContent="center">
|
||||
<BackgroundText>
|
||||
<Trans>
|
||||
Once you're done, come back to GDevelop and the bundle will be
|
||||
added to your account automatically.
|
||||
</Trans>
|
||||
</BackgroundText>
|
||||
</Line>
|
||||
</>
|
||||
),
|
||||
}
|
||||
: isCheckingPurchasesAfterLogin
|
||||
? {
|
||||
subtitle: <Trans>Loading your profile...</Trans>,
|
||||
content: (
|
||||
<Line justifyContent="center" alignItems="center">
|
||||
<CircularProgress size={20} />
|
||||
</Line>
|
||||
),
|
||||
}
|
||||
: {
|
||||
subtitle: (
|
||||
<Trans>
|
||||
The bundle {bundleListingData.name} will be linked to your account{' '}
|
||||
{profile.email}.
|
||||
</Trans>
|
||||
),
|
||||
content: shouldUseOrSimulateAppStoreProduct ? null : (
|
||||
<Line justifyContent="center" alignItems="center">
|
||||
<Text>
|
||||
<Trans>
|
||||
A new secure window will open to complete the purchase.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
),
|
||||
};
|
||||
),
|
||||
};
|
||||
|
||||
const allowPurchase =
|
||||
profile &&
|
||||
@@ -344,8 +389,17 @@ const BundlePurchaseDialog = ({
|
||||
const dialogActions = [
|
||||
<FlatButton
|
||||
key="cancel"
|
||||
label={purchaseSuccessful ? <Trans>Close</Trans> : <Trans>Cancel</Trans>}
|
||||
onClick={onClose}
|
||||
label={
|
||||
isOpeningUrl ? (
|
||||
<Trans>Loading...</Trans>
|
||||
) : purchaseSuccessful || willReceiveAnEmailForThePurchase ? (
|
||||
<Trans>Close</Trans>
|
||||
) : (
|
||||
<Trans>Cancel</Trans>
|
||||
)
|
||||
}
|
||||
onClick={onCloseDialog}
|
||||
disabled={isOpeningUrl}
|
||||
/>,
|
||||
allowPurchase ? (
|
||||
<DialogPrimaryButton
|
||||
@@ -363,9 +417,9 @@ const BundlePurchaseDialog = ({
|
||||
title={<Trans>{bundleListingData.name}</Trans>}
|
||||
maxWidth="sm"
|
||||
open
|
||||
onRequestClose={onClose}
|
||||
onRequestClose={onCloseDialog}
|
||||
actions={dialogActions}
|
||||
onApply={purchaseSuccessful ? onClose : onWillPurchase}
|
||||
onApply={purchaseSuccessful ? onCloseDialog : onWillPurchase}
|
||||
cannotBeDismissed // Prevent the user from continuing by clicking outside.
|
||||
flexColumnBody
|
||||
>
|
||||
|
@@ -30,15 +30,17 @@ type CreditsPackageStoreState = {|
|
||||
openCreditsUsageDialog: CreditsUsageDialogOptions => void,
|
||||
|};
|
||||
|
||||
export const initialCreditsPackageStoreState: CreditsPackageStoreState = {
|
||||
fetchCreditsPackages: () => {},
|
||||
creditsPackageListingDatas: null,
|
||||
error: null,
|
||||
openCreditsPackageDialog: () => {},
|
||||
closeCreditsPackageDialog: () => {},
|
||||
openCreditsUsageDialog: () => {},
|
||||
};
|
||||
|
||||
export const CreditsPackageStoreContext = React.createContext<CreditsPackageStoreState>(
|
||||
{
|
||||
fetchCreditsPackages: () => {},
|
||||
creditsPackageListingDatas: null,
|
||||
error: null,
|
||||
openCreditsPackageDialog: () => {},
|
||||
closeCreditsPackageDialog: () => {},
|
||||
openCreditsUsageDialog: () => {},
|
||||
}
|
||||
initialCreditsPackageStoreState
|
||||
);
|
||||
|
||||
// Ids are in the form "amount_credits" (e.g: "500_credits").
|
||||
|
@@ -15,7 +15,7 @@ type CourseChapterStoreState = {|
|
||||
listedCourseChapters: CourseChapterListingData[] | null,
|
||||
|};
|
||||
|
||||
const initialCourseStoreState: CourseChapterStoreState = {
|
||||
export const initialCourseStoreState: CourseChapterStoreState = {
|
||||
listedCourses: null,
|
||||
listedCourseChapters: null,
|
||||
};
|
||||
|
@@ -5,6 +5,7 @@ import Window from '../Utils/Window';
|
||||
export type Route =
|
||||
| 'onboarding' // For compatibility when there was only one tutorial.
|
||||
| 'guided-lesson' // New way of opening a tutorial.
|
||||
| 'signup'
|
||||
| 'subscription'
|
||||
| 'games-dashboard'
|
||||
| 'asset-store' // For compatibility when there was only asset packs.
|
||||
@@ -14,7 +15,8 @@ export type Route =
|
||||
| 'education'
|
||||
| 'learn'
|
||||
| 'play'
|
||||
| 'standalone';
|
||||
| 'standalone'
|
||||
| 'ask-ai';
|
||||
type RouteKey =
|
||||
| 'initial-dialog'
|
||||
| 'game-id'
|
||||
@@ -27,7 +29,9 @@ type RouteKey =
|
||||
| 'course-id'
|
||||
| 'create-from-example'
|
||||
| 'recommended-plan-id'
|
||||
| 'playable-game-id';
|
||||
| 'playable-game-id'
|
||||
| 'purchase-id'
|
||||
| 'claimable-token';
|
||||
export type RouteArguments = { [RouteKey]: string };
|
||||
|
||||
export type Router = {|
|
||||
@@ -37,14 +41,14 @@ export type Router = {|
|
||||
navigateToRoute: (route: Route, additionalArgument?: RouteArguments) => void,
|
||||
|};
|
||||
|
||||
const initialState: Router = {
|
||||
export const initialRouterState: Router = {
|
||||
routeArguments: {},
|
||||
removeRouteArguments: () => {},
|
||||
addRouteArguments: () => {},
|
||||
navigateToRoute: () => {},
|
||||
};
|
||||
|
||||
const RouterContext = React.createContext<Router>(initialState);
|
||||
const RouterContext = React.createContext<Router>(initialRouterState);
|
||||
|
||||
export default RouterContext;
|
||||
|
||||
|
@@ -98,8 +98,8 @@ const StandaloneDialog = ({ onClose }: Props) => {
|
||||
courses={courses}
|
||||
getCourseCompletion={getCourseCompletion}
|
||||
noActions
|
||||
simpleCheckout
|
||||
onPurchaseDone={() => {
|
||||
fastCheckout
|
||||
onCloseAfterPurchaseDone={() => {
|
||||
navigateToRoute('learn', {
|
||||
bundle: selectedBundleListingData.id,
|
||||
});
|
||||
@@ -112,7 +112,10 @@ const StandaloneDialog = ({ onClose }: Props) => {
|
||||
};
|
||||
|
||||
const StandaloneDialogWithErrorBoundary = (props: Props) => (
|
||||
<ErrorBoundary componentTitle={<Trans>About dialog</Trans>} scope="about">
|
||||
<ErrorBoundary
|
||||
componentTitle={<Trans>Standalone dialog</Trans>}
|
||||
scope="standalone"
|
||||
>
|
||||
<StandaloneDialog {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
@@ -194,7 +194,7 @@ import { QuickCustomizationDialog } from '../QuickCustomization/QuickCustomizati
|
||||
import { type ObjectWithContext } from '../ObjectsList/EnumerateObjects';
|
||||
import useGamesList from '../GameDashboard/UseGamesList';
|
||||
import useCapturesManager from './UseCapturesManager';
|
||||
import useOpenPageForRouting from './useOpenPageForRouting';
|
||||
import useHomePageSwitch from './useHomePageSwitch';
|
||||
import RobotIcon from '../ProjectCreation/RobotIcon';
|
||||
import PublicProfileContext from '../Profile/PublicProfileContext';
|
||||
import { useGamesPlatformFrame } from './EditorContainers/HomePage/PlaySection/UseGamesPlatformFrame';
|
||||
@@ -2098,9 +2098,8 @@ const MainFrame = (props: Props) => {
|
||||
[setStandaloneDialogOpen]
|
||||
);
|
||||
|
||||
const { navigateToRoute } = useOpenPageForRouting({
|
||||
const { navigateToRoute } = useHomePageSwitch({
|
||||
openHomePage,
|
||||
openStandaloneDialog,
|
||||
closeDialogs: closeDialogsToOpenHomePage,
|
||||
});
|
||||
|
||||
@@ -3318,6 +3317,7 @@ const MainFrame = (props: Props) => {
|
||||
openInAppTutorialDialog: selectInAppTutorial,
|
||||
openProfileDialog: onOpenProfileDialog,
|
||||
openAskAi,
|
||||
openStandaloneDialog,
|
||||
});
|
||||
|
||||
const onChangeProjectName = async (newName: string): Promise<void> => {
|
||||
|
@@ -14,19 +14,15 @@ const homePageRoutes: Route[] = [
|
||||
'education',
|
||||
];
|
||||
|
||||
const standaloneRoute = 'standalone';
|
||||
|
||||
/**
|
||||
* This hook is used to be able to use route arguments from anywhere to open the homepage.
|
||||
* It should close dialogs that prevent the navigation to the homepage.
|
||||
*/
|
||||
const useOpenPageForRouting = ({
|
||||
const useHomePageSwitch = ({
|
||||
openHomePage,
|
||||
openStandaloneDialog,
|
||||
closeDialogs,
|
||||
}: {|
|
||||
openHomePage: () => void,
|
||||
openStandaloneDialog: () => void,
|
||||
closeDialogs: () => void,
|
||||
|}) => {
|
||||
const {
|
||||
@@ -45,24 +41,12 @@ const useOpenPageForRouting = ({
|
||||
closeDialogs();
|
||||
openHomePage();
|
||||
}
|
||||
|
||||
if (initialDialog === standaloneRoute) {
|
||||
closeDialogs();
|
||||
openStandaloneDialog();
|
||||
removeRouteArguments(['initial-dialog']);
|
||||
}
|
||||
},
|
||||
[
|
||||
routeArguments,
|
||||
openHomePage,
|
||||
closeDialogs,
|
||||
openStandaloneDialog,
|
||||
removeRouteArguments,
|
||||
]
|
||||
[routeArguments, openHomePage, closeDialogs, removeRouteArguments]
|
||||
);
|
||||
return {
|
||||
navigateToRoute,
|
||||
};
|
||||
};
|
||||
|
||||
export default useOpenPageForRouting;
|
||||
export default useHomePageSwitch;
|
@@ -30,6 +30,7 @@ import {
|
||||
type Bundle,
|
||||
} from '../Utils/GDevelopServices/Asset';
|
||||
import { type Purchase } from '../Utils/GDevelopServices/Shop';
|
||||
import { type ClaimedProductOptions } from './PurchaseClaimDialog';
|
||||
|
||||
export type AuthenticatedUser = {|
|
||||
authenticated: boolean,
|
||||
@@ -69,9 +70,18 @@ export type AuthenticatedUser = {|
|
||||
) => Promise<void>,
|
||||
onResetPassword: ForgotPasswordForm => Promise<void>,
|
||||
onOpenLoginDialog: () => void,
|
||||
onOpenLoginWithPurchaseClaimDialog: (
|
||||
claimedProductOptions: ClaimedProductOptions
|
||||
) => void,
|
||||
onOpenEditProfileDialog: () => void,
|
||||
onOpenChangeEmailDialog: () => void,
|
||||
onOpenCreateAccountDialog: () => void,
|
||||
onOpenCreateAccountWithPurchaseClaimDialog: (
|
||||
claimedProductOptions: ClaimedProductOptions
|
||||
) => void,
|
||||
onOpenPurchaseClaimDialog: (
|
||||
claimedProductOptions: ClaimedProductOptions
|
||||
) => void,
|
||||
onBadgesChanged: () => Promise<void>,
|
||||
onCloudProjectsChanged: () => Promise<void>,
|
||||
onRefreshUserProfile: () => Promise<void>,
|
||||
@@ -131,9 +141,12 @@ export const initialAuthenticatedUser = {
|
||||
onEditProfile: async () => {},
|
||||
onResetPassword: async () => {},
|
||||
onOpenLoginDialog: () => {},
|
||||
onOpenLoginWithPurchaseClaimDialog: () => {},
|
||||
onOpenEditProfileDialog: () => {},
|
||||
onOpenChangeEmailDialog: () => {},
|
||||
onOpenCreateAccountDialog: () => {},
|
||||
onOpenCreateAccountWithPurchaseClaimDialog: () => {},
|
||||
onOpenPurchaseClaimDialog: () => {},
|
||||
onBadgesChanged: async () => {},
|
||||
onCloudProjectsChanged: async () => {},
|
||||
onRefreshUserProfile: async () => {},
|
||||
|
@@ -63,6 +63,11 @@ import { showErrorBox } from '../UI/Messages/MessageBox';
|
||||
import { userCancellationErrorName } from '../LoginProvider/Utils';
|
||||
import { listUserPurchases } from '../Utils/GDevelopServices/Shop';
|
||||
import { listNotifications } from '../Utils/GDevelopServices/Notification';
|
||||
import LoginWithPurchaseClaimDialog from './LoginWithPurchaseClaimDialog';
|
||||
import CreateAccountWithPurchaseClaimDialog from './CreateAccountWithPurchaseClaimDialog';
|
||||
import PurchaseClaimDialog, {
|
||||
type ClaimedProductOptions,
|
||||
} from './PurchaseClaimDialog';
|
||||
|
||||
type Props = {|
|
||||
authentication: Authentication,
|
||||
@@ -73,7 +78,9 @@ type Props = {|
|
||||
type State = {|
|
||||
authenticatedUser: AuthenticatedUser,
|
||||
loginDialogOpen: boolean,
|
||||
loginWithPurchaseClaimDialogOpen: boolean,
|
||||
createAccountDialogOpen: boolean,
|
||||
createAccountWithPurchaseClaimDialogOpen: boolean,
|
||||
loginInProgress: boolean,
|
||||
createAccountInProgress: boolean,
|
||||
editProfileDialogOpen: boolean,
|
||||
@@ -90,6 +97,7 @@ type State = {|
|
||||
changeEmailDialogOpen: boolean,
|
||||
changeEmailInProgress: boolean,
|
||||
userSnackbarMessage: ?React.Node,
|
||||
claimedProductOptions: ?ClaimedProductOptions,
|
||||
|};
|
||||
|
||||
const cleanUserTracesOnDevice = async () => {
|
||||
@@ -109,7 +117,9 @@ export default class AuthenticatedUserProvider extends React.Component<
|
||||
state = {
|
||||
authenticatedUser: initialAuthenticatedUser,
|
||||
loginDialogOpen: false,
|
||||
loginWithPurchaseClaimDialogOpen: false,
|
||||
createAccountDialogOpen: false,
|
||||
createAccountWithPurchaseClaimDialogOpen: false,
|
||||
loginInProgress: false,
|
||||
createAccountInProgress: false,
|
||||
editProfileDialogOpen: false,
|
||||
@@ -126,6 +136,7 @@ export default class AuthenticatedUserProvider extends React.Component<
|
||||
changeEmailDialogOpen: false,
|
||||
changeEmailInProgress: false,
|
||||
userSnackbarMessage: null,
|
||||
claimedProductOptions: null,
|
||||
};
|
||||
_automaticallyUpdateUserProfile = true;
|
||||
_hasNotifiedUserAboutEmailVerification = false;
|
||||
@@ -210,9 +221,22 @@ export default class AuthenticatedUserProvider extends React.Component<
|
||||
onBadgesChanged: this._fetchUserBadges,
|
||||
onCloudProjectsChanged: this._fetchUserCloudProjects,
|
||||
onOpenLoginDialog: () => this.openLoginDialog(true),
|
||||
onOpenLoginWithPurchaseClaimDialog: (
|
||||
claimedProductOptions: ClaimedProductOptions
|
||||
) => this.openLoginWithPurchaseClaimDialog(true, claimedProductOptions),
|
||||
onOpenEditProfileDialog: () => this.openEditProfileDialog(true),
|
||||
onOpenChangeEmailDialog: () => this.openChangeEmailDialog(true),
|
||||
onOpenCreateAccountDialog: () => this.openCreateAccountDialog(true),
|
||||
onOpenCreateAccountWithPurchaseClaimDialog: (
|
||||
claimedProductOptions: ClaimedProductOptions
|
||||
) =>
|
||||
this.openCreateAccountWithPurchaseClaimDialog(
|
||||
true,
|
||||
claimedProductOptions
|
||||
),
|
||||
onOpenPurchaseClaimDialog: (
|
||||
claimedProductOptions: ClaimedProductOptions
|
||||
) => this.openPurchaseClaimDialog(claimedProductOptions),
|
||||
onRefreshUserProfile: this._fetchUserProfile,
|
||||
onRefreshFirebaseProfile: async () => {
|
||||
await this._reloadFirebaseProfile();
|
||||
@@ -1132,7 +1156,15 @@ export default class AuthenticatedUserProvider extends React.Component<
|
||||
});
|
||||
await this._fetchUserProfileWithoutThrowingErrors({ resetState: true });
|
||||
this.openLoginDialog(false);
|
||||
this.openLoginWithPurchaseClaimDialog(
|
||||
false,
|
||||
this.state.claimedProductOptions
|
||||
);
|
||||
this.openCreateAccountDialog(false);
|
||||
this.openCreateAccountWithPurchaseClaimDialog(
|
||||
false,
|
||||
this.state.claimedProductOptions
|
||||
);
|
||||
this._showLoginSnackbar(this.state.authenticatedUser);
|
||||
} catch (apiCallError) {
|
||||
if (apiCallError.name !== userCancellationErrorName) {
|
||||
@@ -1187,6 +1219,10 @@ export default class AuthenticatedUserProvider extends React.Component<
|
||||
await authentication.login(form);
|
||||
await this._fetchUserProfileWithoutThrowingErrors({ resetState: true });
|
||||
this.openLoginDialog(false);
|
||||
this.openLoginWithPurchaseClaimDialog(
|
||||
false,
|
||||
this.state.claimedProductOptions
|
||||
);
|
||||
this._showLoginSnackbar(this.state.authenticatedUser);
|
||||
} catch (apiCallError) {
|
||||
this.setState({
|
||||
@@ -1289,6 +1325,10 @@ export default class AuthenticatedUserProvider extends React.Component<
|
||||
resetState: true,
|
||||
});
|
||||
this.openCreateAccountDialog(false);
|
||||
this.openCreateAccountWithPurchaseClaimDialog(
|
||||
false,
|
||||
this.state.claimedProductOptions
|
||||
);
|
||||
sendSignupDone(form.email);
|
||||
const firebaseUser = this.state.authenticatedUser.firebaseUser;
|
||||
aliasUserForAnalyticsAfterSignUp(firebaseUser);
|
||||
@@ -1452,6 +1492,18 @@ export default class AuthenticatedUserProvider extends React.Component<
|
||||
});
|
||||
};
|
||||
|
||||
openLoginWithPurchaseClaimDialog = (
|
||||
open: boolean = true,
|
||||
claimedProductOptions: ?ClaimedProductOptions = null
|
||||
) => {
|
||||
this.setState({
|
||||
loginWithPurchaseClaimDialogOpen: open,
|
||||
createAccountWithPurchaseClaimDialogOpen: false,
|
||||
apiCallError: null,
|
||||
claimedProductOptions,
|
||||
});
|
||||
};
|
||||
|
||||
showUserSnackbar = ({ message }: {| message: ?React.Node |}) => {
|
||||
this.setState({
|
||||
// The message is wrapped here to prevent crashes when Google Translate
|
||||
@@ -1475,6 +1527,26 @@ export default class AuthenticatedUserProvider extends React.Component<
|
||||
});
|
||||
};
|
||||
|
||||
openCreateAccountWithPurchaseClaimDialog = (
|
||||
open: boolean = true,
|
||||
claimedProductOptions: ?ClaimedProductOptions = null
|
||||
) => {
|
||||
this.setState({
|
||||
loginWithPurchaseClaimDialogOpen: false,
|
||||
createAccountWithPurchaseClaimDialogOpen: open,
|
||||
apiCallError: null,
|
||||
claimedProductOptions,
|
||||
});
|
||||
};
|
||||
|
||||
openPurchaseClaimDialog = (
|
||||
claimedProductOptions: ?ClaimedProductOptions = null
|
||||
) => {
|
||||
this.setState({
|
||||
claimedProductOptions,
|
||||
});
|
||||
};
|
||||
|
||||
openChangeEmailDialog = (open: boolean = true) => {
|
||||
this.setState({
|
||||
changeEmailDialogOpen: open,
|
||||
@@ -1614,13 +1686,33 @@ export default class AuthenticatedUserProvider extends React.Component<
|
||||
}}
|
||||
onGoToCreateAccount={() => this.openCreateAccountDialog(true)}
|
||||
onLogin={this._doLogin}
|
||||
onLogout={this._doLogout}
|
||||
onLoginWithProvider={this._doLoginWithProvider}
|
||||
loginInProgress={this.state.loginInProgress}
|
||||
error={this.state.apiCallError}
|
||||
onForgotPassword={this._doForgotPassword}
|
||||
/>
|
||||
)}
|
||||
{this.state.loginWithPurchaseClaimDialogOpen &&
|
||||
this.state.claimedProductOptions && (
|
||||
<LoginWithPurchaseClaimDialog
|
||||
onClose={() => {
|
||||
this._cancelLoginOrSignUp();
|
||||
this.openLoginWithPurchaseClaimDialog(false);
|
||||
}}
|
||||
onGoToCreateAccount={() =>
|
||||
this.openCreateAccountWithPurchaseClaimDialog(
|
||||
true,
|
||||
this.state.claimedProductOptions
|
||||
)
|
||||
}
|
||||
onLogin={this._doLogin}
|
||||
onLoginWithProvider={this._doLoginWithProvider}
|
||||
loginInProgress={this.state.loginInProgress}
|
||||
error={this.state.apiCallError}
|
||||
onForgotPassword={this._doForgotPassword}
|
||||
claimedProductOptions={this.state.claimedProductOptions}
|
||||
/>
|
||||
)}
|
||||
{this.state.authenticatedUser.profile &&
|
||||
this.state.editProfileDialogOpen && (
|
||||
<EditProfileDialog
|
||||
@@ -1694,6 +1786,28 @@ export default class AuthenticatedUserProvider extends React.Component<
|
||||
error={this.state.apiCallError}
|
||||
/>
|
||||
)}
|
||||
{this.state.createAccountWithPurchaseClaimDialogOpen &&
|
||||
this.state.claimedProductOptions && (
|
||||
<CreateAccountWithPurchaseClaimDialog
|
||||
onClose={() => {
|
||||
this._cancelLoginOrSignUp();
|
||||
this.openCreateAccountWithPurchaseClaimDialog(false);
|
||||
}}
|
||||
onGoToLogin={() =>
|
||||
this.openLoginWithPurchaseClaimDialog(
|
||||
true,
|
||||
this.state.claimedProductOptions
|
||||
)
|
||||
}
|
||||
onCreateAccount={form =>
|
||||
this._doCreateAccount(form, this.props.preferencesValues)
|
||||
}
|
||||
onLoginWithProvider={this._doLoginWithProvider}
|
||||
createAccountInProgress={this.state.createAccountInProgress}
|
||||
error={this.state.apiCallError}
|
||||
claimedProductOptions={this.state.claimedProductOptions}
|
||||
/>
|
||||
)}
|
||||
{this.state.emailVerificationDialogOpen && (
|
||||
<EmailVerificationDialog
|
||||
authenticatedUser={this.state.authenticatedUser}
|
||||
@@ -1711,6 +1825,12 @@ export default class AuthenticatedUserProvider extends React.Component<
|
||||
onSendEmail={this._doSendEmailVerification}
|
||||
/>
|
||||
)}
|
||||
{this.state.claimedProductOptions && (
|
||||
<PurchaseClaimDialog
|
||||
claimedProductOptions={this.state.claimedProductOptions}
|
||||
onClose={() => this.openPurchaseClaimDialog(null)}
|
||||
/>
|
||||
)}
|
||||
<Snackbar
|
||||
open={!!this.state.userSnackbarMessage}
|
||||
autoHideDuration={3000}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
@@ -189,10 +189,18 @@ const CreateAccountDialog = ({
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<GDevelopGLogo fontSize="large" />
|
||||
<Text size="section-title" align="center" noMargin>
|
||||
<Trans>Welcome to GDevelop!</Trans>
|
||||
</Text>
|
||||
{
|
||||
<ColumnStackLayout
|
||||
noMargin
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<GDevelopGLogo fontSize="large" />
|
||||
<Text size="section-title" align="center" noMargin>
|
||||
<Trans>Welcome to GDevelop!</Trans>
|
||||
</Text>
|
||||
</ColumnStackLayout>
|
||||
}
|
||||
<div style={styles.formContainer}>
|
||||
<CreateAccountForm
|
||||
onCreateAccount={createAccount}
|
||||
|
185
newIDE/app/src/Profile/CreateAccountWithPurchaseClaimDialog.js
Normal file
185
newIDE/app/src/Profile/CreateAccountWithPurchaseClaimDialog.js
Normal file
@@ -0,0 +1,185 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import Dialog, { DialogPrimaryButton } from '../UI/Dialog';
|
||||
import {
|
||||
type RegisterForm,
|
||||
type AuthError,
|
||||
type IdentityProvider,
|
||||
} from '../Utils/GDevelopServices/Authentication';
|
||||
import { type UsernameAvailability } from '../Utils/GDevelopServices/User';
|
||||
import LeftLoader from '../UI/LeftLoader';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import { isUsernameValid } from './UsernameField';
|
||||
import HelpButton from '../UI/HelpButton';
|
||||
import Text from '../UI/Text';
|
||||
import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import CreateAccountForm from './CreateAccountForm';
|
||||
import { CorsAwareImage } from '../UI/CorsAwareImage';
|
||||
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
|
||||
import { type ClaimedProductOptions } from './PurchaseClaimDialog';
|
||||
|
||||
const getStyles = ({ isMobile }) => {
|
||||
return {
|
||||
formContainer: {
|
||||
display: 'flex',
|
||||
width: isMobile ? '95%' : '90%',
|
||||
marginTop: 10,
|
||||
flexDirection: 'column',
|
||||
},
|
||||
previewImage: {
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
display: 'block',
|
||||
objectFit: 'contain',
|
||||
borderRadius: 8,
|
||||
border: '1px solid lightgrey',
|
||||
boxSizing: 'border-box', // Take border in account for sizing to avoid cumulative layout shift.
|
||||
// Prevent cumulative layout shift by enforcing
|
||||
// the 16:9 ratio.
|
||||
aspectRatio: '16 / 9',
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
position: 'relative',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
onClose: () => void,
|
||||
onGoToLogin: () => void,
|
||||
onCreateAccount: (form: RegisterForm) => Promise<void>,
|
||||
onLoginWithProvider: (provider: IdentityProvider) => Promise<void>,
|
||||
createAccountInProgress: boolean,
|
||||
error: ?AuthError,
|
||||
claimedProductOptions: ClaimedProductOptions,
|
||||
|};
|
||||
|
||||
const CreateAccountWithPurchaseClaimDialog = ({
|
||||
onClose,
|
||||
onGoToLogin,
|
||||
onCreateAccount,
|
||||
onLoginWithProvider,
|
||||
createAccountInProgress,
|
||||
error,
|
||||
claimedProductOptions: { productListingData: claimedProduct },
|
||||
}: Props) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const styles = getStyles({ isMobile });
|
||||
const [email, setEmail] = React.useState<string>('');
|
||||
const [password, setPassword] = React.useState<string>('');
|
||||
const [username, setUsername] = React.useState<string>('');
|
||||
const [getNewsletterEmail, setGetNewsletterEmail] = React.useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [
|
||||
usernameAvailability,
|
||||
setUsernameAvailability,
|
||||
] = React.useState<?UsernameAvailability>(null);
|
||||
const [
|
||||
isValidatingUsername,
|
||||
setIsValidatingUsername,
|
||||
] = React.useState<boolean>(false);
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
|
||||
const canCreateAccount =
|
||||
!createAccountInProgress &&
|
||||
isUsernameValid(username, { allowEmpty: true }) &&
|
||||
!isValidatingUsername &&
|
||||
(!usernameAvailability || usernameAvailability.isAvailable);
|
||||
|
||||
const createAccount = async () => {
|
||||
if (!canCreateAccount) return;
|
||||
try {
|
||||
await onCreateAccount({
|
||||
email: email.trim(),
|
||||
password,
|
||||
username,
|
||||
getNewsletterEmail,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error while creating account', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={null} // This dialog has a custom design to be more welcoming, the title is set in the content.
|
||||
id="create-account-with-purchase-claim-dialog"
|
||||
actions={[
|
||||
<FlatButton
|
||||
label={<Trans>Cancel</Trans>}
|
||||
key="close"
|
||||
primary={false}
|
||||
onClick={onClose}
|
||||
/>,
|
||||
<LeftLoader isLoading={createAccountInProgress} key="create-account">
|
||||
<DialogPrimaryButton
|
||||
label={<Trans>Create account</Trans>}
|
||||
id="create-account-button"
|
||||
primary
|
||||
disabled={!canCreateAccount}
|
||||
onClick={createAccount}
|
||||
/>
|
||||
</LeftLoader>,
|
||||
]}
|
||||
secondaryActions={[
|
||||
<HelpButton key="help" helpPagePath={'/interface/profile'} />,
|
||||
]}
|
||||
cannotBeDismissed={createAccountInProgress}
|
||||
onApply={createAccount}
|
||||
onRequestClose={() => {
|
||||
if (!createAccountInProgress) onClose();
|
||||
}}
|
||||
maxWidth="md"
|
||||
open
|
||||
flexColumnBody
|
||||
>
|
||||
<ColumnStackLayout
|
||||
noMargin
|
||||
expand
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<ColumnStackLayout justifyContent="center" alignItems="center" noMargin>
|
||||
{claimedProduct.productType === 'BUNDLE' && (
|
||||
<CorsAwareImage
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
background: gdevelopTheme.paper.backgroundColor.light,
|
||||
}}
|
||||
src={claimedProduct.thumbnailUrls[0]}
|
||||
alt={`Preview image of bundle ${claimedProduct.name}`}
|
||||
/>
|
||||
)}
|
||||
<Text size="section-title" align="center" noMargin>
|
||||
<Trans>Create an account to activate your purchase!</Trans>
|
||||
</Text>
|
||||
</ColumnStackLayout>
|
||||
<div style={styles.formContainer}>
|
||||
<CreateAccountForm
|
||||
onCreateAccount={createAccount}
|
||||
onLoginWithProvider={onLoginWithProvider}
|
||||
email={email}
|
||||
onChangeEmail={setEmail}
|
||||
password={password}
|
||||
onChangePassword={setPassword}
|
||||
username={username}
|
||||
onChangeUsername={setUsername}
|
||||
optInNewsletterEmail={getNewsletterEmail}
|
||||
onChangeOptInNewsletterEmail={setGetNewsletterEmail}
|
||||
createAccountInProgress={createAccountInProgress}
|
||||
error={error}
|
||||
onChangeUsernameAvailability={setUsernameAvailability}
|
||||
isValidatingUsername={isValidatingUsername}
|
||||
onChangeIsValidatingUsername={setIsValidatingUsername}
|
||||
onGoToLogin={onGoToLogin}
|
||||
/>
|
||||
</div>
|
||||
</ColumnStackLayout>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateAccountWithPurchaseClaimDialog;
|
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import Dialog, { DialogPrimaryButton } from '../UI/Dialog';
|
||||
import {
|
||||
type LoginForm as LoginFormType,
|
||||
@@ -33,7 +33,6 @@ type Props = {|
|
||||
onClose: () => void,
|
||||
onGoToCreateAccount: () => void,
|
||||
onLogin: (form: LoginFormType) => Promise<void>,
|
||||
onLogout: () => Promise<void>,
|
||||
onLoginWithProvider: (provider: IdentityProvider) => Promise<void>,
|
||||
onForgotPassword: (form: ForgotPasswordForm) => Promise<void>,
|
||||
loginInProgress: boolean,
|
||||
@@ -44,7 +43,6 @@ const LoginDialog = ({
|
||||
onClose,
|
||||
onGoToCreateAccount,
|
||||
onLogin,
|
||||
onLogout,
|
||||
onLoginWithProvider,
|
||||
onForgotPassword,
|
||||
loginInProgress,
|
||||
@@ -93,10 +91,14 @@ const LoginDialog = ({
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<GDevelopGLogo fontSize="large" />
|
||||
<Text noMargin size="section-title" align="center">
|
||||
<Trans>Log in to your account</Trans>
|
||||
</Text>
|
||||
{
|
||||
<ColumnStackLayout noMargin justifyContent="center" alignItems="center">
|
||||
<GDevelopGLogo fontSize="large" />
|
||||
<Text noMargin size="section-title" align="center">
|
||||
<Trans>Log in to your account</Trans>
|
||||
</Text>
|
||||
</ColumnStackLayout>
|
||||
}
|
||||
<div style={styles.formContainer}>
|
||||
<LoginForm
|
||||
onLogin={doLogin}
|
||||
|
163
newIDE/app/src/Profile/LoginWithPurchaseClaimDialog.js
Normal file
163
newIDE/app/src/Profile/LoginWithPurchaseClaimDialog.js
Normal file
@@ -0,0 +1,163 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import Dialog, { DialogPrimaryButton } from '../UI/Dialog';
|
||||
import {
|
||||
type LoginForm as LoginFormType,
|
||||
type ForgotPasswordForm,
|
||||
type AuthError,
|
||||
type IdentityProvider,
|
||||
} from '../Utils/GDevelopServices/Authentication';
|
||||
import LoginForm from './LoginForm';
|
||||
import LeftLoader from '../UI/LeftLoader';
|
||||
import Text from '../UI/Text';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import HelpButton from '../UI/HelpButton';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import { CorsAwareImage } from '../UI/CorsAwareImage';
|
||||
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
|
||||
import { type ClaimedProductOptions } from './PurchaseClaimDialog';
|
||||
|
||||
const getStyles = ({ isMobile }) => {
|
||||
return {
|
||||
formContainer: {
|
||||
display: 'flex',
|
||||
width: isMobile ? '95%' : '90%',
|
||||
marginTop: 10,
|
||||
flexDirection: 'column',
|
||||
},
|
||||
previewImage: {
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
display: 'block',
|
||||
objectFit: 'contain',
|
||||
borderRadius: 8,
|
||||
border: '1px solid lightgrey',
|
||||
boxSizing: 'border-box', // Take border in account for sizing to avoid cumulative layout shift.
|
||||
// Prevent cumulative layout shift by enforcing
|
||||
// the 16:9 ratio.
|
||||
aspectRatio: '16 / 9',
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
position: 'relative',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
onClose: () => void,
|
||||
onGoToCreateAccount: () => void,
|
||||
onLogin: (form: LoginFormType) => Promise<void>,
|
||||
onLoginWithProvider: (provider: IdentityProvider) => Promise<void>,
|
||||
onForgotPassword: (form: ForgotPasswordForm) => Promise<void>,
|
||||
loginInProgress: boolean,
|
||||
error: ?AuthError,
|
||||
claimedProductOptions: ClaimedProductOptions,
|
||||
|};
|
||||
|
||||
const LoginWithPurchaseClaimDialog = ({
|
||||
onClose,
|
||||
onGoToCreateAccount,
|
||||
onLogin,
|
||||
onLoginWithProvider,
|
||||
onForgotPassword,
|
||||
loginInProgress,
|
||||
error,
|
||||
claimedProductOptions: { productListingData: claimedProduct },
|
||||
}: Props) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const [email, setEmail] = React.useState('');
|
||||
const [password, setPassword] = React.useState('');
|
||||
const styles = getStyles({ isMobile });
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
|
||||
const doLogin = () => {
|
||||
if (loginInProgress) return;
|
||||
|
||||
onLogin({
|
||||
email: email.trim(),
|
||||
password,
|
||||
});
|
||||
};
|
||||
|
||||
const actions = [
|
||||
<FlatButton
|
||||
label={<Trans>Cancel</Trans>}
|
||||
key="cancel"
|
||||
primary={false}
|
||||
onClick={onClose}
|
||||
/>,
|
||||
<LeftLoader isLoading={loginInProgress} key="login">
|
||||
<DialogPrimaryButton
|
||||
id="login-button"
|
||||
label={<Trans>Login</Trans>}
|
||||
primary
|
||||
onClick={doLogin}
|
||||
disabled={loginInProgress}
|
||||
/>
|
||||
</LeftLoader>,
|
||||
];
|
||||
|
||||
const secondaryActions = [
|
||||
<HelpButton key="help" helpPagePath={'/interface/profile'} />,
|
||||
];
|
||||
|
||||
const dialogContent = (
|
||||
<ColumnStackLayout
|
||||
noMargin
|
||||
expand
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<ColumnStackLayout justifyContent="center" alignItems="center" noMargin>
|
||||
{claimedProduct.productType === 'BUNDLE' && (
|
||||
<CorsAwareImage
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
background: gdevelopTheme.paper.backgroundColor.light,
|
||||
}}
|
||||
src={claimedProduct.thumbnailUrls[0]}
|
||||
alt={`Preview image of bundle ${claimedProduct.name}`}
|
||||
/>
|
||||
)}
|
||||
<Text size="section-title" align="center" noMargin>
|
||||
<Trans>Log in to your account to activate your purchase!</Trans>
|
||||
</Text>
|
||||
</ColumnStackLayout>
|
||||
<div style={styles.formContainer}>
|
||||
<LoginForm
|
||||
onLogin={doLogin}
|
||||
onLoginWithProvider={onLoginWithProvider}
|
||||
email={email}
|
||||
onChangeEmail={setEmail}
|
||||
password={password}
|
||||
onChangePassword={setPassword}
|
||||
onForgotPassword={onForgotPassword}
|
||||
loginInProgress={loginInProgress}
|
||||
error={error}
|
||||
onGoToCreateAccount={onGoToCreateAccount}
|
||||
/>
|
||||
</div>
|
||||
</ColumnStackLayout>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={null} // This dialog has a custom design to be more welcoming, the title is set in the content.
|
||||
id="login-dialog"
|
||||
actions={actions}
|
||||
secondaryActions={secondaryActions}
|
||||
cannotBeDismissed={loginInProgress}
|
||||
onRequestClose={onClose}
|
||||
onApply={doLogin}
|
||||
maxWidth="md"
|
||||
open
|
||||
flexColumnBody
|
||||
>
|
||||
{dialogContent}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginWithPurchaseClaimDialog;
|
257
newIDE/app/src/Profile/PurchaseClaimDialog.js
Normal file
257
newIDE/app/src/Profile/PurchaseClaimDialog.js
Normal file
@@ -0,0 +1,257 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import {
|
||||
claimPurchase,
|
||||
type BundleListingData,
|
||||
} from '../Utils/GDevelopServices/Shop';
|
||||
import Dialog, { DialogPrimaryButton } from '../UI/Dialog';
|
||||
import AuthenticatedUserContext from './AuthenticatedUserContext';
|
||||
import Text from '../UI/Text';
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import Mark from '../UI/CustomSvgIcons/Mark';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import { LineStackLayout, ResponsiveLineStackLayout } from '../UI/Layout';
|
||||
import useAlertDialog from '../UI/Alert/useAlertDialog';
|
||||
import { extractGDevelopApiErrorStatusAndCode } from '../Utils/GDevelopServices/Errors';
|
||||
import { CorsAwareImage } from '../UI/CorsAwareImage';
|
||||
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
|
||||
import RouterContext from '../MainFrame/RouterContext';
|
||||
|
||||
export type ClaimedProductOptions = {|
|
||||
productListingData: BundleListingData, // Add more product types in the future.
|
||||
purchaseId: string,
|
||||
claimableToken: string,
|
||||
|};
|
||||
|
||||
const styles = {
|
||||
previewImage: {
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
display: 'block',
|
||||
objectFit: 'contain',
|
||||
borderRadius: 8,
|
||||
border: '1px solid lightgrey',
|
||||
boxSizing: 'border-box', // Take border in account for sizing to avoid cumulative layout shift.
|
||||
// Prevent cumulative layout shift by enforcing
|
||||
// the 16:9 ratio.
|
||||
aspectRatio: '16 / 9',
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
position: 'relative',
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
claimedProductOptions: ClaimedProductOptions,
|
||||
onClose: () => void,
|
||||
|};
|
||||
|
||||
const PurchaseClaimDialog = ({
|
||||
claimedProductOptions: { productListingData, purchaseId, claimableToken },
|
||||
onClose,
|
||||
}: Props) => {
|
||||
const {
|
||||
getAuthorizationHeader,
|
||||
profile,
|
||||
onPurchaseSuccessful,
|
||||
onRefreshBundlePurchases,
|
||||
} = React.useContext(AuthenticatedUserContext);
|
||||
const [isActivating, setIsActivating] = React.useState(false);
|
||||
const [isPurchaseActivated, setIsPurchaseActivated] = React.useState(false);
|
||||
const { showAlert } = useAlertDialog();
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const { navigateToRoute } = React.useContext(RouterContext);
|
||||
const shouldClaimOnOpen = React.useRef(!profile);
|
||||
|
||||
const activatePurchase = React.useCallback(
|
||||
async () => {
|
||||
if (!profile || isActivating || isPurchaseActivated) return;
|
||||
|
||||
setIsActivating(true);
|
||||
let updatedPurchase = null;
|
||||
try {
|
||||
updatedPurchase = await claimPurchase({
|
||||
getAuthorizationHeader,
|
||||
purchaseId,
|
||||
claimableToken,
|
||||
userId: profile.id,
|
||||
});
|
||||
} catch (error) {
|
||||
const extractedStatusAndCode = extractGDevelopApiErrorStatusAndCode(
|
||||
error
|
||||
);
|
||||
let message = t`An error occurred while activating your purchase. Please contact support if the problem persists.`;
|
||||
if (
|
||||
extractedStatusAndCode &&
|
||||
extractedStatusAndCode.status === 400 &&
|
||||
extractedStatusAndCode.code === 'purchase/already-claimed'
|
||||
) {
|
||||
message = t`This purchase has already been activated.`;
|
||||
} else if (
|
||||
extractedStatusAndCode &&
|
||||
extractedStatusAndCode.status === 403
|
||||
) {
|
||||
if (extractedStatusAndCode.code === 'purchase/cannot-claim') {
|
||||
message = t`This purchase cannot be claimed.`;
|
||||
} else if (extractedStatusAndCode.code === 'purchase/invalid-token') {
|
||||
message = t`The token used to claim this purchase is invalid.`;
|
||||
}
|
||||
} else if (
|
||||
extractedStatusAndCode &&
|
||||
extractedStatusAndCode.status === 404
|
||||
) {
|
||||
message = t`This purchase could not be found. Please contact support for more information.`;
|
||||
} else if (
|
||||
extractedStatusAndCode &&
|
||||
extractedStatusAndCode.status === 409
|
||||
) {
|
||||
if (extractedStatusAndCode.code === 'purchase/already-owned') {
|
||||
message = t`This account already owns this product, you cannot activate it again.`;
|
||||
}
|
||||
} else if (
|
||||
extractedStatusAndCode &&
|
||||
extractedStatusAndCode.status === 500
|
||||
) {
|
||||
message = t`The server is currently unavailable. Please try again later.`;
|
||||
}
|
||||
showAlert({
|
||||
title: t`Could not activate purchase`,
|
||||
message,
|
||||
});
|
||||
setIsActivating(false);
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (updatedPurchase.productType === 'BUNDLE') {
|
||||
await onRefreshBundlePurchases();
|
||||
navigateToRoute('learn', {
|
||||
bundle: updatedPurchase.productId,
|
||||
});
|
||||
}
|
||||
onPurchaseSuccessful();
|
||||
} finally {
|
||||
setIsActivating(false);
|
||||
setIsPurchaseActivated(true);
|
||||
}
|
||||
},
|
||||
[
|
||||
profile,
|
||||
isActivating,
|
||||
getAuthorizationHeader,
|
||||
purchaseId,
|
||||
claimableToken,
|
||||
showAlert,
|
||||
onRefreshBundlePurchases,
|
||||
onPurchaseSuccessful,
|
||||
onClose,
|
||||
isPurchaseActivated,
|
||||
navigateToRoute,
|
||||
]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (profile && shouldClaimOnOpen.current) {
|
||||
// The user has just logged in or signed up, so we can proceed with the
|
||||
// claim.
|
||||
activatePurchase();
|
||||
shouldClaimOnOpen.current = false; // Ensure we don't try to claim again if profile changes.
|
||||
}
|
||||
},
|
||||
[profile, activatePurchase]
|
||||
);
|
||||
|
||||
if (!profile) {
|
||||
// The dialog is meant to be displayed as soon as the user is authenticated.
|
||||
// If they're not, they can be in the middle of logging in or signing up, so
|
||||
// we don't show anything, until they're logged in.
|
||||
return null;
|
||||
}
|
||||
|
||||
const productType = productListingData
|
||||
? productListingData.productType
|
||||
: 'product';
|
||||
const productName = productListingData ? productListingData.name : '';
|
||||
|
||||
const dialogContents = isPurchaseActivated
|
||||
? {
|
||||
subtitle: <Trans>Your {productType} has been activated!</Trans>,
|
||||
content: (
|
||||
<Line justifyContent="center" alignItems="center" noMargin>
|
||||
<Text>
|
||||
<Trans>You can now go back to use your new product.</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
),
|
||||
}
|
||||
: {
|
||||
subtitle: (
|
||||
<Trans>
|
||||
The {productType} {productName} will be linked to your account{' '}
|
||||
{profile.email}
|
||||
</Trans>
|
||||
),
|
||||
content: null,
|
||||
};
|
||||
|
||||
const dialogActions = [
|
||||
<FlatButton
|
||||
key="cancel"
|
||||
label={isPurchaseActivated ? <Trans>Close</Trans> : <Trans>Cancel</Trans>}
|
||||
onClick={onClose}
|
||||
disabled={isActivating}
|
||||
/>,
|
||||
!isPurchaseActivated ? (
|
||||
<DialogPrimaryButton
|
||||
key="activate"
|
||||
primary
|
||||
label={
|
||||
isActivating ? <Trans>Activating...</Trans> : <Trans>Activate</Trans>
|
||||
}
|
||||
onClick={activatePurchase}
|
||||
disabled={isActivating}
|
||||
/>
|
||||
) : null,
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={<Trans>Activate {productName}</Trans>}
|
||||
maxWidth="md"
|
||||
open
|
||||
onRequestClose={onClose}
|
||||
actions={dialogActions}
|
||||
onApply={isPurchaseActivated ? onClose : activatePurchase}
|
||||
cannotBeDismissed // Prevent the user from continuing by clicking outside.
|
||||
flexColumnBody
|
||||
>
|
||||
<ResponsiveLineStackLayout noMargin alignItems="center">
|
||||
{productListingData && (
|
||||
<CorsAwareImage
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
background: gdevelopTheme.paper.backgroundColor.light,
|
||||
}}
|
||||
src={productListingData.thumbnailUrls[0]}
|
||||
alt={`Preview image of product ${productListingData.name}`}
|
||||
/>
|
||||
)}
|
||||
<Column>
|
||||
<LineStackLayout
|
||||
justifyContent="flex-start"
|
||||
alignItems="center"
|
||||
noMargin
|
||||
>
|
||||
{isPurchaseActivated && <Mark />}
|
||||
<Text size="sub-title">{dialogContents.subtitle}</Text>
|
||||
</LineStackLayout>
|
||||
{dialogContents.content}
|
||||
</Column>
|
||||
</ResponsiveLineStackLayout>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default PurchaseClaimDialog;
|
@@ -844,7 +844,9 @@ export default function SubscriptionDialog({
|
||||
key="create-account"
|
||||
label={<Trans>Create my account</Trans>}
|
||||
primary
|
||||
onClick={authenticatedUser.onOpenCreateAccountDialog}
|
||||
onClick={() =>
|
||||
authenticatedUser.onOpenCreateAccountDialog()
|
||||
}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
|
@@ -278,28 +278,28 @@ export const QuickPublish = ({
|
||||
primary
|
||||
icon={<Google />}
|
||||
label={<Trans>Google</Trans>}
|
||||
onClick={onOpenCreateAccountDialog}
|
||||
onClick={() => onOpenCreateAccountDialog()}
|
||||
fullWidth
|
||||
/>
|
||||
<RaisedButton
|
||||
primary
|
||||
icon={<GitHub />}
|
||||
label={<Trans>Github</Trans>}
|
||||
onClick={onOpenCreateAccountDialog}
|
||||
onClick={() => onOpenCreateAccountDialog()}
|
||||
fullWidth
|
||||
/>
|
||||
<RaisedButton
|
||||
primary
|
||||
icon={<Apple />}
|
||||
label={<Trans>Apple</Trans>}
|
||||
onClick={onOpenCreateAccountDialog}
|
||||
onClick={() => onOpenCreateAccountDialog()}
|
||||
fullWidth
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
<FlatButton
|
||||
primary
|
||||
label={<Trans>Use your email</Trans>}
|
||||
onClick={onOpenCreateAccountDialog}
|
||||
onClick={() => onOpenCreateAccountDialog()}
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
</div>
|
||||
|
@@ -70,7 +70,8 @@ type ErrorBoundaryScope =
|
||||
| 'box-search-result'
|
||||
| 'list-search-result'
|
||||
| 'custom-object-editor-canvas'
|
||||
| 'project-file-list';
|
||||
| 'project-file-list'
|
||||
| 'standalone';
|
||||
|
||||
export const getEditorErrorBoundaryProps = (
|
||||
editorKey: string
|
||||
|
@@ -104,7 +104,7 @@ const UserChip = ({ onOpenProfile }: Props) => {
|
||||
<Trans>Sign up</Trans>
|
||||
</span>
|
||||
}
|
||||
onClick={onOpenCreateAccountDialog}
|
||||
onClick={() => onOpenCreateAccountDialog()}
|
||||
primary
|
||||
/>
|
||||
</LineStackLayout>
|
||||
|
@@ -317,6 +317,22 @@ export const listListedBundles = async (): Promise<
|
||||
return bundles;
|
||||
};
|
||||
|
||||
export const getListedBundle = async ({
|
||||
bundleId,
|
||||
visibility,
|
||||
}: {|
|
||||
bundleId: string,
|
||||
visibility?: 'all',
|
||||
|}): Promise<?BundleListingData> => {
|
||||
const response = await client.get(`/bundle/${bundleId}`, {
|
||||
params: {
|
||||
visibility,
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const listSellerAssetPacks = async ({
|
||||
sellerId,
|
||||
}: {|
|
||||
@@ -476,15 +492,17 @@ export const getPurchaseCheckoutUrl = ({
|
||||
|
||||
export const getStripeCheckoutUrl = ({
|
||||
userId,
|
||||
userUuid,
|
||||
productId,
|
||||
priceName,
|
||||
userEmail,
|
||||
password,
|
||||
}: {|
|
||||
userId: string,
|
||||
userId?: string,
|
||||
userUuid?: string,
|
||||
productId: string,
|
||||
priceName: string,
|
||||
userEmail: string,
|
||||
userEmail?: string,
|
||||
password?: string,
|
||||
|}) => {
|
||||
const url = new URL(
|
||||
@@ -493,8 +511,9 @@ export const getStripeCheckoutUrl = ({
|
||||
|
||||
url.searchParams.set('productId', productId);
|
||||
url.searchParams.set('priceName', priceName);
|
||||
url.searchParams.set('userId', userId);
|
||||
url.searchParams.set('customerEmail', userEmail);
|
||||
if (userUuid) url.searchParams.set('userUuid', userUuid);
|
||||
if (userId) url.searchParams.set('userId', userId);
|
||||
if (userEmail) url.searchParams.set('customerEmail', userEmail);
|
||||
if (password) url.searchParams.set('password', password);
|
||||
|
||||
return url.toString();
|
||||
@@ -730,3 +749,29 @@ export const redeemPrivateAssetPack = async ({
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const claimPurchase = async ({
|
||||
getAuthorizationHeader,
|
||||
userId,
|
||||
purchaseId,
|
||||
claimableToken,
|
||||
}: {|
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
userId: string,
|
||||
purchaseId: string,
|
||||
claimableToken: string,
|
||||
|}): Promise<Purchase> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const result = await client.post(
|
||||
`/purchase/${purchaseId}/action/claim`,
|
||||
{ claimableToken },
|
||||
{
|
||||
params: {
|
||||
userId,
|
||||
},
|
||||
headers: { Authorization: authorizationHeader },
|
||||
}
|
||||
);
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
@@ -4,6 +4,10 @@ import RouterContext from '../MainFrame/RouterContext';
|
||||
import { SubscriptionSuggestionContext } from '../Profile/Subscription/SubscriptionSuggestionContext';
|
||||
import { FLING_GAME_IN_APP_TUTORIAL_ID } from './GDevelopServices/InAppTutorial';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import { t } from '@lingui/macro';
|
||||
import { getListedBundle } from './GDevelopServices/Shop';
|
||||
import useAlertDialog from '../UI/Alert/useAlertDialog';
|
||||
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
|
||||
|
||||
type Props = {|
|
||||
openInAppTutorialDialog: (tutorialId: string) => void,
|
||||
@@ -13,6 +17,7 @@ type Props = {|
|
||||
aiRequestId: string | null,
|
||||
paneIdentifier: 'left' | 'center' | 'right' | null,
|
||||
|}) => void,
|
||||
openStandaloneDialog: () => void,
|
||||
|};
|
||||
|
||||
/**
|
||||
@@ -23,6 +28,7 @@ const useOpenInitialDialog = ({
|
||||
openInAppTutorialDialog,
|
||||
openProfileDialog,
|
||||
openAskAi,
|
||||
openStandaloneDialog,
|
||||
}: Props) => {
|
||||
const { routeArguments, removeRouteArguments } = React.useContext(
|
||||
RouterContext
|
||||
@@ -32,75 +38,143 @@ const useOpenInitialDialog = ({
|
||||
);
|
||||
const {
|
||||
onOpenCreateAccountDialog,
|
||||
onOpenCreateAccountWithPurchaseClaimDialog,
|
||||
onOpenPurchaseClaimDialog,
|
||||
onOpenLoginDialog,
|
||||
authenticated,
|
||||
loginState,
|
||||
} = React.useContext(AuthenticatedUserContext);
|
||||
const { showAlert } = useAlertDialog();
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
switch (routeArguments['initial-dialog']) {
|
||||
case 'subscription':
|
||||
let recommendedPlanId =
|
||||
routeArguments['recommended-plan-id'] || 'gdevelop_silver';
|
||||
async function openCorrespondingDialog() {
|
||||
switch (routeArguments['initial-dialog']) {
|
||||
case 'subscription':
|
||||
const recommendedPlanId =
|
||||
routeArguments['recommended-plan-id'] || 'gdevelop_silver';
|
||||
|
||||
openSubscriptionDialog({
|
||||
analyticsMetadata: {
|
||||
reason: 'Landing dialog at opening',
|
||||
recommendedPlanId,
|
||||
placementId: 'opening-from-link',
|
||||
},
|
||||
});
|
||||
removeRouteArguments(['initial-dialog', 'recommended-plan-id']);
|
||||
break;
|
||||
case 'signup':
|
||||
// Add timeout to give time to the app to sign in with Firebase
|
||||
// to make sure the most relevant dialog is opened.
|
||||
const signupTimeoutId = setTimeout(() => {
|
||||
if (authenticated) {
|
||||
openProfileDialog();
|
||||
} else {
|
||||
onOpenCreateAccountDialog();
|
||||
openSubscriptionDialog({
|
||||
analyticsMetadata: {
|
||||
reason: 'Landing dialog at opening',
|
||||
recommendedPlanId,
|
||||
placementId: 'opening-from-link',
|
||||
},
|
||||
});
|
||||
removeRouteArguments(['initial-dialog', 'recommended-plan-id']);
|
||||
break;
|
||||
case 'signup':
|
||||
if (loginState !== 'done') {
|
||||
// Wait for the login state to be done (user is authenticated or not) before opening the dialog.
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const claimableToken = routeArguments['claimable-token'];
|
||||
const purchaseId = routeArguments['purchase-id'];
|
||||
if (purchaseId && claimableToken) {
|
||||
const bundleId = routeArguments['bundle'];
|
||||
let claimedProduct = null;
|
||||
if (bundleId) {
|
||||
const listedBundle = await getListedBundle({
|
||||
bundleId,
|
||||
visibility: 'all',
|
||||
});
|
||||
claimedProduct = listedBundle;
|
||||
}
|
||||
|
||||
if (!claimedProduct) {
|
||||
console.error(
|
||||
`The bundle with id ${bundleId} does not exist. Cannot claim.`
|
||||
);
|
||||
await showAlert({
|
||||
title: t`Unknown bundle`,
|
||||
message: t`The bundle you are trying to claim does not exist anymore. Please contact support if you think this is an error.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const claimedProductOptions = {
|
||||
productListingData: claimedProduct,
|
||||
purchaseId,
|
||||
claimableToken,
|
||||
};
|
||||
|
||||
if (authenticated) {
|
||||
onOpenPurchaseClaimDialog(claimedProductOptions);
|
||||
} else {
|
||||
onOpenCreateAccountWithPurchaseClaimDialog(
|
||||
claimedProductOptions
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (authenticated) {
|
||||
openProfileDialog();
|
||||
} else {
|
||||
onOpenCreateAccountDialog();
|
||||
}
|
||||
} finally {
|
||||
removeRouteArguments([
|
||||
'initial-dialog',
|
||||
'purchase-id',
|
||||
'claimable-token',
|
||||
'bundle',
|
||||
]);
|
||||
}
|
||||
break;
|
||||
case 'onboarding':
|
||||
case 'guided-lesson':
|
||||
const tutorialId = routeArguments['tutorial-id'];
|
||||
if (tutorialId) {
|
||||
openInAppTutorialDialog(tutorialId);
|
||||
} else {
|
||||
// backward compatibility, open the fling game tutorial.
|
||||
openInAppTutorialDialog(FLING_GAME_IN_APP_TUTORIAL_ID);
|
||||
}
|
||||
removeRouteArguments(['initial-dialog', 'tutorial-id']);
|
||||
break;
|
||||
case 'games-dashboard':
|
||||
// Do nothing as it should open the games dashboard on the homepage
|
||||
// in the manage tab. So the homepage handles the route arguments itself.
|
||||
break;
|
||||
case 'ask-ai':
|
||||
openAskAi({
|
||||
mode: 'agent',
|
||||
aiRequestId: null,
|
||||
paneIdentifier: 'center',
|
||||
});
|
||||
removeRouteArguments(['initial-dialog']);
|
||||
}, 2000);
|
||||
return () => clearTimeout(signupTimeoutId);
|
||||
case 'onboarding':
|
||||
case 'guided-lesson':
|
||||
const tutorialId = routeArguments['tutorial-id'];
|
||||
if (tutorialId) {
|
||||
openInAppTutorialDialog(tutorialId);
|
||||
} else {
|
||||
// backward compatibility, open the fling game tutorial.
|
||||
openInAppTutorialDialog(FLING_GAME_IN_APP_TUTORIAL_ID);
|
||||
}
|
||||
removeRouteArguments(['initial-dialog', 'tutorial-id']);
|
||||
break;
|
||||
case 'games-dashboard':
|
||||
// Do nothing as it should open the games dashboard on the homepage
|
||||
// in the manage tab. So the homepage handles the route arguments itself.
|
||||
break;
|
||||
case 'ask-ai':
|
||||
openAskAi({
|
||||
mode: 'agent',
|
||||
aiRequestId: null,
|
||||
paneIdentifier: 'center',
|
||||
});
|
||||
removeRouteArguments(['initial-dialog']);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
break;
|
||||
case 'standalone':
|
||||
openStandaloneDialog();
|
||||
removeRouteArguments(['initial-dialog']);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
openCorrespondingDialog();
|
||||
},
|
||||
[
|
||||
routeArguments,
|
||||
openInAppTutorialDialog,
|
||||
openProfileDialog,
|
||||
openStandaloneDialog,
|
||||
removeRouteArguments,
|
||||
openSubscriptionDialog,
|
||||
authenticated,
|
||||
onOpenCreateAccountDialog,
|
||||
onOpenCreateAccountWithPurchaseClaimDialog,
|
||||
onOpenLoginDialog,
|
||||
openAskAi,
|
||||
loginState,
|
||||
showAlert,
|
||||
gdevelopTheme,
|
||||
onOpenPurchaseClaimDialog,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
@@ -351,7 +351,14 @@ export default class Window {
|
||||
}
|
||||
}
|
||||
|
||||
static openExternalURL(url: string) {
|
||||
static openExternalURL(
|
||||
url: string,
|
||||
{
|
||||
shouldOpenInSameTabIfPossible,
|
||||
}: {|
|
||||
shouldOpenInSameTabIfPossible?: boolean,
|
||||
|} = {}
|
||||
) {
|
||||
if (!url) return;
|
||||
|
||||
if (electron) {
|
||||
@@ -359,7 +366,7 @@ export default class Window {
|
||||
return;
|
||||
}
|
||||
|
||||
window.open(url, '_blank');
|
||||
window.open(url, shouldOpenInSameTabIfPossible ? '_self' : '_blank');
|
||||
}
|
||||
|
||||
static getOrientation(): 'portrait' | 'landscape' {
|
||||
|
@@ -29,6 +29,7 @@ import {
|
||||
type AssetShortHeader,
|
||||
type Asset,
|
||||
type PublicAssetPacks,
|
||||
type Bundle,
|
||||
} from '../../Utils/GDevelopServices/Asset';
|
||||
import { formatISO, subDays } from 'date-fns';
|
||||
import { type Comment } from '../../Utils/GDevelopServices/Play';
|
||||
@@ -36,7 +37,11 @@ import {
|
||||
type Announcement,
|
||||
type Promotion,
|
||||
} from '../../Utils/GDevelopServices/Announcement';
|
||||
import { type PrivateGameTemplateListingData } from '../../Utils/GDevelopServices/Shop';
|
||||
import {
|
||||
type PrivateGameTemplateListingData,
|
||||
type PrivateAssetPackListingData,
|
||||
type BundleListingData,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import { fakeAchievements } from './FakeAchievements';
|
||||
import { type FileMetadataAndStorageProviderName } from '../../ProjectsStorage';
|
||||
import {
|
||||
@@ -894,9 +899,12 @@ export const defaultAuthenticatedUserWithNoSubscription: AuthenticatedUser = {
|
||||
onResetPassword: async () => {},
|
||||
onEditProfile: async () => {},
|
||||
onOpenLoginDialog: () => {},
|
||||
onOpenLoginWithPurchaseClaimDialog: () => {},
|
||||
onOpenEditProfileDialog: () => {},
|
||||
onOpenChangeEmailDialog: () => {},
|
||||
onOpenCreateAccountDialog: () => {},
|
||||
onOpenCreateAccountWithPurchaseClaimDialog: () => {},
|
||||
onOpenPurchaseClaimDialog: () => {},
|
||||
onOpenEmailVerificationDialog: () => {},
|
||||
onBadgesChanged: async () => {},
|
||||
onCloudProjectsChanged: async () => {},
|
||||
@@ -2190,8 +2198,18 @@ export const fakePrivateGameTemplateListingData: PrivateGameTemplateListingData
|
||||
categories: ['adventure'],
|
||||
updatedAt: '2020-01-01',
|
||||
createdAt: '2020-01-01',
|
||||
thumbnailUrls: [],
|
||||
prices: [],
|
||||
thumbnailUrls: [
|
||||
'https://resources.gdevelop-app.com/staging/private-assets/French Food/thumbnail1.png',
|
||||
],
|
||||
prices: [
|
||||
{
|
||||
name: 'commercial_USD',
|
||||
value: 499,
|
||||
currency: 'USD',
|
||||
usageType: 'commercial',
|
||||
stripePriceId: 'price_1JHhXYZfakeStripePriceId',
|
||||
},
|
||||
],
|
||||
creditPrices: [],
|
||||
appStoreProductId: 'fake-app-store-product-id',
|
||||
includedListableProductIds: [],
|
||||
@@ -2336,6 +2354,161 @@ export const fakeAssetPacks: PublicAssetPacks = {
|
||||
],
|
||||
};
|
||||
|
||||
export const fakePrivateAssetPack1ListingData: PrivateAssetPackListingData = {
|
||||
id: '56a50a9e-57ef-4d1d-a3f2-c918d593a6e2',
|
||||
sellerId: 'tVUYpNMz1AfsbzJtxUEpPTuu4Mn1',
|
||||
isSellerGDevelop: false,
|
||||
productType: 'ASSET_PACK',
|
||||
thumbnailUrls: [
|
||||
'https://resources.gdevelop-app.com/staging/private-assets/French Food/thumbnail1.png',
|
||||
],
|
||||
updatedAt: '2022-09-14T12:43:51.329Z',
|
||||
createdAt: '2022-09-14T12:43:51.329Z',
|
||||
listing: 'ASSET_PACK',
|
||||
description: '5 assets',
|
||||
name: 'French Food',
|
||||
categories: ['props'],
|
||||
prices: [
|
||||
{
|
||||
value: 1500,
|
||||
name: 'commercial_USD',
|
||||
stripePriceId: 'stripePriceId',
|
||||
currency: 'USD',
|
||||
usageType: 'commercial',
|
||||
},
|
||||
],
|
||||
creditPrices: [
|
||||
{
|
||||
amount: 1500,
|
||||
usageType: 'commercial',
|
||||
},
|
||||
],
|
||||
appStoreProductId: null,
|
||||
sellerStripeAccountId: 'sellerStripeProductId',
|
||||
stripeProductId: 'stripeProductId',
|
||||
};
|
||||
|
||||
export const fakePrivateAssetPack2ListingData: PrivateAssetPackListingData = {
|
||||
id: '56a50a9e-57ef-4d1d-a3f2-c918d568ef234',
|
||||
sellerId: 'tVUYpNMz1AfsbzJtxUEpPTuu4Mn1',
|
||||
isSellerGDevelop: false,
|
||||
productType: 'ASSET_PACK',
|
||||
thumbnailUrls: [
|
||||
'https://resources.gdevelop-app.com/staging/private-assets/French Sounds/thumbnail0.png',
|
||||
],
|
||||
updatedAt: '2022-09-14T12:43:51.329Z',
|
||||
createdAt: '2022-09-14T12:43:51.329Z',
|
||||
listing: 'ASSET_PACK',
|
||||
description: '8 assets',
|
||||
name: 'French Sounds',
|
||||
categories: ['sounds'],
|
||||
prices: [
|
||||
{
|
||||
value: 1000,
|
||||
usageType: 'commercial',
|
||||
stripePriceId: 'stripePriceId',
|
||||
currency: 'USD',
|
||||
name: 'commercial_USD',
|
||||
},
|
||||
],
|
||||
creditPrices: [
|
||||
{
|
||||
amount: 1000,
|
||||
usageType: 'commercial',
|
||||
},
|
||||
],
|
||||
appStoreProductId: 'fake.product.id',
|
||||
sellerStripeAccountId: 'sellerStripeProductId',
|
||||
stripeProductId: 'stripeProductId',
|
||||
};
|
||||
|
||||
export const fakeBundleListingData: BundleListingData = {
|
||||
id: 'bundle-123',
|
||||
sellerId: 'tVUYpNMz1AfsbzJtxUEpPTuu4Mn1',
|
||||
isSellerGDevelop: false,
|
||||
productType: 'BUNDLE',
|
||||
thumbnailUrls: [
|
||||
'https://resources.gdevelop-app.com/staging/private-assets/French Food/thumbnail1.png',
|
||||
'https://resources.gdevelop-app.com/staging/private-assets/French Sounds/thumbnail0.png',
|
||||
],
|
||||
updatedAt: '2022-09-14T12:43:51.329Z',
|
||||
createdAt: '2022-09-14T12:43:51.329Z',
|
||||
listing: 'BUNDLE',
|
||||
description: 'The ultimate French bundle with food and sounds',
|
||||
name: 'French Complete Bundle',
|
||||
categories: ['starter'],
|
||||
prices: [
|
||||
{
|
||||
value: 2000,
|
||||
name: 'default_USD',
|
||||
stripePriceId: 'stripePriceId',
|
||||
currency: 'USD',
|
||||
usageType: 'default',
|
||||
},
|
||||
],
|
||||
appStoreProductId: null,
|
||||
sellerStripeAccountId: 'sellerStripeProductId',
|
||||
stripeProductId: 'stripeProductId',
|
||||
includedListableProducts: [
|
||||
{
|
||||
productId: fakePrivateAssetPack1ListingData.id,
|
||||
productType: 'ASSET_PACK',
|
||||
usageType: 'commercial',
|
||||
},
|
||||
{
|
||||
productId: fakePrivateAssetPack2ListingData.id,
|
||||
productType: 'ASSET_PACK',
|
||||
usageType: 'commercial',
|
||||
},
|
||||
{
|
||||
productId: fakePrivateGameTemplateListingData.id,
|
||||
productType: 'GAME_TEMPLATE',
|
||||
usageType: 'commercial',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const fakeBundle: Bundle = {
|
||||
id: 'bundle-123',
|
||||
name: 'French Complete Bundle',
|
||||
nameByLocale: {
|
||||
en: 'French Complete Bundle',
|
||||
},
|
||||
categories: ['starter'],
|
||||
previewImageUrls: [
|
||||
'https://resources.gdevelop-app.com/assets/Packs/gdevelop platformer.png',
|
||||
'https://resources.gdevelop-app.com/assets/Packs/space shooter.png',
|
||||
'https://resources.gdevelop-app.com/assets/Packs/particles emitter.png',
|
||||
],
|
||||
updatedAt: '2022-09-15T08:17:59.977Z',
|
||||
createdAt: '2022-09-14T12:27:27.173Z',
|
||||
tag: 'french bundle',
|
||||
longDescription:
|
||||
'This is the best bundle about french food and sounds. It includes everything you need to create a French-themed game.',
|
||||
longDescriptionByLocale: {
|
||||
en:
|
||||
'This is the best bundle about french food and sounds. It includes everything you need to create a French-themed game.',
|
||||
},
|
||||
includedProducts: [
|
||||
{
|
||||
productId: fakePrivateAssetPack1ListingData.id,
|
||||
productType: 'ASSET_PACK',
|
||||
usageType: 'commercial',
|
||||
},
|
||||
{
|
||||
productId: fakePrivateAssetPack2ListingData.id,
|
||||
productType: 'ASSET_PACK',
|
||||
usageType: 'commercial',
|
||||
},
|
||||
{
|
||||
productId: fakePrivateGameTemplateListingData.id,
|
||||
productType: 'GAME_TEMPLATE',
|
||||
usageType: 'commercial',
|
||||
},
|
||||
],
|
||||
includedRedemptionCodes: [],
|
||||
};
|
||||
|
||||
export const commentUnprocessed: Comment = {
|
||||
id: 'comment-unprocessed-id',
|
||||
type: 'FEEDBACK',
|
||||
|
@@ -0,0 +1,233 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import paperDecorator from '../../../PaperDecorator';
|
||||
import BundleInformationPage from '../../../../AssetStore/Bundles/BundleInformationPage';
|
||||
import {
|
||||
client as assetApiAxiosClient,
|
||||
type Bundle,
|
||||
type Course,
|
||||
} from '../../../../Utils/GDevelopServices/Asset';
|
||||
import { client as userApiAxiosClient } from '../../../../Utils/GDevelopServices/User';
|
||||
import { client as shopApiAxiosClient } from '../../../../Utils/GDevelopServices/Shop';
|
||||
import { type BundleListingData } from '../../../../Utils/GDevelopServices/Shop';
|
||||
import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserContext';
|
||||
import {
|
||||
fakeAssetPackLicenses,
|
||||
fakeBundle,
|
||||
fakeBundleListingData,
|
||||
fakePrivateAssetPack1ListingData,
|
||||
fakePrivateAssetPack2ListingData,
|
||||
fakePrivateGameTemplateListingData,
|
||||
fakeSilverAuthenticatedUserWithCloudProjects,
|
||||
} from '../../../../fixtures/GDevelopServicesTestData';
|
||||
import {
|
||||
BundleStoreContext,
|
||||
initialBundleStoreState,
|
||||
} from '../../../../AssetStore/Bundles/BundleStoreContext';
|
||||
import {
|
||||
initialPrivateGameTemplateStoreState,
|
||||
PrivateGameTemplateStoreContext,
|
||||
} from '../../../../AssetStore/PrivateGameTemplates/PrivateGameTemplateStoreContext';
|
||||
import {
|
||||
AssetStoreContext,
|
||||
initialAssetStoreState,
|
||||
} from '../../../../AssetStore/AssetStoreContext';
|
||||
import { ProductLicenseStoreStateProvider } from '../../../../AssetStore/ProductLicense/ProductLicenseStoreContext';
|
||||
import { SubscriptionSuggestionProvider } from '../../../../Profile/Subscription/SubscriptionSuggestionContext';
|
||||
import { type AuthenticatedUser } from '../../../../Profile/AuthenticatedUserContext';
|
||||
import CourseStoreContext, {
|
||||
initialCourseStoreState,
|
||||
} from '../../../../Course/CourseStoreContext';
|
||||
import {
|
||||
CreditsPackageStoreContext,
|
||||
initialCreditsPackageStoreState,
|
||||
} from '../../../../AssetStore/CreditsPackages/CreditsPackageStoreContext';
|
||||
|
||||
export default {
|
||||
title: 'AssetStore/AssetStore/BundleInformationPage',
|
||||
component: BundleInformationPage,
|
||||
decorators: [paperDecorator],
|
||||
};
|
||||
|
||||
const sellerPublicProfile = {
|
||||
id: 'tVUYpNMz1AfsbzJtxUEpPTuu4Mn1',
|
||||
username: 'CreatorUserName',
|
||||
description: 'I create bundles for GDevelop.',
|
||||
};
|
||||
|
||||
const allBundleListingData = [fakeBundleListingData];
|
||||
const allPrivateAssetPackListingData = [
|
||||
fakePrivateAssetPack1ListingData,
|
||||
fakePrivateAssetPack2ListingData,
|
||||
];
|
||||
|
||||
const allPrivateGameTemplateListingData = [fakePrivateGameTemplateListingData];
|
||||
|
||||
const mockCourses: Array<Course> = [];
|
||||
|
||||
const BundleInformationPageStory = ({
|
||||
bundleListingDataToDisplay,
|
||||
receivedBundles = [],
|
||||
authenticatedUser = fakeSilverAuthenticatedUserWithCloudProjects,
|
||||
delayResponse = 0,
|
||||
errorCode,
|
||||
errorMessage,
|
||||
}: {
|
||||
bundleListingDataToDisplay: BundleListingData,
|
||||
authenticatedUser?: AuthenticatedUser,
|
||||
receivedBundles?: Array<Bundle>,
|
||||
delayResponse?: number,
|
||||
errorCode?: number,
|
||||
errorMessage?: string,
|
||||
}) => {
|
||||
const userServiceMock = new MockAdapter(userApiAxiosClient, {
|
||||
delayResponse,
|
||||
});
|
||||
userServiceMock
|
||||
.onGet(`/user-public-profile/${bundleListingDataToDisplay.sellerId}`)
|
||||
.reply(200, sellerPublicProfile)
|
||||
.onGet(`/user/${bundleListingDataToDisplay.sellerId}/badge`)
|
||||
.reply(200, [])
|
||||
.onGet(`/achievement`)
|
||||
.reply(200, []);
|
||||
|
||||
const assetServiceMock = new MockAdapter(assetApiAxiosClient, {
|
||||
delayResponse,
|
||||
});
|
||||
assetServiceMock
|
||||
.onGet(`/bundle/${bundleListingDataToDisplay.id}`)
|
||||
.reply(errorCode || 200, errorCode ? errorMessage || null : fakeBundle)
|
||||
.onAny()
|
||||
.reply(config => {
|
||||
console.error(`Unexpected call to ${config.url} (${config.method})`);
|
||||
return [504, null];
|
||||
});
|
||||
|
||||
const shopServiceMock = new MockAdapter(shopApiAxiosClient, {
|
||||
delayResponse,
|
||||
});
|
||||
shopServiceMock
|
||||
.onGet('/product-license')
|
||||
.reply(200, fakeAssetPackLicenses)
|
||||
.onPost(`/product/${bundleListingDataToDisplay.id}/action/redeem`)
|
||||
.reply(config => {
|
||||
action('Claim bundle')();
|
||||
return [200, 'OK'];
|
||||
})
|
||||
.onAny()
|
||||
.reply(config => {
|
||||
console.error(`Unexpected call to ${config.url} (${config.method})`);
|
||||
return [504, null];
|
||||
});
|
||||
|
||||
return (
|
||||
<BundleStoreContext.Provider
|
||||
value={{
|
||||
...initialBundleStoreState,
|
||||
bundleListingDatas: allBundleListingData,
|
||||
}}
|
||||
>
|
||||
<PrivateGameTemplateStoreContext.Provider
|
||||
value={{
|
||||
...initialPrivateGameTemplateStoreState,
|
||||
privateGameTemplateListingDatas: allPrivateGameTemplateListingData,
|
||||
}}
|
||||
>
|
||||
<AssetStoreContext.Provider
|
||||
value={{
|
||||
...initialAssetStoreState,
|
||||
privateAssetPackListingDatas: allPrivateAssetPackListingData,
|
||||
}}
|
||||
>
|
||||
<AuthenticatedUserContext.Provider
|
||||
value={{
|
||||
...authenticatedUser,
|
||||
receivedBundles,
|
||||
bundlePurchases: receivedBundles.map(receivedBundle => ({
|
||||
id: 'purchase-id',
|
||||
productType: 'BUNDLE',
|
||||
usageType: 'commercial',
|
||||
productId: receivedBundle.id,
|
||||
buyerId: authenticatedUser.profile
|
||||
? authenticatedUser.profile.id
|
||||
: 'userId',
|
||||
receiverId: authenticatedUser.profile
|
||||
? authenticatedUser.profile.id
|
||||
: 'userId',
|
||||
createdAt: new Date(1707519600000).toString(),
|
||||
})),
|
||||
}}
|
||||
>
|
||||
<CourseStoreContext.Provider
|
||||
value={{
|
||||
...initialCourseStoreState,
|
||||
listedCourses: [],
|
||||
listedCourseChapters: [],
|
||||
}}
|
||||
>
|
||||
<CreditsPackageStoreContext.Provider
|
||||
value={{
|
||||
...initialCreditsPackageStoreState,
|
||||
creditsPackageListingDatas: [],
|
||||
}}
|
||||
>
|
||||
<SubscriptionSuggestionProvider>
|
||||
<ProductLicenseStoreStateProvider>
|
||||
<BundleInformationPage
|
||||
bundleListingData={bundleListingDataToDisplay}
|
||||
onAssetPackOpen={() => action('open asset pack')()}
|
||||
onGameTemplateOpen={() => action('open game template')()}
|
||||
onBundleOpen={() => action('open bundle')()}
|
||||
onCourseOpen={() => action('open course')()}
|
||||
getSubscriptionPlansWithPricingSystems={() => []}
|
||||
courses={mockCourses}
|
||||
getCourseCompletion={() => null}
|
||||
/>
|
||||
</ProductLicenseStoreStateProvider>
|
||||
</SubscriptionSuggestionProvider>
|
||||
</CreditsPackageStoreContext.Provider>
|
||||
</CourseStoreContext.Provider>
|
||||
</AuthenticatedUserContext.Provider>
|
||||
</AssetStoreContext.Provider>
|
||||
</PrivateGameTemplateStoreContext.Provider>
|
||||
</BundleStoreContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = () => (
|
||||
<BundleInformationPageStory
|
||||
bundleListingDataToDisplay={fakeBundleListingData}
|
||||
/>
|
||||
);
|
||||
|
||||
export const ForAlreadyPurchasedBundle = () => (
|
||||
<BundleInformationPageStory
|
||||
bundleListingDataToDisplay={fakeBundleListingData}
|
||||
receivedBundles={[fakeBundle]}
|
||||
/>
|
||||
);
|
||||
|
||||
export const Loading = () => (
|
||||
<BundleInformationPageStory
|
||||
bundleListingDataToDisplay={fakeBundleListingData}
|
||||
delayResponse={10000}
|
||||
/>
|
||||
);
|
||||
|
||||
export const With404 = () => (
|
||||
<BundleInformationPageStory
|
||||
bundleListingDataToDisplay={fakeBundleListingData}
|
||||
errorCode={404}
|
||||
/>
|
||||
);
|
||||
|
||||
export const WithUnknownError = () => (
|
||||
<BundleInformationPageStory
|
||||
bundleListingDataToDisplay={fakeBundleListingData}
|
||||
errorCode={500}
|
||||
errorMessage={'Internal server error'}
|
||||
/>
|
||||
);
|
@@ -0,0 +1,60 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import paperDecorator from '../../../PaperDecorator';
|
||||
import BundlePurchaseDialog from '../../../../AssetStore/Bundles/BundlePurchaseDialog';
|
||||
import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserContext';
|
||||
import {
|
||||
fakeSilverAuthenticatedUser,
|
||||
fakeNotAuthenticatedUser,
|
||||
fakeBundleListingData,
|
||||
} from '../../../../fixtures/GDevelopServicesTestData';
|
||||
|
||||
export default {
|
||||
title: 'AssetStore/AssetStore/BundlePurchaseDialog',
|
||||
component: BundlePurchaseDialog,
|
||||
decorators: [paperDecorator],
|
||||
parameters: {
|
||||
initialState: {
|
||||
isBuying: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NotLoggedIn = () => {
|
||||
return (
|
||||
<AuthenticatedUserContext.Provider value={fakeNotAuthenticatedUser}>
|
||||
<BundlePurchaseDialog
|
||||
bundleListingData={fakeBundleListingData}
|
||||
usageType="default"
|
||||
onClose={() => action('close')()}
|
||||
/>
|
||||
</AuthenticatedUserContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const LoggedIn = () => {
|
||||
return (
|
||||
<AuthenticatedUserContext.Provider value={fakeSilverAuthenticatedUser}>
|
||||
<BundlePurchaseDialog
|
||||
bundleListingData={fakeBundleListingData}
|
||||
usageType="default"
|
||||
onClose={() => action('close')()}
|
||||
/>
|
||||
</AuthenticatedUserContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const FastCheckout = () => {
|
||||
return (
|
||||
<AuthenticatedUserContext.Provider value={fakeNotAuthenticatedUser}>
|
||||
<BundlePurchaseDialog
|
||||
bundleListingData={fakeBundleListingData}
|
||||
usageType="default"
|
||||
onClose={() => action('close')()}
|
||||
fastCheckout
|
||||
/>
|
||||
</AuthenticatedUserContext.Provider>
|
||||
);
|
||||
};
|
@@ -0,0 +1,60 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import paperDecorator from '../../PaperDecorator';
|
||||
import CreateAccountWithPurchaseClaimDialog from '../../../Profile/CreateAccountWithPurchaseClaimDialog';
|
||||
import { fakeBundleListingData } from '../../../fixtures/GDevelopServicesTestData';
|
||||
|
||||
export default {
|
||||
title: 'Profile/CreateAccountWithPurchaseClaimDialog',
|
||||
component: CreateAccountWithPurchaseClaimDialog,
|
||||
decorators: [paperDecorator],
|
||||
};
|
||||
|
||||
const claimedProductOptions = {
|
||||
productListingData: fakeBundleListingData,
|
||||
purchaseId: 'purchase-123',
|
||||
claimableToken: 'token-123',
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onClose: () => action('onClose')(),
|
||||
onGoToLogin: action('onGoToLogin'),
|
||||
onCreateAccount: action('onCreateAccount'),
|
||||
onLoginWithProvider: action('onLoginWithProvider'),
|
||||
createAccountInProgress: false,
|
||||
error: null,
|
||||
claimedProductOptions,
|
||||
};
|
||||
|
||||
export const Default = () => (
|
||||
<CreateAccountWithPurchaseClaimDialog {...defaultProps} />
|
||||
);
|
||||
|
||||
export const PasswordErrorFromBackend = () => (
|
||||
<CreateAccountWithPurchaseClaimDialog
|
||||
{...defaultProps}
|
||||
error={{ code: 'auth/weak-password' }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const EmailErrorFromBackend = () => (
|
||||
<CreateAccountWithPurchaseClaimDialog
|
||||
{...defaultProps}
|
||||
error={{ code: 'auth/invalid-email' }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const AccountExists = () => (
|
||||
<CreateAccountWithPurchaseClaimDialog
|
||||
{...defaultProps}
|
||||
error={{ code: 'auth/account-exists-with-different-credential' }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const Submitting = () => (
|
||||
<CreateAccountWithPurchaseClaimDialog
|
||||
{...defaultProps}
|
||||
createAccountInProgress
|
||||
/>
|
||||
);
|
@@ -14,7 +14,6 @@ const defaultProps = {
|
||||
onClose: action('onClose'),
|
||||
onForgotPassword: action('onForgotPassword'),
|
||||
onLogin: action('onLogin'),
|
||||
onLogout: action('onLogout'),
|
||||
onLoginWithProvider: action('onLoginWithProvider'),
|
||||
onGoToCreateAccount: action('onGoToCreateAccount'),
|
||||
loginInProgress: false,
|
||||
|
@@ -0,0 +1,62 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import paperDecorator from '../../PaperDecorator';
|
||||
import LoginWithPurchaseClaimDialog from '../../../Profile/LoginWithPurchaseClaimDialog';
|
||||
import { fakeBundleListingData } from '../../../fixtures/GDevelopServicesTestData';
|
||||
|
||||
export default {
|
||||
title: 'Profile/LoginWithPurchaseClaimDialog',
|
||||
component: LoginWithPurchaseClaimDialog,
|
||||
decorators: [paperDecorator],
|
||||
};
|
||||
|
||||
const claimedProductOptions = {
|
||||
productListingData: fakeBundleListingData,
|
||||
purchaseId: 'purchase-123',
|
||||
claimableToken: 'token-123',
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onClose: action('onClose'),
|
||||
onForgotPassword: action('onForgotPassword'),
|
||||
onLogin: action('onLogin'),
|
||||
onLoginWithProvider: action('onLoginWithProvider'),
|
||||
onGoToCreateAccount: action('onGoToCreateAccount'),
|
||||
loginInProgress: false,
|
||||
error: null,
|
||||
claimedProductOptions,
|
||||
};
|
||||
|
||||
export const Default = () => <LoginWithPurchaseClaimDialog {...defaultProps} />;
|
||||
|
||||
export const WeakPasswordErrorFromBackend = () => (
|
||||
<LoginWithPurchaseClaimDialog
|
||||
{...defaultProps}
|
||||
error={{
|
||||
code: 'auth/weak-password',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const InvalidEmailErrorFromBackend = () => (
|
||||
<LoginWithPurchaseClaimDialog
|
||||
{...defaultProps}
|
||||
error={{
|
||||
code: 'auth/invalid-email',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const AccountExistsWithDifferentCredentialErrorFromBackend = () => (
|
||||
<LoginWithPurchaseClaimDialog
|
||||
{...defaultProps}
|
||||
error={{
|
||||
code: 'auth/account-exists-with-different-credential',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const Submitting = () => (
|
||||
<LoginWithPurchaseClaimDialog {...defaultProps} loginInProgress />
|
||||
);
|
@@ -0,0 +1,243 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import paperDecorator from '../../PaperDecorator';
|
||||
import PurchaseClaimDialog from '../../../Profile/PurchaseClaimDialog';
|
||||
import { client as shopApiAxiosClient } from '../../../Utils/GDevelopServices/Shop';
|
||||
import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext';
|
||||
import {
|
||||
fakeSilverAuthenticatedUser,
|
||||
fakeNotAuthenticatedUser,
|
||||
fakeBundleListingData,
|
||||
} from '../../../fixtures/GDevelopServicesTestData';
|
||||
import RouterContext, {
|
||||
initialRouterState,
|
||||
} from '../../../MainFrame/RouterContext';
|
||||
import AlertProvider from '../../../UI/Alert/AlertProvider';
|
||||
|
||||
export default {
|
||||
title: 'PurchaseClaimDialog',
|
||||
component: PurchaseClaimDialog,
|
||||
decorators: [paperDecorator],
|
||||
};
|
||||
|
||||
const purchaseId = 'purchase-id-123';
|
||||
const claimableToken = 'claimable-token-123';
|
||||
|
||||
export const ReadyToActivate = () => {
|
||||
const shopServiceMock = new MockAdapter(shopApiAxiosClient, {
|
||||
delayResponse: 0,
|
||||
});
|
||||
shopServiceMock
|
||||
.onPost(`/purchase/${purchaseId}/action/claim`)
|
||||
.reply(config => {
|
||||
action('Claim purchase')();
|
||||
return [
|
||||
200,
|
||||
{
|
||||
id: purchaseId,
|
||||
productId: fakeBundleListingData.id,
|
||||
productType: 'BUNDLE',
|
||||
usageType: 'default',
|
||||
buyerId: 'buyer-id',
|
||||
receiverId: 'receiver-id',
|
||||
createdAt: new Date(1707519600000).toString(),
|
||||
},
|
||||
];
|
||||
})
|
||||
.onAny()
|
||||
.reply(config => {
|
||||
console.error(`Unexpected call to ${config.url} (${config.method})`);
|
||||
return [504, null];
|
||||
});
|
||||
|
||||
return (
|
||||
<RouterContext.Provider
|
||||
value={{
|
||||
...initialRouterState,
|
||||
navigateToRoute: (routeName, args) => {
|
||||
action('Navigate to route')(routeName, args);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AlertProvider>
|
||||
<AuthenticatedUserContext.Provider value={fakeSilverAuthenticatedUser}>
|
||||
<PurchaseClaimDialog
|
||||
claimedProductOptions={{
|
||||
productListingData: fakeBundleListingData,
|
||||
purchaseId,
|
||||
claimableToken,
|
||||
}}
|
||||
onClose={() => action('close')()}
|
||||
/>
|
||||
</AuthenticatedUserContext.Provider>
|
||||
</AlertProvider>
|
||||
</RouterContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const NotLoggedIn = () => {
|
||||
return (
|
||||
<RouterContext.Provider
|
||||
value={{
|
||||
...initialRouterState,
|
||||
navigateToRoute: (routeName, args) => {
|
||||
action('Navigate to route')(routeName, args);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AlertProvider>
|
||||
<AuthenticatedUserContext.Provider value={fakeNotAuthenticatedUser}>
|
||||
<PurchaseClaimDialog
|
||||
claimedProductOptions={{
|
||||
productListingData: fakeBundleListingData,
|
||||
purchaseId,
|
||||
claimableToken,
|
||||
}}
|
||||
onClose={() => action('close')()}
|
||||
/>
|
||||
</AuthenticatedUserContext.Provider>
|
||||
</AlertProvider>
|
||||
</RouterContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const AlreadyClaimed = () => {
|
||||
const shopServiceMock = new MockAdapter(shopApiAxiosClient, {
|
||||
delayResponse: 0,
|
||||
});
|
||||
shopServiceMock
|
||||
.onPost(`/purchase/${purchaseId}/action/claim`)
|
||||
.reply(() => {
|
||||
return [
|
||||
400,
|
||||
{
|
||||
code: 'purchase/already-claimed',
|
||||
message: 'This purchase has already been claimed.',
|
||||
},
|
||||
];
|
||||
})
|
||||
.onAny()
|
||||
.reply(config => {
|
||||
console.error(`Unexpected call to ${config.url} (${config.method})`);
|
||||
return [504, null];
|
||||
});
|
||||
|
||||
return (
|
||||
<RouterContext.Provider
|
||||
value={{
|
||||
...initialRouterState,
|
||||
navigateToRoute: (routeName, args) => {
|
||||
action('Navigate to route')(routeName, args);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AlertProvider>
|
||||
<AuthenticatedUserContext.Provider value={fakeSilverAuthenticatedUser}>
|
||||
<PurchaseClaimDialog
|
||||
claimedProductOptions={{
|
||||
productListingData: fakeBundleListingData,
|
||||
purchaseId,
|
||||
claimableToken,
|
||||
}}
|
||||
onClose={() => action('close')()}
|
||||
/>
|
||||
</AuthenticatedUserContext.Provider>
|
||||
</AlertProvider>
|
||||
</RouterContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const AlreadyOwned = () => {
|
||||
const shopServiceMock = new MockAdapter(shopApiAxiosClient, {
|
||||
delayResponse: 0,
|
||||
});
|
||||
shopServiceMock
|
||||
.onPost(`/purchase/${purchaseId}/action/claim`)
|
||||
.reply(() => {
|
||||
return [
|
||||
409,
|
||||
{
|
||||
code: 'purchase/already-owned',
|
||||
message: 'This account already owns this product.',
|
||||
},
|
||||
];
|
||||
})
|
||||
.onAny()
|
||||
.reply(config => {
|
||||
console.error(`Unexpected call to ${config.url} (${config.method})`);
|
||||
return [504, null];
|
||||
});
|
||||
|
||||
return (
|
||||
<RouterContext.Provider
|
||||
value={{
|
||||
...initialRouterState,
|
||||
navigateToRoute: (routeName, args) => {
|
||||
action('Navigate to route')(routeName, args);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AlertProvider>
|
||||
<AuthenticatedUserContext.Provider value={fakeSilverAuthenticatedUser}>
|
||||
<PurchaseClaimDialog
|
||||
claimedProductOptions={{
|
||||
productListingData: fakeBundleListingData,
|
||||
purchaseId,
|
||||
claimableToken,
|
||||
}}
|
||||
onClose={() => action('close')()}
|
||||
/>
|
||||
</AuthenticatedUserContext.Provider>
|
||||
</AlertProvider>
|
||||
</RouterContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const InvalidToken = () => {
|
||||
const shopServiceMock = new MockAdapter(shopApiAxiosClient, {
|
||||
delayResponse: 0,
|
||||
});
|
||||
shopServiceMock
|
||||
.onPost(`/purchase/${purchaseId}/action/claim`)
|
||||
.reply(() => {
|
||||
return [
|
||||
403,
|
||||
{
|
||||
code: 'purchase/invalid-token',
|
||||
message: 'The token is invalid.',
|
||||
},
|
||||
];
|
||||
})
|
||||
.onAny()
|
||||
.reply(config => {
|
||||
console.error(`Unexpected call to ${config.url} (${config.method})`);
|
||||
return [504, null];
|
||||
});
|
||||
|
||||
return (
|
||||
<RouterContext.Provider
|
||||
value={{
|
||||
...initialRouterState,
|
||||
navigateToRoute: (routeName, args) => {
|
||||
action('Navigate to route')(routeName, args);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AlertProvider>
|
||||
<AuthenticatedUserContext.Provider value={fakeSilverAuthenticatedUser}>
|
||||
<PurchaseClaimDialog
|
||||
claimedProductOptions={{
|
||||
productListingData: fakeBundleListingData,
|
||||
purchaseId,
|
||||
claimableToken,
|
||||
}}
|
||||
onClose={() => action('close')()}
|
||||
/>
|
||||
</AuthenticatedUserContext.Provider>
|
||||
</AlertProvider>
|
||||
</RouterContext.Provider>
|
||||
);
|
||||
};
|
@@ -0,0 +1,211 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import paperDecorator from '../PaperDecorator';
|
||||
import StandAloneDialog from '../../MainFrame/StandAloneDialog';
|
||||
import {
|
||||
client as assetApiAxiosClient,
|
||||
type Bundle,
|
||||
} from '../../Utils/GDevelopServices/Asset';
|
||||
import { client as userApiAxiosClient } from '../../Utils/GDevelopServices/User';
|
||||
import { client as shopApiAxiosClient } from '../../Utils/GDevelopServices/Shop';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
import {
|
||||
fakeAssetPackLicenses,
|
||||
fakeBundle,
|
||||
fakeBundleListingData,
|
||||
fakePrivateAssetPack1ListingData,
|
||||
fakePrivateAssetPack2ListingData,
|
||||
fakePrivateGameTemplateListingData,
|
||||
fakeSilverAuthenticatedUserWithCloudProjects,
|
||||
} from '../../fixtures/GDevelopServicesTestData';
|
||||
import {
|
||||
BundleStoreContext,
|
||||
initialBundleStoreState,
|
||||
} from '../../AssetStore/Bundles/BundleStoreContext';
|
||||
import {
|
||||
initialPrivateGameTemplateStoreState,
|
||||
PrivateGameTemplateStoreContext,
|
||||
} from '../../AssetStore/PrivateGameTemplates/PrivateGameTemplateStoreContext';
|
||||
import {
|
||||
AssetStoreContext,
|
||||
initialAssetStoreState,
|
||||
} from '../../AssetStore/AssetStoreContext';
|
||||
import { ProductLicenseStoreStateProvider } from '../../AssetStore/ProductLicense/ProductLicenseStoreContext';
|
||||
import { SubscriptionSuggestionProvider } from '../../Profile/Subscription/SubscriptionSuggestionContext';
|
||||
import { type AuthenticatedUser } from '../../Profile/AuthenticatedUserContext';
|
||||
import CourseStoreContext, {
|
||||
initialCourseStoreState,
|
||||
} from '../../Course/CourseStoreContext';
|
||||
import {
|
||||
CreditsPackageStoreContext,
|
||||
initialCreditsPackageStoreState,
|
||||
} from '../../AssetStore/CreditsPackages/CreditsPackageStoreContext';
|
||||
import RouterContext, {
|
||||
initialRouterState,
|
||||
} from '../../MainFrame/RouterContext';
|
||||
|
||||
export default {
|
||||
title: 'StandAloneDialog',
|
||||
component: StandAloneDialog,
|
||||
decorators: [paperDecorator],
|
||||
};
|
||||
|
||||
const sellerPublicProfile = {
|
||||
id: 'tVUYpNMz1AfsbzJtxUEpPTuu4Mn1',
|
||||
username: 'CreatorUserName',
|
||||
description: 'I create bundles for GDevelop.',
|
||||
};
|
||||
|
||||
const allBundleListingData = [fakeBundleListingData];
|
||||
|
||||
const allPrivateAssetPackListingData = [
|
||||
fakePrivateAssetPack1ListingData,
|
||||
fakePrivateAssetPack2ListingData,
|
||||
];
|
||||
|
||||
const allPrivateGameTemplateListingData = [fakePrivateGameTemplateListingData];
|
||||
|
||||
const mockCourses = [];
|
||||
|
||||
const StandAloneDialogStory = ({
|
||||
bundleCategory,
|
||||
receivedBundles = [],
|
||||
authenticatedUser = fakeSilverAuthenticatedUserWithCloudProjects,
|
||||
delayResponse = 0,
|
||||
errorCode,
|
||||
errorMessage,
|
||||
}: {
|
||||
bundleCategory: string,
|
||||
authenticatedUser?: AuthenticatedUser,
|
||||
receivedBundles?: Array<Bundle>,
|
||||
delayResponse?: number,
|
||||
errorCode?: number,
|
||||
errorMessage?: string,
|
||||
}) => {
|
||||
const userServiceMock = new MockAdapter(userApiAxiosClient, {
|
||||
delayResponse,
|
||||
});
|
||||
userServiceMock
|
||||
.onGet(`/user-public-profile/${fakeBundleListingData.sellerId}`)
|
||||
.reply(200, sellerPublicProfile)
|
||||
.onGet(`/user/${fakeBundleListingData.sellerId}/badge`)
|
||||
.reply(200, [])
|
||||
.onGet(`/achievement`)
|
||||
.reply(200, []);
|
||||
|
||||
const assetServiceMock = new MockAdapter(assetApiAxiosClient, {
|
||||
delayResponse,
|
||||
});
|
||||
assetServiceMock
|
||||
.onGet(`/bundle/${fakeBundleListingData.id}`)
|
||||
.reply(errorCode || 200, errorCode ? errorMessage || null : fakeBundle)
|
||||
.onGet(`/course`)
|
||||
.reply(200, mockCourses)
|
||||
.onAny()
|
||||
.reply(config => {
|
||||
console.error(`Unexpected call to ${config.url} (${config.method})`);
|
||||
return [504, null];
|
||||
});
|
||||
|
||||
const shopServiceMock = new MockAdapter(shopApiAxiosClient, {
|
||||
delayResponse,
|
||||
});
|
||||
shopServiceMock
|
||||
.onGet('/product-license')
|
||||
.reply(200, fakeAssetPackLicenses)
|
||||
.onPost(`/product/${fakeBundleListingData.id}/action/redeem`)
|
||||
.reply(config => {
|
||||
action('Claim bundle')();
|
||||
return [200, 'OK'];
|
||||
})
|
||||
.onAny()
|
||||
.reply(config => {
|
||||
console.error(`Unexpected call to ${config.url} (${config.method})`);
|
||||
return [504, null];
|
||||
});
|
||||
|
||||
return (
|
||||
<RouterContext.Provider
|
||||
value={{
|
||||
...initialRouterState,
|
||||
routeArguments: {
|
||||
'bundle-category': bundleCategory,
|
||||
},
|
||||
removeRouteArguments: () => {},
|
||||
navigateToRoute: (routeName, args) => {
|
||||
action('Navigate to route')(routeName, args);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<BundleStoreContext.Provider
|
||||
value={{
|
||||
...initialBundleStoreState,
|
||||
bundleListingDatas: allBundleListingData,
|
||||
}}
|
||||
>
|
||||
<PrivateGameTemplateStoreContext.Provider
|
||||
value={{
|
||||
...initialPrivateGameTemplateStoreState,
|
||||
privateGameTemplateListingDatas: allPrivateGameTemplateListingData,
|
||||
}}
|
||||
>
|
||||
<AssetStoreContext.Provider
|
||||
value={{
|
||||
...initialAssetStoreState,
|
||||
privateAssetPackListingDatas: allPrivateAssetPackListingData,
|
||||
}}
|
||||
>
|
||||
<AuthenticatedUserContext.Provider
|
||||
value={{
|
||||
...authenticatedUser,
|
||||
receivedBundles,
|
||||
bundlePurchases: receivedBundles.map(receivedBundle => ({
|
||||
id: 'purchase-id',
|
||||
productType: 'BUNDLE',
|
||||
usageType: 'commercial',
|
||||
productId: receivedBundle.id,
|
||||
buyerId: authenticatedUser.profile
|
||||
? authenticatedUser.profile.id
|
||||
: 'userId',
|
||||
receiverId: authenticatedUser.profile
|
||||
? authenticatedUser.profile.id
|
||||
: 'userId',
|
||||
createdAt: new Date(1707519600000).toString(),
|
||||
})),
|
||||
}}
|
||||
>
|
||||
<CourseStoreContext.Provider
|
||||
value={{
|
||||
...initialCourseStoreState,
|
||||
listedCourses: [],
|
||||
}}
|
||||
>
|
||||
<CreditsPackageStoreContext.Provider
|
||||
value={{
|
||||
...initialCreditsPackageStoreState,
|
||||
creditsPackageListingDatas: [],
|
||||
}}
|
||||
>
|
||||
<SubscriptionSuggestionProvider>
|
||||
<ProductLicenseStoreStateProvider>
|
||||
<StandAloneDialog onClose={() => action('close')()} />
|
||||
</ProductLicenseStoreStateProvider>
|
||||
</SubscriptionSuggestionProvider>
|
||||
</CreditsPackageStoreContext.Provider>
|
||||
</CourseStoreContext.Provider>
|
||||
</AuthenticatedUserContext.Provider>
|
||||
</AssetStoreContext.Provider>
|
||||
</PrivateGameTemplateStoreContext.Provider>
|
||||
</BundleStoreContext.Provider>
|
||||
</RouterContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = () => <StandAloneDialogStory bundleCategory="starter" />;
|
||||
|
||||
export const Loading = () => (
|
||||
<StandAloneDialogStory bundleCategory="starter" delayResponse={10000} />
|
||||
);
|
Reference in New Issue
Block a user