mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Fix: Avoid purchasing twice the same asset pack (#5482)
This commit is contained in:
@@ -12,7 +12,11 @@ import Grid from '@material-ui/core/Grid';
|
||||
import GridList from '@material-ui/core/GridList';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
import PlaceholderLoader from '../../UI/PlaceholderLoader';
|
||||
import { ResponsiveLineStackLayout, LineStackLayout } from '../../UI/Layout';
|
||||
import {
|
||||
ResponsiveLineStackLayout,
|
||||
LineStackLayout,
|
||||
ColumnStackLayout,
|
||||
} from '../../UI/Layout';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import {
|
||||
getUserPublicProfile,
|
||||
@@ -36,6 +40,7 @@ import {
|
||||
shouldUseAppStoreProduct,
|
||||
} from '../../Utils/AppStorePurchases';
|
||||
import { formatPrivateAssetPackPrice } from './PrivateAssetPackPriceTag';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
|
||||
const sameCreatorPackCountForSmallWindow = 2;
|
||||
const sameCreatorPackCountForMediumWindow = 3;
|
||||
@@ -85,6 +90,7 @@ const PrivateAssetPackInformationPage = ({
|
||||
onAssetPackOpen,
|
||||
}: Props) => {
|
||||
const { id, name, sellerId } = privateAssetPackListingData;
|
||||
const { receivedAssetPacks } = React.useContext(AuthenticatedUserContext);
|
||||
const [assetPack, setAssetPack] = React.useState<?PrivateAssetPack>(null);
|
||||
const [isFetching, setIsFetching] = React.useState<boolean>(false);
|
||||
const [
|
||||
@@ -102,6 +108,12 @@ const PrivateAssetPackInformationPage = ({
|
||||
const [errorText, setErrorText] = React.useState<?React.Node>(null);
|
||||
const windowWidth = useResponsiveWindowWidth();
|
||||
|
||||
const isAlreadyReceived =
|
||||
!!receivedAssetPacks &&
|
||||
!!receivedAssetPacks.find(
|
||||
assetPack => assetPack.id === privateAssetPackListingData.id
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
(async () => {
|
||||
@@ -135,38 +147,54 @@ const PrivateAssetPackInformationPage = ({
|
||||
[id, sellerId, privateAssetPackListingData.appStoreProductId]
|
||||
);
|
||||
|
||||
const onClickBuy = async () => {
|
||||
if (!assetPack) return;
|
||||
try {
|
||||
sendAssetPackBuyClicked({
|
||||
assetPackId: assetPack.id,
|
||||
assetPackName: assetPack.name,
|
||||
assetPackTag: assetPack.tag,
|
||||
assetPackKind: 'private',
|
||||
});
|
||||
|
||||
if (shouldUseAppStoreProduct()) {
|
||||
try {
|
||||
setAppStoreProductBeingBought(true);
|
||||
await purchaseAppStoreProduct(
|
||||
privateAssetPackListingData.appStoreProductId
|
||||
);
|
||||
} finally {
|
||||
setAppStoreProductBeingBought(false);
|
||||
}
|
||||
} else {
|
||||
onOpenPurchaseDialog();
|
||||
const onClickBuy = React.useCallback(
|
||||
async () => {
|
||||
if (!assetPack) return;
|
||||
if (isAlreadyReceived) {
|
||||
onAssetPackOpen(privateAssetPackListingData);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Unable to send event', e);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
sendAssetPackBuyClicked({
|
||||
assetPackId: assetPack.id,
|
||||
assetPackName: assetPack.name,
|
||||
assetPackTag: assetPack.tag,
|
||||
assetPackKind: 'private',
|
||||
});
|
||||
|
||||
if (shouldUseAppStoreProduct()) {
|
||||
try {
|
||||
setAppStoreProductBeingBought(true);
|
||||
await purchaseAppStoreProduct(
|
||||
privateAssetPackListingData.appStoreProductId
|
||||
);
|
||||
} finally {
|
||||
setAppStoreProductBeingBought(false);
|
||||
}
|
||||
} else {
|
||||
onOpenPurchaseDialog();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Unable to send event', e);
|
||||
}
|
||||
},
|
||||
[
|
||||
assetPack,
|
||||
onOpenPurchaseDialog,
|
||||
privateAssetPackListingData,
|
||||
isAlreadyReceived,
|
||||
onAssetPackOpen,
|
||||
]
|
||||
);
|
||||
|
||||
const getBuyButton = i18n => {
|
||||
if (errorText) return null;
|
||||
|
||||
const label = !assetPack ? (
|
||||
<Trans>Loading...</Trans>
|
||||
) : isAlreadyReceived ? (
|
||||
<Trans>Explore assets</Trans>
|
||||
) : isPurchaseDialogOpen || appStoreProductBeingBought ? (
|
||||
<Trans>Processing...</Trans>
|
||||
) : (
|
||||
@@ -241,7 +269,17 @@ const PrivateAssetPackInformationPage = ({
|
||||
horizontalOuterMarginToEatOnMobile={8}
|
||||
/>
|
||||
</Column>
|
||||
<Column useFullHeight expand noMargin>
|
||||
<ColumnStackLayout useFullHeight expand noMargin>
|
||||
{isAlreadyReceived && (
|
||||
<Column noMargin expand>
|
||||
<AlertMessage kind="info">
|
||||
<Trans>
|
||||
You already own this asset pack. Explore the assets
|
||||
to use them in your project.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
</Column>
|
||||
)}
|
||||
<Paper
|
||||
variant="outlined"
|
||||
style={{ padding: windowWidth === 'small' ? 20 : 30 }}
|
||||
@@ -342,7 +380,7 @@ const PrivateAssetPackInformationPage = ({
|
||||
</ResponsiveLineStackLayout>
|
||||
</Column>
|
||||
</Paper>
|
||||
</Column>
|
||||
</ColumnStackLayout>
|
||||
</ResponsiveLineStackLayout>
|
||||
{privateAssetPacksFromSameCreatorListingData &&
|
||||
// Only display packs if there are at least 2. If there is only one,
|
||||
|
@@ -96,7 +96,8 @@ const PrivateAssetPackPurchaseDialog = ({
|
||||
try {
|
||||
setIsPurchasing(true);
|
||||
const checkoutUrl = await getStripeCheckoutUrl(getAuthorizationHeader, {
|
||||
stripePriceId: privateAssetPackListingData.prices[0].stripePriceId,
|
||||
productId: privateAssetPackListingData.id,
|
||||
priceName: privateAssetPackListingData.prices[0].name,
|
||||
userId: profile.id,
|
||||
customerEmail: profile.email,
|
||||
...(password ? { password } : undefined),
|
||||
|
@@ -139,12 +139,14 @@ export const extractFilenameWithExtensionFromProductAuthorizedUrl = (
|
||||
export const getStripeCheckoutUrl = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
{
|
||||
stripePriceId,
|
||||
priceName,
|
||||
productId,
|
||||
userId,
|
||||
customerEmail,
|
||||
password,
|
||||
}: {|
|
||||
stripePriceId: string,
|
||||
priceName: string,
|
||||
productId: string,
|
||||
userId: string,
|
||||
customerEmail: string,
|
||||
password?: string,
|
||||
@@ -154,7 +156,8 @@ export const getStripeCheckoutUrl = async (
|
||||
const response = await client.post(
|
||||
'/purchase/action/create-stripe-checkout-session',
|
||||
{
|
||||
stripePriceId,
|
||||
priceName,
|
||||
productId,
|
||||
customerEmail,
|
||||
password,
|
||||
},
|
||||
|
@@ -13,6 +13,8 @@ import {
|
||||
} from '../../../../Utils/GDevelopServices/ApiConfigs';
|
||||
import { client as assetApiAxiosClient } from '../../../../Utils/GDevelopServices/Asset';
|
||||
import { type PrivateAssetPackListingData } from '../../../../Utils/GDevelopServices/Shop';
|
||||
import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserContext';
|
||||
import { fakeSilverAuthenticatedUserWithCloudProjects } from '../../../../fixtures/GDevelopServicesTestData';
|
||||
|
||||
export default {
|
||||
title: 'AssetStore/AssetStore/PrivateAssetPackInformationPage',
|
||||
@@ -144,6 +146,101 @@ export const Default = () => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const ForAlreadyPurchasedAssetPack = () => {
|
||||
const axiosMock = new MockAdapter(axios, { delayResponse: 0 });
|
||||
axiosMock
|
||||
.onGet(
|
||||
`${GDevelopUserApi.baseUrl}/user-public-profile/${
|
||||
privateAssetPackListingData.sellerId
|
||||
}`
|
||||
)
|
||||
.reply(200, sellerPublicProfile)
|
||||
.onGet(
|
||||
`${GDevelopUserApi.baseUrl}/user/${
|
||||
privateAssetPackListingData.sellerId
|
||||
}/badge`
|
||||
)
|
||||
.reply(200, [])
|
||||
.onGet(`${GDevelopUserApi.baseUrl}/achievement`)
|
||||
.reply(200, []);
|
||||
const assetServiceMock = new MockAdapter(assetApiAxiosClient);
|
||||
assetServiceMock
|
||||
.onGet(
|
||||
`${GDevelopAssetApi.baseUrl}/asset-pack/${privateAssetPackListingData.id}`
|
||||
)
|
||||
.reply(200, privateAssetPackDetails)
|
||||
.onAny()
|
||||
.reply(config => {
|
||||
console.error(`Unexpected call to ${config.url} (${config.method})`);
|
||||
return [504, null];
|
||||
});
|
||||
|
||||
return (
|
||||
<AuthenticatedUserContext.Provider
|
||||
value={{
|
||||
...fakeSilverAuthenticatedUserWithCloudProjects,
|
||||
receivedAssetPacks: [
|
||||
{
|
||||
id: privateAssetPackListingData.id,
|
||||
// Useless data for the component below.
|
||||
name: privateAssetPackListingData.name,
|
||||
createdAt: '2',
|
||||
updatedAt: '2',
|
||||
longDescription: 'longDescription',
|
||||
content: { sprite: 9 },
|
||||
previewImageUrls: [],
|
||||
tag: 'tag',
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<PrivateAssetPackInformationPage
|
||||
privateAssetPackListingData={privateAssetPackListingData}
|
||||
isPurchaseDialogOpen={false}
|
||||
onOpenPurchaseDialog={() => action('open purchase dialog')()}
|
||||
onAssetPackOpen={() => action('open asset pack')()}
|
||||
privateAssetPacksFromSameCreatorListingData={[
|
||||
{
|
||||
id: '56a50a9e-57ef-4d1d-a3f2-c918d593a6e2',
|
||||
sellerId: 'tVUYpNMz1AfsbzJtxUEpPTuu4Mn1',
|
||||
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: 'default', stripePriceId: 'stripePriceId' },
|
||||
],
|
||||
appStoreProductId: null,
|
||||
},
|
||||
{
|
||||
id: '56a50a9e-57ef-4d1d-a3f2-c918d568ef234',
|
||||
sellerId: 'tVUYpNMz1AfsbzJtxUEpPTuu4Mn1',
|
||||
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, name: 'default', stripePriceId: 'stripePriceId' },
|
||||
],
|
||||
appStoreProductId: 'fake.product.id',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</AuthenticatedUserContext.Provider>
|
||||
);
|
||||
};
|
||||
export const WithPurchaseDialogOpen = () => {
|
||||
const axiosMock = new MockAdapter(axios, { delayResponse: 0 });
|
||||
axiosMock
|
||||
|
Reference in New Issue
Block a user