From 8afa18caf6f78edcad542124a09288febf3e1393 Mon Sep 17 00:00:00 2001 From: Randy Schott <1815175+schottra@users.noreply.github.com> Date: Mon, 23 Oct 2023 10:36:20 -0400 Subject: [PATCH] [PAY-2045] Wire up content price and purchase limits (#6417) --- .../common/src/hooks/purchaseContent/index.ts | 2 + .../common/src/hooks/purchaseContent/types.ts | 7 ++ .../purchaseContent/usePayExtraPresets.ts | 6 +- .../usePurchaseContentErrorMessage.ts | 36 ++++++ .../purchaseContent/useUSDCPurchaseConfig.ts | 36 ++++++ packages/common/src/hooks/useRemoteVar.ts | 2 + .../src/services/remote-config/defaults.ts | 9 ++ .../src/services/remote-config/types.ts | 20 +++ packages/common/src/store/buy-usdc/sagas.ts | 57 +++++---- packages/common/src/store/buy-usdc/slice.ts | 18 ++- packages/common/src/store/buy-usdc/types.ts | 12 ++ packages/common/src/store/buy-usdc/utils.ts | 42 +++++++ .../src/store/purchase-content/sagas.ts | 20 ++- .../src/store/purchase-content/slice.ts | 15 ++- .../src/store/purchase-content/types.ts | 14 +++ .../PremiumTrackPurchaseDrawer.tsx | 37 +++--- .../edit-track-screen/EditTrackScreen.tsx | 115 +++++++++++------- .../data-entry/AccessAndSaleModalLegacy.tsx | 8 +- .../components/PurchaseContentFormFooter.tsx | 19 +-- .../upload-page/fields/AccessAndSaleField.tsx | 37 ++++-- 20 files changed, 387 insertions(+), 125 deletions(-) create mode 100644 packages/common/src/hooks/purchaseContent/usePurchaseContentErrorMessage.ts create mode 100644 packages/common/src/hooks/purchaseContent/useUSDCPurchaseConfig.ts diff --git a/packages/common/src/hooks/purchaseContent/index.ts b/packages/common/src/hooks/purchaseContent/index.ts index 1d3f794cd17..c209476e85d 100644 --- a/packages/common/src/hooks/purchaseContent/index.ts +++ b/packages/common/src/hooks/purchaseContent/index.ts @@ -1,5 +1,7 @@ export * from './usePurchaseContentFormConfiguration' export * from './useChallengeCooldownSchedule' +export * from './useUSDCPurchaseConfig' +export * from './usePurchaseContentErrorMessage' export * from './usePayExtraPresets' export * from './utils' export * from './types' diff --git a/packages/common/src/hooks/purchaseContent/types.ts b/packages/common/src/hooks/purchaseContent/types.ts index ba3f61ad2bc..67120100244 100644 --- a/packages/common/src/hooks/purchaseContent/types.ts +++ b/packages/common/src/hooks/purchaseContent/types.ts @@ -18,3 +18,10 @@ export enum PayExtraPreset { export type PurchasableTrackMetadata = UserTrackMetadata & { premium_conditions: PremiumConditionsUSDCPurchase } + +export type USDCPurchaseConfig = { + minContentPriceCents: number + maxContentPriceCents: number + minUSDCPurchaseAmountCents: number + maxUSDCPurchaseAmountCents: number +} diff --git a/packages/common/src/hooks/purchaseContent/usePayExtraPresets.ts b/packages/common/src/hooks/purchaseContent/usePayExtraPresets.ts index 3be3af87e60..1e050c6bd0d 100644 --- a/packages/common/src/hooks/purchaseContent/usePayExtraPresets.ts +++ b/packages/common/src/hooks/purchaseContent/usePayExtraPresets.ts @@ -3,14 +3,12 @@ import { useMemo } from 'react' import { StringKeys } from 'services/remote-config' import { parseIntList } from 'utils/stringUtils' -import { createUseRemoteVarHook } from '../useRemoteVar' +import { RemoteVarHook } from '../useRemoteVar' import { PayExtraAmountPresetValues, PayExtraPreset } from './types' /** Extracts and parses the Pay Extra presets from remote config */ -export const usePayExtraPresets = ( - useRemoteVar: ReturnType -) => { +export const usePayExtraPresets = (useRemoteVar: RemoteVarHook) => { const configValue = useRemoteVar(StringKeys.PAY_EXTRA_PRESET_CENT_AMOUNTS) return useMemo(() => { const [low, medium, high] = parseIntList(configValue) diff --git a/packages/common/src/hooks/purchaseContent/usePurchaseContentErrorMessage.ts b/packages/common/src/hooks/purchaseContent/usePurchaseContentErrorMessage.ts new file mode 100644 index 00000000000..5e6c5ff63c5 --- /dev/null +++ b/packages/common/src/hooks/purchaseContent/usePurchaseContentErrorMessage.ts @@ -0,0 +1,36 @@ +import { BuyUSDCErrorCode } from 'store/index' +import { + PurchaseContentErrorCode, + PurchaseErrorCode +} from 'store/purchase-content' +import { formatPrice } from 'utils/formatUtil' + +import { RemoteVarHook } from '../useRemoteVar' + +import { useUSDCPurchaseConfig } from './useUSDCPurchaseConfig' + +const messages = { + generic: 'Your purchase was unsuccessful.', + minimumPurchase: (minAmount: number) => + `Total purchase amount must be at least $${formatPrice(minAmount)}.`, + maximumPurchase: (maxAmount: number) => + `Total purchase amount may not exceed $${formatPrice(maxAmount)}.` +} + +export const usePurchaseContentErrorMessage = ( + errorCode: PurchaseContentErrorCode, + useRemoteVar: RemoteVarHook +) => { + const { minUSDCPurchaseAmountCents, maxUSDCPurchaseAmountCents } = + useUSDCPurchaseConfig(useRemoteVar) + + switch (errorCode) { + case BuyUSDCErrorCode.MinAmountNotMet: + return messages.minimumPurchase(minUSDCPurchaseAmountCents) + case BuyUSDCErrorCode.MaxAmountExceeded: + return messages.maximumPurchase(maxUSDCPurchaseAmountCents) + case BuyUSDCErrorCode.OnrampError: + case PurchaseErrorCode.Unknown: + return messages.generic + } +} diff --git a/packages/common/src/hooks/purchaseContent/useUSDCPurchaseConfig.ts b/packages/common/src/hooks/purchaseContent/useUSDCPurchaseConfig.ts new file mode 100644 index 00000000000..02a016dbd48 --- /dev/null +++ b/packages/common/src/hooks/purchaseContent/useUSDCPurchaseConfig.ts @@ -0,0 +1,36 @@ +import { useMemo } from 'react' + +import { IntKeys } from 'services/remote-config' + +import { RemoteVarHook } from '../useRemoteVar' + +import { USDCPurchaseConfig } from './types' + +/** Fetches the USDC/purchase content remote config values */ +export const useUSDCPurchaseConfig = ( + useRemoteVar: RemoteVarHook +): USDCPurchaseConfig => { + const minContentPriceCents = useRemoteVar(IntKeys.MIN_CONTENT_PRICE_CENTS) + const maxContentPriceCents = useRemoteVar(IntKeys.MAX_CONTENT_PRICE_CENTS) + const minUSDCPurchaseAmountCents = useRemoteVar( + IntKeys.MIN_USDC_PURCHASE_AMOUNT_CENTS + ) + const maxUSDCPurchaseAmountCents = useRemoteVar( + IntKeys.MAX_USDC_PURCHASE_AMOUNT_CENTS + ) + + return useMemo( + () => ({ + minContentPriceCents, + maxContentPriceCents, + minUSDCPurchaseAmountCents, + maxUSDCPurchaseAmountCents + }), + [ + minContentPriceCents, + maxContentPriceCents, + minUSDCPurchaseAmountCents, + maxUSDCPurchaseAmountCents + ] + ) +} diff --git a/packages/common/src/hooks/useRemoteVar.ts b/packages/common/src/hooks/useRemoteVar.ts index cdefe010d5c..05a9dfca33c 100644 --- a/packages/common/src/hooks/useRemoteVar.ts +++ b/packages/common/src/hooks/useRemoteVar.ts @@ -44,3 +44,5 @@ export const createUseRemoteVarHook = ({ return useRemoteVar } + +export type RemoteVarHook = ReturnType diff --git a/packages/common/src/services/remote-config/defaults.ts b/packages/common/src/services/remote-config/defaults.ts index 3b05f1fd223..cebe27ab029 100644 --- a/packages/common/src/services/remote-config/defaults.ts +++ b/packages/common/src/services/remote-config/defaults.ts @@ -4,6 +4,11 @@ const ETH_PROVIDER_URLS = process.env.REACT_APP_ETH_PROVIDER_URL || '' const DEFAULT_ENTRY_TTL = 1 /* min */ * 60 /* seconds */ * 1000 /* ms */ const DEFAULT_HANDLE_VERIFICATION_TIMEOUT_MILLIS = 5_000 +export const MIN_USDC_PURCHASE_AMOUNT_CENTS = 100 +export const MAX_USDC_PURCHASE_AMOUNT_CENTS = 150000 +export const MIN_CONTENT_PRICE_CENTS = 100 +export const MAX_CONTENT_PRICE_CENTS = 150000 + export const remoteConfigIntDefaults: { [key in IntKeys]: number | null } = { [IntKeys.IMAGE_QUICK_FETCH_TIMEOUT_MS]: 5000, [IntKeys.IMAGE_QUICK_FETCH_PERFORMANCE_BATCH_SIZE]: 20, @@ -29,6 +34,10 @@ export const remoteConfigIntDefaults: { [key in IntKeys]: number | null } = { [IntKeys.CHALLENGE_CLAIM_COMPLETION_POLL_TIMEOUT_MS]: 10000, [IntKeys.MIN_AUDIO_PURCHASE_AMOUNT]: 5, [IntKeys.MAX_AUDIO_PURCHASE_AMOUNT]: 999, + [IntKeys.MIN_USDC_PURCHASE_AMOUNT_CENTS]: MIN_USDC_PURCHASE_AMOUNT_CENTS, + [IntKeys.MAX_USDC_PURCHASE_AMOUNT_CENTS]: MAX_USDC_PURCHASE_AMOUNT_CENTS, + [IntKeys.MIN_CONTENT_PRICE_CENTS]: MIN_CONTENT_PRICE_CENTS, + [IntKeys.MAX_CONTENT_PRICE_CENTS]: MAX_CONTENT_PRICE_CENTS, [IntKeys.BUY_TOKEN_WALLET_POLL_DELAY_MS]: 1000, [IntKeys.BUY_TOKEN_WALLET_POLL_MAX_RETRIES]: 120, [IntKeys.BUY_AUDIO_SLIPPAGE]: 3, diff --git a/packages/common/src/services/remote-config/types.ts b/packages/common/src/services/remote-config/types.ts index dedae7fe39d..00cf65a5bf5 100644 --- a/packages/common/src/services/remote-config/types.ts +++ b/packages/common/src/services/remote-config/types.ts @@ -129,6 +129,26 @@ export enum IntKeys { */ MAX_AUDIO_PURCHASE_AMOUNT = 'MAX_AUDIO_PURCHASE_AMOUNT', + /** + * Minimum price for purchasable content in cents + */ + MIN_CONTENT_PRICE_CENTS = 'MIN_CONTENT_PRICE_CENTS', + + /** + * Maximum price for purchasable content in cents + */ + MAX_CONTENT_PRICE_CENTS = 'MAX_CONTENT_PRICE_CENTS', + + /** + * Minimum USDC (in cents) required to purchase in the BuyUSDC modal + */ + MIN_USDC_PURCHASE_AMOUNT_CENTS = 'MIN_USDC_PURCHASE_AMOUNT_CENTS', + + /** + * Maximum USDC (in cents) required to purchase in the BuyUSDC modal + */ + MAX_USDC_PURCHASE_AMOUNT_CENTS = 'MAX_USDC_PURCHASE_AMOUNT_CENTS', + /** * The time to delay between polls of the user wallet when performing a purchase of $AUDIO/$USDC */ diff --git a/packages/common/src/store/buy-usdc/sagas.ts b/packages/common/src/store/buy-usdc/sagas.ts index 679e99c1145..6cf39711318 100644 --- a/packages/common/src/store/buy-usdc/sagas.ts +++ b/packages/common/src/store/buy-usdc/sagas.ts @@ -14,7 +14,6 @@ import { pollForBalanceChange, relayTransaction } from 'services/audius-backend/solana' -import { IntKeys } from 'services/remote-config' import { getAccountUser } from 'store/account/selectors' import { getContext } from 'store/effects' import { getFeePayer } from 'store/solana/selectors' @@ -33,25 +32,8 @@ import { startRecoveryIfNecessary, recoveryStatusChanged } from './slice' -import { USDCOnRampProvider } from './types' -import { getUSDCUserBank } from './utils' - -// TODO: Configurable min/max usdc purchase amounts? -function* getBuyUSDCRemoteConfig() { - const remoteConfigInstance = yield* getContext('remoteConfigInstance') - yield* call([remoteConfigInstance, remoteConfigInstance.waitForRemoteConfig]) - const retryDelayMs = - remoteConfigInstance.getRemoteVar(IntKeys.BUY_TOKEN_WALLET_POLL_DELAY_MS) ?? - undefined - const maxRetryCount = - remoteConfigInstance.getRemoteVar( - IntKeys.BUY_TOKEN_WALLET_POLL_MAX_RETRIES - ) ?? undefined - return { - maxRetryCount, - retryDelayMs - } -} +import { BuyUSDCError, BuyUSDCErrorCode, USDCOnRampProvider } from './types' +import { getBuyUSDCRemoteConfig, getUSDCUserBank } from './utils' type PurchaseStepParams = { desiredAmount: number @@ -104,20 +86,20 @@ function* purchaseStep({ ) return {} } else if (result.failure) { - const error = result.failure.payload?.error - ? result.failure.payload.error - : new Error('Unknown error') + const errorString = result.failure.payload?.error + ? result.failure.payload.error.message + : 'Unknown error' yield* call( track, make({ eventName: Name.BUY_USDC_ON_RAMP_FAILURE, provider, - error: error.message + error: errorString }) ) // Throw up to the flow above this - throw error + throw new BuyUSDCError(BuyUSDCErrorCode.OnrampError, errorString) } yield* call( track, @@ -224,13 +206,31 @@ function* doBuyUSDC({ const reportToSentry = yield* getContext('reportToSentry') const { track, make } = yield* getContext('analytics') const audiusBackendInstance = yield* getContext('audiusBackendInstance') + const config = yield* call(getBuyUSDCRemoteConfig) const userBank = yield* getUSDCUserBank() const rootAccount = yield* call(getRootSolanaAccount, audiusBackendInstance) try { if (provider !== USDCOnRampProvider.STRIPE) { - throw new Error('USDC Purchase is only supported via Stripe') + throw new BuyUSDCError( + BuyUSDCErrorCode.OnrampError, + 'USDC Purchase is only supported via Stripe' + ) + } + + if (desiredAmount < config.minUSDCPurchaseAmountCents) { + throw new BuyUSDCError( + BuyUSDCErrorCode.MinAmountNotMet, + `Minimum USDC purchase amount is ${config.minUSDCPurchaseAmountCents} cents` + ) + } + + if (desiredAmount > config.maxUSDCPurchaseAmountCents) { + throw new BuyUSDCError( + BuyUSDCErrorCode.MaxAmountExceeded, + `Maximum USDC purchase amount is ${config.maxUSDCPurchaseAmountCents} cents` + ) } yield* put( @@ -291,7 +291,10 @@ function* doBuyUSDC({ }) ) } catch (e) { - const error = e as Error + const error = + e instanceof BuyUSDCError + ? e + : new BuyUSDCError(BuyUSDCErrorCode.OnrampError, `${e}`) yield* call(reportToSentry, { level: ErrorLevel.Error, error, diff --git a/packages/common/src/store/buy-usdc/slice.ts b/packages/common/src/store/buy-usdc/slice.ts index 8eee70eb991..70c93e738be 100644 --- a/packages/common/src/store/buy-usdc/slice.ts +++ b/packages/common/src/store/buy-usdc/slice.ts @@ -1,6 +1,11 @@ import { Action, createSlice, PayloadAction } from '@reduxjs/toolkit' -import { BuyUSDCStage, USDCOnRampProvider, PurchaseInfo } from './types' +import { + BuyUSDCStage, + USDCOnRampProvider, + PurchaseInfo, + BuyUSDCError +} from './types' type StripeSessionStatus = | 'initialized' @@ -18,7 +23,7 @@ type RecoveryStatus = 'idle' | 'in-progress' | 'success' | 'failure' type BuyUSDCState = { stage: BuyUSDCStage - error?: Error + error?: BuyUSDCError provider: USDCOnRampProvider onSuccess?: OnSuccess stripeSessionStatus?: StripeSessionStatus @@ -59,10 +64,13 @@ const slice = createSlice({ onrampSucceeded: (state) => { state.stage = BuyUSDCStage.CONFIRMING_PURCHASE }, - onrampFailed: (state, action: PayloadAction<{ error: Error }>) => { - state.error = new Error(`Stripe onramp failed: ${action.payload}`) + onrampFailed: (_state, _action: PayloadAction<{ error: Error }>) => { + // handled by saga }, - buyUSDCFlowFailed: (state, action: PayloadAction<{ error: Error }>) => { + buyUSDCFlowFailed: ( + state, + action: PayloadAction<{ error: BuyUSDCError }> + ) => { state.error = action.payload.error }, buyUSDCFlowSucceeded: (state) => { diff --git a/packages/common/src/store/buy-usdc/types.ts b/packages/common/src/store/buy-usdc/types.ts index a1743d64389..0cae95ae70b 100644 --- a/packages/common/src/store/buy-usdc/types.ts +++ b/packages/common/src/store/buy-usdc/types.ts @@ -18,3 +18,15 @@ export enum BuyUSDCStage { CANCELED = 'CANCELED', FINISH = 'FINISH' } + +export enum BuyUSDCErrorCode { + MinAmountNotMet = 'MinAmountNotMet', + MaxAmountExceeded = 'MaxAmountExceeded', + OnrampError = 'OnrampError' +} + +export class BuyUSDCError extends Error { + constructor(public code: BuyUSDCErrorCode, message: string) { + super(message) + } +} diff --git a/packages/common/src/store/buy-usdc/utils.ts b/packages/common/src/store/buy-usdc/utils.ts index 7a4192314f1..341f30ed20e 100644 --- a/packages/common/src/store/buy-usdc/utils.ts +++ b/packages/common/src/store/buy-usdc/utils.ts @@ -1,6 +1,13 @@ import { call, select } from 'typed-redux-saga' import { createUserBankIfNeeded } from 'services/audius-backend/solana' +import { IntKeys } from 'services/index' +import { + MAX_CONTENT_PRICE_CENTS, + MAX_USDC_PURCHASE_AMOUNT_CENTS, + MIN_CONTENT_PRICE_CENTS, + MIN_USDC_PURCHASE_AMOUNT_CENTS +} from 'services/remote-config/defaults' import { getContext } from 'store/effects' import { getFeePayer } from 'store/solana/selectors' @@ -22,3 +29,38 @@ export function* getUSDCUserBank(ethAddress?: string) { recordAnalytics: track }) } + +export function* getBuyUSDCRemoteConfig() { + const remoteConfigInstance = yield* getContext('remoteConfigInstance') + yield* call([remoteConfigInstance, remoteConfigInstance.waitForRemoteConfig]) + + const minContentPriceCents = + remoteConfigInstance.getRemoteVar(IntKeys.MIN_CONTENT_PRICE_CENTS) ?? + MIN_CONTENT_PRICE_CENTS + const maxContentPriceCents = + remoteConfigInstance.getRemoteVar(IntKeys.MAX_CONTENT_PRICE_CENTS) ?? + MAX_CONTENT_PRICE_CENTS + const minUSDCPurchaseAmountCents = + remoteConfigInstance.getRemoteVar(IntKeys.MIN_USDC_PURCHASE_AMOUNT_CENTS) ?? + MIN_USDC_PURCHASE_AMOUNT_CENTS + const maxUSDCPurchaseAmountCents = + remoteConfigInstance.getRemoteVar(IntKeys.MAX_USDC_PURCHASE_AMOUNT_CENTS) ?? + MAX_USDC_PURCHASE_AMOUNT_CENTS + + const retryDelayMs = + remoteConfigInstance.getRemoteVar(IntKeys.BUY_TOKEN_WALLET_POLL_DELAY_MS) ?? + undefined + const maxRetryCount = + remoteConfigInstance.getRemoteVar( + IntKeys.BUY_TOKEN_WALLET_POLL_MAX_RETRIES + ) ?? undefined + + return { + minContentPriceCents, + maxContentPriceCents, + minUSDCPurchaseAmountCents, + maxUSDCPurchaseAmountCents, + maxRetryCount, + retryDelayMs + } +} diff --git a/packages/common/src/store/purchase-content/sagas.ts b/packages/common/src/store/purchase-content/sagas.ts index 1b1fea09b50..af94b060d68 100644 --- a/packages/common/src/store/purchase-content/sagas.ts +++ b/packages/common/src/store/purchase-content/sagas.ts @@ -18,7 +18,7 @@ import { onrampOpened, onrampCanceled } from 'store/buy-usdc/slice' -import { USDCOnRampProvider } from 'store/buy-usdc/types' +import { BuyUSDCError, USDCOnRampProvider } from 'store/buy-usdc/types' import { getUSDCUserBank } from 'store/buy-usdc/utils' import { getTrack } from 'store/cache/tracks/selectors' import { getUser } from 'store/cache/users/selectors' @@ -38,7 +38,7 @@ import { purchaseContentFlowFailed, startPurchaseContentFlow } from './slice' -import { ContentType } from './types' +import { ContentType, PurchaseContentError, PurchaseErrorCode } from './types' const { getUserId } = accountSelectors @@ -208,7 +208,9 @@ function* doStartPurchaseContentFlow({ return } if (result.failed) { - yield* put(purchaseContentFlowFailed()) + yield* put( + purchaseContentFlowFailed({ error: result.failed.payload.error }) + ) return } } @@ -246,19 +248,25 @@ function* doStartPurchaseContentFlow({ make({ eventName: Name.PURCHASE_CONTENT_SUCCESS, contentId, contentType }) ) } catch (e: unknown) { + // If we get a known error, pipe it through directly. Otherwise make sure we + // have a properly contstructed error to put into the slice. + const error = + e instanceof PurchaseContentError || e instanceof BuyUSDCError + ? e + : new PurchaseContentError(PurchaseErrorCode.Unknown, `${e}`) yield* call(reportToSentry, { level: ErrorLevel.Error, - error: e as Error, + error, additionalInfo: { contentId, contentType } }) - yield* put(purchaseContentFlowFailed()) + yield* put(purchaseContentFlowFailed({ error })) yield* call( track, make({ eventName: Name.PURCHASE_CONTENT_FAILURE, contentId, contentType, - error: (e as Error).message + error: error.message }) ) } diff --git a/packages/common/src/store/purchase-content/slice.ts b/packages/common/src/store/purchase-content/slice.ts index c3127814136..86903263277 100644 --- a/packages/common/src/store/purchase-content/slice.ts +++ b/packages/common/src/store/purchase-content/slice.ts @@ -2,7 +2,11 @@ import { Action, createSlice, PayloadAction } from '@reduxjs/toolkit' import { ID } from 'models/Identifiers' -import { ContentType, PurchaseContentStage } from './types' +import { + ContentType, + PurchaseContentError, + PurchaseContentStage +} from './types' type OnSuccess = { action?: Action @@ -17,7 +21,7 @@ type PurchaseContentState = { extraAmount?: number /** Used for analytics */ extraAmountPreset?: string - error?: Error + error?: PurchaseContentError onSuccess?: OnSuccess } @@ -73,8 +77,11 @@ const slice = createSlice({ ) => { state.stage = PurchaseContentStage.FINISH }, - purchaseContentFlowFailed: (state) => { - state.error = new Error('Content purchase failed') + purchaseContentFlowFailed: ( + state, + action: PayloadAction<{ error: PurchaseContentError }> + ) => { + state.error = action.payload.error }, cleanup: () => initialState } diff --git a/packages/common/src/store/purchase-content/types.ts b/packages/common/src/store/purchase-content/types.ts index 539f4b2ad24..2c195d285c9 100644 --- a/packages/common/src/store/purchase-content/types.ts +++ b/packages/common/src/store/purchase-content/types.ts @@ -1,3 +1,5 @@ +import { BuyUSDCErrorCode } from '../buy-usdc' + export enum ContentType { TRACK = 'track' } @@ -10,3 +12,15 @@ export enum PurchaseContentStage { CANCELED = 'CANCELED', FINISH = 'FINISH' } + +export enum PurchaseErrorCode { + Unknown = 'Unknown' +} + +export type PurchaseContentErrorCode = BuyUSDCErrorCode | PurchaseErrorCode + +export class PurchaseContentError extends Error { + constructor(public code: PurchaseContentErrorCode, message: string) { + super(message) + } +} diff --git a/packages/mobile/src/components/premium-track-purchase-drawer/PremiumTrackPurchaseDrawer.tsx b/packages/mobile/src/components/premium-track-purchase-drawer/PremiumTrackPurchaseDrawer.tsx index c903c3b46a2..7719841bb49 100644 --- a/packages/mobile/src/components/premium-track-purchase-drawer/PremiumTrackPurchaseDrawer.tsx +++ b/packages/mobile/src/components/premium-track-purchase-drawer/PremiumTrackPurchaseDrawer.tsx @@ -1,6 +1,9 @@ import { useCallback, type ReactNode, useEffect } from 'react' -import type { PurchasableTrackMetadata } from '@audius/common' +import type { + PurchasableTrackMetadata, + PurchaseContentError +} from '@audius/common' import { PurchaseContentStage, formatPrice, @@ -11,6 +14,7 @@ import { statusIsNotFinalized, useGetTrackById, usePayExtraPresets, + usePurchaseContentErrorMessage, usePurchaseContentFormConfiguration } from '@audius/common' import { Formik, useFormikContext } from 'formik' @@ -66,8 +70,7 @@ const messages = { } ), - termsOfUse: 'Terms of Use.', - error: 'Your purchase was unsuccessful.' + termsOfUse: 'Terms of Use.' } const useStyles = makeStyles(({ spacing, typography, palette }) => ({ @@ -136,6 +139,19 @@ const useStyles = makeStyles(({ spacing, typography, palette }) => ({ } })) +const RenderError = ({ error: { code } }: { error: PurchaseContentError }) => { + const styles = useStyles() + const { accentRed } = useThemeColors() + return ( + + + + {usePurchaseContentErrorMessage(code, useRemoteVar)} + + + ) +} + const PremiumTrackPurchaseDrawerHeader = ({ onClose }: { @@ -182,7 +198,7 @@ const getButtonText = (isUnlocking: boolean, amountDue: number) => const RenderForm = ({ track }: { track: PurchasableTrackMetadata }) => { const navigation = useNavigation() const styles = useStyles() - const { specialLightGreen, accentRed, secondary } = useThemeColors() + const { specialLightGreen, secondary } = useThemeColors() const presetValues = usePayExtraPresets(useRemoteVar) const { submitForm, resetForm } = useFormikContext() @@ -253,18 +269,7 @@ const RenderForm = ({ track }: { track: PurchasableTrackMetadata }) => { {isPurchaseSuccessful ? null : ( - {error ? ( - - - - {messages.error} - - - ) : null} + {error ? : null}