Fix: Avoid purchasing twice the same asset pack (#5482)

This commit is contained in:
AlexandreS
2023-07-17 17:28:56 +02:00
committed by GitHub
parent 4d1d763bd9
commit 4f65fa0d82
4 changed files with 171 additions and 32 deletions

View File

@@ -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,

View File

@@ -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),

View File

@@ -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,
},

View File

@@ -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