From a15e3d853227e4cc9b70a955fdbb8fb82326621d Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Fri, 26 Jan 2024 17:14:05 -0800 Subject: [PATCH] [PAY-2400] Break CoinflowWithdrawModal out into its own modal (#7357) --- .../modals/coinflow-withdraw-modal/index.ts | 20 +++++ packages/common/src/store/ui/modals/index.ts | 1 + .../common/src/store/ui/modals/parentSlice.ts | 3 +- .../common/src/store/ui/modals/reducers.ts | 4 +- packages/common/src/store/ui/modals/types.ts | 3 + .../CoinflowWithdrawModal.module.css | 16 ++++ .../components/CoinflowWithdrawModal.tsx | 86 +++++++++++++++++++ .../CoinflowWithdrawPage.module.css | 5 ++ .../components/CoinflowWithdrawPage.tsx | 68 ++------------- .../components/PrepareTransfer.tsx | 12 ++- packages/web/src/pages/modals/Modals.tsx | 4 +- .../application/ui/withdraw-usdc/sagas.ts | 33 ++++++- 12 files changed, 186 insertions(+), 69 deletions(-) create mode 100644 packages/common/src/store/ui/modals/coinflow-withdraw-modal/index.ts create mode 100644 packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawModal.module.css create mode 100644 packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawModal.tsx create mode 100644 packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawPage.module.css diff --git a/packages/common/src/store/ui/modals/coinflow-withdraw-modal/index.ts b/packages/common/src/store/ui/modals/coinflow-withdraw-modal/index.ts new file mode 100644 index 00000000000..e1681e9230f --- /dev/null +++ b/packages/common/src/store/ui/modals/coinflow-withdraw-modal/index.ts @@ -0,0 +1,20 @@ +import { createModal } from '../createModal' + +export type CoinflowWithdrawModalState = { + amount: number +} + +const coinflowWithdrawModal = createModal({ + reducerPath: 'CoinflowWithdraw', + initialState: { + isOpen: false, + amount: 5 + }, + sliceSelector: (state) => state.ui.modals +}) + +export const { + hook: useCoinflowWithdrawModal, + actions: coinflowWithdrawModalActions, + reducer: coinflowWithdrawModalReducer +} = coinflowWithdrawModal diff --git a/packages/common/src/store/ui/modals/index.ts b/packages/common/src/store/ui/modals/index.ts index ddefa97f59d..1b68c9faa2a 100644 --- a/packages/common/src/store/ui/modals/index.ts +++ b/packages/common/src/store/ui/modals/index.ts @@ -6,6 +6,7 @@ export { sagas as modalsSagas } from './sagas' export * from './create-chat-modal' export * from './coinflow-onramp-modal' +export * from './coinflow-withdraw-modal' export * from './leaving-audius-modal' export * from './inbox-unavailable-modal' export * from './usdc-purchase-details-modal' diff --git a/packages/common/src/store/ui/modals/parentSlice.ts b/packages/common/src/store/ui/modals/parentSlice.ts index c0b485183cd..bdb373f536e 100644 --- a/packages/common/src/store/ui/modals/parentSlice.ts +++ b/packages/common/src/store/ui/modals/parentSlice.ts @@ -59,7 +59,8 @@ export const initialState: BasicModalsState = { USDCManualTransferModal: { isOpen: false }, CoinflowOnramp: { isOpen: false }, AddFundsModal: { isOpen: false }, - Welcome: { isOpen: false } + Welcome: { isOpen: false }, + CoinflowWithdraw: { isOpen: false } } const slice = createSlice({ diff --git a/packages/common/src/store/ui/modals/reducers.ts b/packages/common/src/store/ui/modals/reducers.ts index 7708648206a..6c1734df22c 100644 --- a/packages/common/src/store/ui/modals/reducers.ts +++ b/packages/common/src/store/ui/modals/reducers.ts @@ -15,6 +15,7 @@ import { usdcManualTransferModalReducer } from './usdc-manual-transfer-modal' import { usdcPurchaseDetailsModalReducer } from './usdc-purchase-details-modal' import { usdcTransactionDetailsModalReducer } from './usdc-transaction-details-modal' import { withdrawUSDCModalReducer } from './withdraw-usdc-modal' +import { coinflowWithdrawModalReducer } from './coinflow-withdraw-modal' /** * Create a bunch of reducers that do nothing, so that the state is maintained and not lost through the child reducers @@ -42,7 +43,8 @@ const combinedReducers = combineReducers({ AddFundsModal: addFundsModalReducer, USDCTransactionDetailsModal: usdcTransactionDetailsModalReducer, PremiumContentPurchaseModal: premiumContentPurchaseModalReducer, - CoinflowOnramp: coinflowOnrampModalReducer + CoinflowOnramp: coinflowOnrampModalReducer, + CoinflowWithdraw: coinflowWithdrawModalReducer }) /** diff --git a/packages/common/src/store/ui/modals/types.ts b/packages/common/src/store/ui/modals/types.ts index a9210c9e5f4..e0025c5fa13 100644 --- a/packages/common/src/store/ui/modals/types.ts +++ b/packages/common/src/store/ui/modals/types.ts @@ -13,6 +13,7 @@ import { USDCManualTransferModalState } from './usdc-manual-transfer-modal' import { USDCPurchaseDetailsModalState } from './usdc-purchase-details-modal' import { USDCTransactionDetailsModalState } from './usdc-transaction-details-modal' import { WithdrawUSDCModalState } from './withdraw-usdc-modal' +import { CoinflowWithdrawModalState } from './coinflow-withdraw-modal' export type Modals = | 'TiersExplainer' @@ -67,6 +68,7 @@ export type Modals = | 'USDCManualTransferModal' | 'AddFundsModal' | 'Welcome' + | 'CoinflowWithdraw' export type BasicModalsState = { [modal in Modals]: BaseModalState @@ -85,6 +87,7 @@ export type StatefulModalsState = { USDCManualTransferModal: USDCManualTransferModalState AddFundsModal: AddFundsModalState PremiumContentPurchaseModal: PremiumContentPurchaseModalState + CoinflowWithdraw: CoinflowWithdrawModalState } export type ModalsState = BasicModalsState & StatefulModalsState diff --git a/packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawModal.module.css b/packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawModal.module.css new file mode 100644 index 00000000000..406b1c20bd8 --- /dev/null +++ b/packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawModal.module.css @@ -0,0 +1,16 @@ +.modalWrapper { + height: 100%; +} + +.modalBody { + height: 100%; + max-width: 720px; + max-height: 800px; +} + +.modalWrapper iframe { + max-height: 100% !important; + min-height: 800px !important; + max-width: 100% !important; + margin: 0 auto !important; +} diff --git a/packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawModal.tsx b/packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawModal.tsx new file mode 100644 index 00000000000..5549bc69ae3 --- /dev/null +++ b/packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawModal.tsx @@ -0,0 +1,86 @@ +import { useCallback } from 'react' + +import { + useCoinflowAdapter, + useCoinflowWithdrawModal, + withdrawUSDCActions +} from '@audius/common' +import { CoinflowWithdraw } from '@coinflowlabs/react' +import { useDispatch } from 'react-redux' + +import ModalDrawer from 'pages/audio-rewards-page/components/modals/ModalDrawer' +import { env } from 'services/env' +import zIndex from 'utils/zIndex' + +import styles from './CoinflowWithdrawModal.module.css' + +const { coinflowWithdrawalCanceled, coinflowWithdrawalSucceeded } = + withdrawUSDCActions + +const parseTransactionFromSuccessParams = (params: string) => { + try { + const parsed = JSON.parse(params) + return parsed.data as string + } catch (e) { + console.error( + `Failed to parse transaction from params: ${params}, received error: ${e}` + ) + return '' + } +} + +const MERCHANT_ID = env.COINFLOW_MERCHANT_ID +const IS_PRODUCTION = env.ENVIRONMENT === 'production' + +export const CoinflowWithdrawModal = () => { + const { + data: { amount }, + isOpen, + onClose, + onClosed + } = useCoinflowWithdrawModal() + + const adapter = useCoinflowAdapter() + const dispatch = useDispatch() + + const handleClose = useCallback(() => { + dispatch(coinflowWithdrawalCanceled()) + onClose() + }, [dispatch, onClose]) + + const handleSuccess = useCallback( + (params: string) => { + const transaction = parseTransactionFromSuccessParams(params) + dispatch(coinflowWithdrawalSucceeded({ transaction })) + onClose() + }, + [dispatch] + ) + + const showContent = isOpen && adapter + + return ( + + {showContent ? ( + + ) : null} + + ) +} diff --git a/packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawPage.module.css b/packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawPage.module.css new file mode 100644 index 00000000000..a758dfcb92f --- /dev/null +++ b/packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawPage.module.css @@ -0,0 +1,5 @@ +.spinner { + height: var(--unit-10); + width: var(--unit-10); + align-self: center; +} diff --git a/packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawPage.tsx b/packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawPage.tsx index e47af8fef4e..cf869b9fe13 100644 --- a/packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawPage.tsx +++ b/packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawPage.tsx @@ -1,67 +1,13 @@ -import { useCallback, useState } from 'react' +import { Flex } from '@audius/harmony' -import { useCoinflowAdapter, withdrawUSDCActions } from '@audius/common' -import { CoinflowWithdraw } from '@coinflowlabs/react' -import { useField } from 'formik' -import { useDispatch } from 'react-redux' -import { useUnmount } from 'react-use' +import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' -import { env } from 'services/env' - -import { AMOUNT, WithdrawFormValues } from '../types' - -const { coinflowWithdrawalCanceled, coinflowWithdrawalSucceeded } = - withdrawUSDCActions - -const parseTransactionFromSuccessParams = (params: string) => { - try { - const parsed = JSON.parse(params) - return parsed.data as string - } catch (e) { - console.error( - `Failed to parse transaction from params: ${params}, received error: ${e}` - ) - return '' - } -} - -const MERCHANT_ID = env.COINFLOW_MERCHANT_ID -const IS_PRODUCTION = env.ENVIRONMENT === 'production' +import styles from './CoinflowWithdrawPage.module.css' export const CoinflowWithdrawPage = () => { - const adapter = useCoinflowAdapter() - const dispatch = useDispatch() - const [{ value: amountCents }] = - useField(AMOUNT) - const [completed, setCompleted] = useState(false) - - useUnmount(() => { - // There is no cancelation callback for coinflow, but we want - // to make sure any waiting saga finishes. - if (!completed) { - dispatch(coinflowWithdrawalCanceled()) - } - }) - - const handleSuccess = useCallback( - (params: string) => { - const transaction = parseTransactionFromSuccessParams(params) - setCompleted(true) - dispatch(coinflowWithdrawalSucceeded({ transaction })) - }, - [dispatch] + return ( + + + ) - - return adapter ? ( - - ) : null } diff --git a/packages/web/src/components/withdraw-usdc-modal/components/PrepareTransfer.tsx b/packages/web/src/components/withdraw-usdc-modal/components/PrepareTransfer.tsx index d9448025fff..0e3473018ed 100644 --- a/packages/web/src/components/withdraw-usdc-modal/components/PrepareTransfer.tsx +++ b/packages/web/src/components/withdraw-usdc-modal/components/PrepareTransfer.tsx @@ -4,13 +4,17 @@ import { CoinflowWithdrawState, withdrawUSDCSelectors, WithdrawUSDCModalPages, - useWithdrawUSDCModal + useWithdrawUSDCModal, + useCoinflowWithdrawModal } from '@audius/common' import { Flex, Text } from '@audius/harmony' +import { useField } from 'formik' import { useSelector } from 'react-redux' import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' +import { WithdrawFormValues, AMOUNT } from '../types' + import styles from './PrepareTransfer.module.css' const { getCoinflowState } = withdrawUSDCSelectors @@ -23,13 +27,17 @@ const messages = { export const PrepareTransfer = () => { const coinflowState = useSelector(getCoinflowState) + const { onOpen } = useCoinflowWithdrawModal() const { setData } = useWithdrawUSDCModal() + const [{ value: amountCents }] = + useField(AMOUNT) useEffect(() => { if (coinflowState === CoinflowWithdrawState.READY_FOR_WITHDRAWAL) { setData({ page: WithdrawUSDCModalPages.COINFLOW_TRANSFER }) + onOpen({ amount: amountCents / 100.0 }) } - }, [coinflowState, setData]) + }, [coinflowState, setData, onOpen, amountCents]) return ( + >((action: any) => { + return ( + action.type === buyUSDCActions.recoveryStatusChanged.type && + (action?.payload?.status === Status.SUCCESS || + action?.payload.status === Status.ERROR) + ) + }) + yield* put(cleanupWithdrawUSDC()) + yield* put(closeWithdrawUSDCModal()) + // Buy USDC recovery already logs to sentry and makes an analytics event + // so add some logs to help discern which flow the recovery was triggered + // from and help aid in debugging should this ever hit. + if (action.payload.status === Status.ERROR) { + // Breadcrumb hint: + console.warn( + 'Failed to transfer funds back from root wallet:', + rootSolanaAccount.publicKey.toBase58() + ) + // Console error for sentry issue + console.error('Failed to recover funds from Coinflow Withdraw') + } } } catch (e: unknown) { const error = e as Error