Skip to content
This repository has been archived by the owner on Oct 4, 2023. It is now read-only.

Commit

Permalink
[PAY-614][PAY-541] Add recovery flow and modal for buying $AUDIO (#2043)
Browse files Browse the repository at this point in the history
* Add recovery modal UI

* Add BuyAudioRecovery modal to state

* Add string amount to AmountObject, and 'unknown' provider for onramps

* Add libs method for getting metadata, rename args to include units, and enable libs call to create stripe session

* Refactor core BuyAudio flow to have reusable code for the recovery flow

* Remove precalculation of fees since we calculate fees to check recovery flow anyway

* Don't reset on close, but rather open using a special action

* Check for recovery on modal open, safer localstorage sanitizing

* Remove unused vars

* Move functions

* Always show success screen after recovery
  • Loading branch information
rickyrombo authored Oct 4, 2022
1 parent 31698f3 commit cad36e3
Show file tree
Hide file tree
Showing 14 changed files with 718 additions and 276 deletions.
27 changes: 13 additions & 14 deletions packages/common/src/store/ui/buy-audio/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'

import { Status } from '../../../models/Status'

import { BuyAudioStage, OnRampProvider, PurchaseInfoErrorType } from './types'

type AmountObject = {
amount: number
uiAmount: number
uiAmountString: string
}
import {
AmountObject,
BuyAudioStage,
OnRampProvider,
PurchaseInfoErrorType
} from './types'

type PurchaseInfo = {
isError: false
Expand Down Expand Up @@ -90,9 +89,6 @@ const slice = createSlice({
}
state.purchaseInfoStatus = Status.ERROR
},
precalculateSwapFees: () => {
// Triggers a saga to calculate and cache swap fees
},
cacheAssociatedTokenAccount: (
state,
{
Expand All @@ -112,9 +108,13 @@ const slice = createSlice({
clearFeesCache: (state) => {
state.feesCache = initialState.feesCache
},
restart: (state) => {
startBuyAudioFlow: (
state,
action: PayloadAction<{ provider: OnRampProvider }>
) => {
state.stage = BuyAudioStage.START
state.error = undefined
state.provider = action.payload.provider
},
onRampOpened: (state, _action: PayloadAction<PurchaseInfo>) => {
state.stage = BuyAudioStage.PURCHASING
Expand Down Expand Up @@ -153,15 +153,14 @@ export const {
cacheAssociatedTokenAccount,
cacheTransactionFees,
clearFeesCache,
restart,
startBuyAudioFlow,
onRampOpened,
onRampSucceeded,
onRampCanceled,
swapStarted,
swapCompleted,
transferStarted,
transferCompleted,
precalculateSwapFees
transferCompleted
} = slice.actions

export default slice.reducer
Expand Down
10 changes: 9 additions & 1 deletion packages/common/src/store/ui/buy-audio/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export type JupiterTokenListing = {

export enum OnRampProvider {
COINBASE = 'coinbase',
STRIPE = 'stripe'
STRIPE = 'stripe',
UNKNOWN = 'unknown'
}

export type JupiterTokenSymbol = keyof typeof TOKEN_LISTING_MAP
Expand All @@ -36,3 +37,10 @@ export enum BuyAudioStage {
TRANSFERRING = 'TRANSFERRING',
FINISH = 'FINISH'
}

export type AmountObject = {
amount: number
amountString: string
uiAmount: number
uiAmountString: string
}
1 change: 1 addition & 0 deletions packages/common/src/store/ui/modals/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const initialState: ModalsState = {
DeletePlaylistConfirmation: false,
FeatureFlagOverride: false,
BuyAudio: false,
BuyAudioRecovery: false,
TransactionDetails: false,
VipDiscord: false,
StripeOnRamp: false
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/store/ui/modals/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type Modals =
| 'DeletePlaylistConfirmation'
| 'FeatureFlagOverride'
| 'BuyAudio'
| 'BuyAudioRecovery'
| 'TransactionDetails'
| 'VipDiscord'
| 'StripeOnRamp'
Expand Down
5 changes: 4 additions & 1 deletion packages/common/src/store/ui/transaction-details/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ export type TransactionDetails =
| {
signature: string
transactionType: TransactionType.PURCHASE
method: TransactionMethod.COINBASE | TransactionMethod.STRIPE
method:
| TransactionMethod.COINBASE
| TransactionMethod.STRIPE
| TransactionMethod.RECEIVE
date: string
change: StringAudio
balance: StringAudio
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/utils/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export const convertJSBIToAmountObject = (amount: JSBI, decimals: number) => {
: quotient.toString()
return {
amount: JSBI.toNumber(amount),
amountString: JSBI.toString(),
uiAmount: JSBI.toNumber(amount) / 10 ** decimals,
uiAmountString
}
Expand Down
9 changes: 9 additions & 0 deletions packages/web/src/assets/img/iconRaisedHand.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 2 additions & 12 deletions packages/web/src/components/buy-audio-modal/BuyAudioModal.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { useCallback } from 'react'

import {
buyAudioActions,
buyAudioSelectors,
BuyAudioStage
} from '@audius/common'
import { buyAudioSelectors, BuyAudioStage } from '@audius/common'
import {
Modal,
ModalContentPages,
ModalHeader,
ModalTitle
} from '@audius/stems'
import { useDispatch, useSelector } from 'react-redux'
import { useSelector } from 'react-redux'

import IconGoldBadgeSrc from 'assets/img/tokenBadgeGold40@2x.png'
import { useModalState } from 'common/hooks/useModalState'
Expand Down Expand Up @@ -54,7 +50,6 @@ const stageToPage = (stage: BuyAudioStage) => {
}

export const BuyAudioModal = () => {
const dispatch = useDispatch()
const [isOpen, setIsOpen] = useModalState('BuyAudio')
const stage = useSelector(getBuyAudioFlowStage)
const provider = useSelector(getBuyAudioProvider)
Expand All @@ -66,10 +61,6 @@ export const BuyAudioModal = () => {
setIsOpen(false)
}, [setIsOpen])

const handleClosed = useCallback(
() => dispatch(buyAudioActions.restart()),
[dispatch]
)
if (provider === undefined && isOpen) {
console.error('BuyAudio modal opened without a provider. Aborting...')
return null
Expand All @@ -79,7 +70,6 @@ export const BuyAudioModal = () => {
<Modal
isOpen={isOpen && provider !== undefined}
onClose={handleClose}
onClosed={handleClosed}
bodyClassName={styles.modal}
dismissOnClickOutside={!inProgress || error}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.content > div {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}

.spinner {
width: 24px;
height: 24px;
}

.helpText {
white-space: pre-line;
line-height: 150%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useCallback } from 'react'

import { Modal, ModalContent, ModalHeader, ModalTitle } from '@audius/stems'

import { ReactComponent as IconRaisedHand } from 'assets/img/iconRaisedHand.svg'
import { useModalState } from 'common/hooks/useModalState'
import LoadingSpinner from 'components/loading-spinner/LoadingSpinner'

import styles from './BuyAudioRecoveryModal.module.css'

const messages = {
holdOn: 'Hold On',
helpText:
'Your purchase of $AUDIO was interrupted.\nGive us a moment while we finish things up.'
}

export const BuyAudioRecoveryModal = () => {
const [isOpen, setIsOpen] = useModalState('BuyAudioRecovery')
const handleClose = useCallback(() => {
setIsOpen(false)
}, [setIsOpen])
return (
<Modal isOpen={isOpen} onClose={handleClose}>
<ModalHeader onClose={handleClose}>
<ModalTitle title={messages.holdOn} icon={<IconRaisedHand />} />
</ModalHeader>
<ModalContent className={styles.content}>
<LoadingSpinner className={styles.spinner} />
<div className={styles.helpText}>{messages.helpText}</div>
</ModalContent>
</Modal>
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect } from 'react'
import { useCallback } from 'react'

import {
Nullable,
Expand Down Expand Up @@ -31,7 +31,7 @@ const { getHasAssociatedWallets } = tokenDashboardPageSelectors
const { pressReceive, pressSend, pressConnectWallets } =
tokenDashboardPageActions
const { getAccountBalance, getAccountTotalBalance } = walletSelectors
const { precalculateSwapFees, setProvider } = buyAudioActions
const { startBuyAudioFlow } = buyAudioActions

const messages = {
receiveLabel: 'Receive',
Expand Down Expand Up @@ -138,24 +138,17 @@ export const WalletManagementTile = () => {
useSelector(getAccountTotalBalance) ?? null
const hasMultipleWallets = useSelector(getHasAssociatedWallets)
const [, setOpen] = useModalState('AudioBreakdown')
const [, setBuyAudioModalOpen] = useModalState('BuyAudio')

const onClickOpen = useCallback(() => {
setOpen(true)
}, [setOpen])

const onBuyWithCoinbaseClicked = useCallback(() => {
dispatch(setProvider({ provider: OnRampProvider.COINBASE }))
setBuyAudioModalOpen(true)
}, [dispatch, setBuyAudioModalOpen])
dispatch(startBuyAudioFlow({ provider: OnRampProvider.COINBASE }))
}, [dispatch])

const onBuyWithStripeClicked = useCallback(() => {
dispatch(setProvider({ provider: OnRampProvider.STRIPE }))
setBuyAudioModalOpen(true)
}, [dispatch, setBuyAudioModalOpen])

useEffect(() => {
dispatch(precalculateSwapFees())
dispatch(startBuyAudioFlow({ provider: OnRampProvider.STRIPE }))
}, [dispatch])

return (
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/pages/modals/Modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import AddToPlaylistModal from 'components/add-to-playlist/desktop/AddToPlaylist
import AppCTAModal from 'components/app-cta-modal/AppCTAModal'
import BrowserPushConfirmationModal from 'components/browser-push-confirmation-modal/BrowserPushConfirmationModal'
import { BuyAudioModal } from 'components/buy-audio-modal/BuyAudioModal'
import { BuyAudioRecoveryModal } from 'components/buy-audio-modal/BuyAudioRecoveryModal'
import CollectibleDetailsModal from 'components/collectibles/components/CollectibleDetailsModal'
import DeletePlaylistConfirmationModal from 'components/delete-playlist-confirmation-modal/DeletePlaylistConfirmationModal'
import EditFolderModal from 'components/edit-folder-modal/EditFolderModal'
Expand Down Expand Up @@ -89,6 +90,7 @@ const Modals = () => {
<BuyAudioModal />
<TransactionDetailsModal />
<StripeOnRampModal />
<BuyAudioRecoveryModal />
</>
)
}
Expand Down
33 changes: 19 additions & 14 deletions packages/web/src/services/audius-backend/BuyAudio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,12 @@ export const getAudioAccountInfo = async ({
export const pollForAudioBalanceChange = async ({
tokenAccount,
initialBalance,
retryDelay = DEFAULT_RETRY_DELAY,
retryDelayMs = DEFAULT_RETRY_DELAY,
maxRetryCount = DEFAULT_MAX_RETRY_COUNT
}: {
tokenAccount: PublicKey
initialBalance?: u64
retryDelay?: number
retryDelayMs?: number
maxRetryCount?: number
}) => {
let retries = 0
Expand All @@ -146,7 +146,7 @@ export const pollForAudioBalanceChange = async ({
`Polling AUDIO balance (${initialBalance} === ${tokenAccountInfo.amount}) [${retries}/${maxRetryCount}]`
)
}
await delay(retryDelay)
await delay(retryDelayMs)
tokenAccountInfo = await getAudioAccountInfo({ tokenAccount })
}
if (
Expand All @@ -167,12 +167,12 @@ export const pollForAudioBalanceChange = async ({
export const pollForSolBalanceChange = async ({
rootAccount,
initialBalance,
retryDelay = DEFAULT_RETRY_DELAY,
retryDelayMs = DEFAULT_RETRY_DELAY,
maxRetryCount = DEFAULT_MAX_RETRY_COUNT
}: {
rootAccount: PublicKey
initialBalance?: number
retryDelay?: number
retryDelayMs?: number
maxRetryCount?: number
}) => {
const connection = await getSolanaConnection()
Expand All @@ -187,7 +187,7 @@ export const pollForSolBalanceChange = async ({
balance / LAMPORTS_PER_SOL
}) [${retries}/${maxRetryCount}]`
)
await delay(retryDelay)
await delay(retryDelayMs)
balance = await connection.getBalance(rootAccount, 'finalized')
}
if (balance !== initialBalance) {
Expand Down Expand Up @@ -249,7 +249,14 @@ export const saveUserBankTransactionMetadata = async (
data: InAppAudioPurchaseMetadata
) => {
await waitForLibsInit()
// return await libs().identityService!.saveUserBankTransactionMetadata(data)
return await libs().identityService!.saveUserBankTransactionMetadata(data)
}

export const getUserBankTransactionMetadata = async (transactionId: string) => {
await waitForLibsInit()
return await libs().identityService!.getUserBankTransactionMetadata(
transactionId
)
}

export const createStripeSession = async ({
Expand All @@ -259,11 +266,9 @@ export const createStripeSession = async ({
destinationWallet: string
amount: string
}) => {
throw new Error('createStripeSession: Not implemented')
// TODO: Update libs with this call
// await waitForLibsInit()
// return await libs().identityService!.createStripeSession({
// destinationWallet,
// amount
// })
await waitForLibsInit()
return await libs().identityService!.createStripeSession({
destinationWallet,
amount
})
}
Loading

0 comments on commit cad36e3

Please sign in to comment.