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

Commit

Permalink
[PAY-600][PAY-599][PAY-607][PAY-617] RemoteConfig for Buy Audio and b…
Browse files Browse the repository at this point in the history
…etter transactiondetails link (#1861)

* [PAY-600] Make preset amounts configurable

* [PAY-599] Make polling configurable

* [PAY-607] Fix icon buttons on transaction details modal

* Remove unused

* [PAY-617] Don't let the modal dismiss when in progress
  • Loading branch information
rickyrombo committed Sep 7, 2022
1 parent 35393b5 commit 3f32abc
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 56 deletions.
7 changes: 5 additions & 2 deletions packages/common/src/services/remote-config/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ export const remoteConfigIntDefaults: { [key in IntKeys]: number | null } = {
[IntKeys.CHALLENGE_CLAIM_COMPLETION_POLL_FREQUENCY_MS]: 1000,
[IntKeys.CHALLENGE_CLAIM_COMPLETION_POLL_TIMEOUT_MS]: 10000,
[IntKeys.MIN_AUDIO_PURCHASE_AMOUNT]: 5,
[IntKeys.MAX_AUDIO_PURCHASE_AMOUNT]: 999
[IntKeys.MAX_AUDIO_PURCHASE_AMOUNT]: 999,
[IntKeys.BUY_AUDIO_WALLET_POLL_DELAY_MS]: 1000,
[IntKeys.BUY_AUDIO_WALLET_POLL_MAX_RETRIES]: 120
}

export const remoteConfigStringDefaults: {
Expand Down Expand Up @@ -60,7 +62,8 @@ export const remoteConfigStringDefaults: {
[StringKeys.ORACLE_ETH_ADDRESS]: null,
[StringKeys.ORACLE_ENDPOINT]: null,
[StringKeys.REWARDS_ATTESTATION_ENDPOINTS]: null,
[StringKeys.MIN_APP_VERSION]: '1.0.0'
[StringKeys.MIN_APP_VERSION]: '1.0.0',
[StringKeys.BUY_AUDIO_PRESET_AMOUNTS]: '5,10,25,50,100'
}
export const remoteConfigDoubleDefaults: {
[key in DoubleKeys]: number | null
Expand Down
17 changes: 15 additions & 2 deletions packages/common/src/services/remote-config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,17 @@ export enum IntKeys {
/**
* Maximum AUDIO required to purchase in the BuyAudio modal
*/
MAX_AUDIO_PURCHASE_AMOUNT = 'MAX_AUDIO_PURCHASE_AMOUNT'
MAX_AUDIO_PURCHASE_AMOUNT = 'MAX_AUDIO_PURCHASE_AMOUNT',

/**
* The time to delay between polls of the user wallet when performing a purchase of $AUDIO
*/
BUY_AUDIO_WALLET_POLL_DELAY_MS = 'BUY_AUDIO_WALLET_POLL_DELAY_MS',

/**
* The maximum amount of times to poll the user wallet before giving up on an $AUDIO purchase
*/
BUY_AUDIO_WALLET_POLL_MAX_RETRIES = 'BUY_AUDIO_WALLET_POLL_MAX_RETRIES'
}

export enum BooleanKeys {
Expand Down Expand Up @@ -268,7 +278,10 @@ export enum StringKeys {
REWARDS_ATTESTATION_ENDPOINTS = 'REWARDS_ATTESTATION_ENDPOINTS',

/** Minimum required version for the app */
MIN_APP_VERSION = 'MIN_APP_VERSION'
MIN_APP_VERSION = 'MIN_APP_VERSION',

/** Preset amounts for the Buy Audio modal */
BUY_AUDIO_PRESET_AMOUNTS = 'BUY_AUDIO_PRESET_AMOUNTS'
}

export type AllRemoteConfigKeys =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const BuyAudioModal = () => {
const [isOpen, setIsOpen] = useModalState('BuyAudio')
const stage = useSelector(getBuyAudioFlowStage)
const currentPage = stageToPage(stage)
const inProgress = currentPage === 1

const handleClose = useCallback(() => {
setIsOpen(false)
Expand All @@ -73,8 +74,9 @@ export const BuyAudioModal = () => {
onClose={handleClose}
onClosed={handleClosed}
bodyClassName={styles.modal}
dismissOnClickOutside={!inProgress}
>
<ModalHeader onClose={handleClose}>
<ModalHeader onClose={handleClose} showDismissButton={!inProgress}>
<ModalTitle title={messages.buyAudio} icon={<IconGoldBadge />} />
</ModalHeader>
<ModalContentPages
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useCallback } from 'react'
import { useCallback, useMemo } from 'react'

import { buyAudioActions, buyAudioSelectors } from '@audius/common'
import { buyAudioActions, buyAudioSelectors, StringKeys } from '@audius/common'
import { useDispatch, useSelector } from 'react-redux'

import { useRemoteVar } from 'hooks/useRemoteConfig'

import styles from './AmountInputPage.module.css'
import { AudioAmountPicker } from './AudioAmountPicker'
import { CoinbaseBuyAudioButton } from './CoinbaseBuyAudioButton'
Expand All @@ -18,6 +20,7 @@ const messages = {
export const AmountInputPage = () => {
const dispatch = useDispatch()
const purchaseInfo = useSelector(getAudioPurchaseInfo)
const presetAmountsConfig = useRemoteVar(StringKeys.BUY_AUDIO_PRESET_AMOUNTS)

const handleAmountChange = useCallback(
(amount) => {
Expand All @@ -33,10 +36,14 @@ export const AmountInputPage = () => {
[dispatch]
)

const presetAmounts = useMemo(() => {
return presetAmountsConfig.split(',').map((amount) => amount.trim())
}, [presetAmountsConfig])

return (
<div className={styles.inputPage}>
<AudioAmountPicker
presetAmounts={['5', '10', '25', '50', '100']}
presetAmounts={presetAmounts}
onAmountChanged={handleAmountChange}
/>
<PurchaseQuote />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
color: var(--neutral-light-4);
}

.blockHeader svg path {
fill: var(--neutral-light-4);
}

.blockContent {
display: flex;
align-items: center;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
.iconButton {
display: flex;
.link {
transition: color var(--quick);
display: inline-flex;
align-items: center;
gap: 4px;
color: inherit;
}
.iconButton svg {
width: 16px;
height: 16px;
.link svg {
width: 1em;
height: 1em;
}
.iconButton path {
.link svg path {
transition: fill var(--quick);
fill: var(--neutral-light-4);
}
.link:hover {
color: var(--secondary);
}
.link:hover svg path {
fill: var(--secondary);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { InAppAudioPurchaseMetadata, formatNumberString } from '@audius/common'
import { IconButton } from '@audius/stems'

import { ReactComponent as IconExternalLink } from 'assets/img/iconExternalLink.svg'
import {
Expand Down Expand Up @@ -31,35 +30,31 @@ export const TransactionPurchaseMetadata = ({
</Block>
<Block
header={
<>
{messages.purchased}
<IconButton
className={styles.iconButton}
icon={<IconExternalLink />}
title={messages.viewOnExplorer}
aria-label={messages.viewOnExplorer}
href={`https://explorer.solana.com/tx/${metadata.purchaseTransactionId}`}
target='_blank'
/>
</>
<a
className={styles.link}
href={`https://explorer.solana.com/tx/${metadata.purchaseTransactionId}`}
target='_blank'
title={messages.viewOnExplorer}
rel='noreferrer'
>
{messages.purchased} <IconExternalLink />
</a>
}
>
<IconSOL />
{formatNumberString(metadata.sol, { maxDecimals: 2 })}
</Block>
<Block
header={
<>
{messages.convertedTo}
<IconButton
className={styles.iconButton}
icon={<IconExternalLink />}
title={messages.viewOnExplorer}
aria-label={messages.viewOnExplorer}
href={`https://explorer.solana.com/tx/${metadata.swapTransactionId}`}
target='_blank'
/>
</>
<a
className={styles.link}
href={`https://explorer.solana.com/tx/${metadata.swapTransactionId}`}
target='_blank'
title={messages.viewOnExplorer}
rel='noreferrer'
>
{messages.convertedTo} <IconExternalLink />
</a>
}
>
<IconAUDIO />
Expand Down
36 changes: 19 additions & 17 deletions packages/web/src/services/audius-backend/BuyAudio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,8 @@ import { waitForLibsInit } from 'services/audius-backend/eagerLoadUtils'
// @ts-ignore
const libs = (): AudiusLibs => window.audiusLibs

const TOKEN_ACCOUNT_POLL_MS = 5000
const MAX_TOKEN_ACCOUNT_POLL_COUNT = 20

const SOL_ACCOUNT_POLL_MS = 5000
const MAX_SOL_ACCOUNT_POLL_COUNT = 20
const DEFAULT_RETRY_DELAY = 1000
const DEFAULT_MAX_RETRY_COUNT = 120

const ROOT_ACCOUNT_SIZE = 0 // Root account takes 0 bytes, but still pays rent!
const ATA_SIZE = 165 // Size allocated for an associated token account
Expand Down Expand Up @@ -121,31 +118,35 @@ export const getAudioAccountInfo = async ({

export const pollForAudioBalanceChange = async ({
tokenAccount,
initialBalance
initialBalance,
retryDelay = DEFAULT_RETRY_DELAY,
maxRetryCount = DEFAULT_MAX_RETRY_COUNT
}: {
tokenAccount: PublicKey
initialBalance?: u64
retryDelay?: number
maxRetryCount?: number
}) => {
let retries = 0
let tokenAccountInfo = await getAudioAccountInfo({ tokenAccount })
while (
(!tokenAccountInfo ||
initialBalance === undefined ||
tokenAccountInfo.amount.eq(initialBalance)) &&
retries++ <= MAX_TOKEN_ACCOUNT_POLL_COUNT
retries++ < maxRetryCount
) {
if (!tokenAccountInfo) {
console.debug(
`AUDIO account not found. Retrying... ${retries}/${MAX_TOKEN_ACCOUNT_POLL_COUNT}`
`AUDIO account not found. Retrying... ${retries}/${maxRetryCount}`
)
} else if (initialBalance === undefined) {
initialBalance = tokenAccountInfo.amount
} else if (tokenAccountInfo.amount.eq(initialBalance)) {
console.debug(
`Polling AUDIO balance (${initialBalance} === ${tokenAccountInfo.amount}) [${retries}/${MAX_TOKEN_ACCOUNT_POLL_COUNT}]`
`Polling AUDIO balance (${initialBalance} === ${tokenAccountInfo.amount}) [${retries}/${maxRetryCount}]`
)
}
await delay(TOKEN_ACCOUNT_POLL_MS)
await delay(retryDelay)
tokenAccountInfo = await getAudioAccountInfo({ tokenAccount })
}
if (
Expand All @@ -165,27 +166,28 @@ export const pollForAudioBalanceChange = async ({

export const pollForSolBalanceChange = async ({
rootAccount,
initialBalance
initialBalance,
retryDelay = DEFAULT_RETRY_DELAY,
maxRetryCount = DEFAULT_MAX_RETRY_COUNT
}: {
rootAccount: PublicKey
initialBalance?: number
retryDelay?: number
maxRetryCount?: number
}) => {
const connection = await getSolanaConnection()
let balance = await connection.getBalance(rootAccount, 'finalized')
if (initialBalance === undefined) {
initialBalance = balance
}
let retries = 0
while (
balance === initialBalance &&
retries++ <= MAX_SOL_ACCOUNT_POLL_COUNT
) {
while (balance === initialBalance && retries++ < maxRetryCount) {
console.debug(
`Polling SOL balance (${initialBalance / LAMPORTS_PER_SOL} === ${
balance / LAMPORTS_PER_SOL
}) [${retries}/${MAX_SOL_ACCOUNT_POLL_COUNT}]`
}) [${retries}/${maxRetryCount}]`
)
await delay(SOL_ACCOUNT_POLL_MS)
await delay(retryDelay)
balance = await connection.getBalance(rootAccount, 'finalized')
}
if (balance !== initialBalance) {
Expand Down
18 changes: 16 additions & 2 deletions packages/web/src/store/application/ui/buy-audio/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,16 @@ function* startBuyAudioFlow({
feePayerKeypairs: [rootAccount],
skipPreflight: true
})
const remoteConfigInstance = yield* getContext('remoteConfigInstance')
yield* call(remoteConfigInstance.waitForRemoteConfig)
const retryDelay =
remoteConfigInstance.getRemoteVar(
IntKeys.BUY_AUDIO_WALLET_POLL_DELAY_MS
) ?? undefined
const maxRetryCount =
remoteConfigInstance.getRemoteVar(
IntKeys.BUY_AUDIO_WALLET_POLL_MAX_RETRIES
) ?? undefined

// Ensure userbank is created
yield* fork(function* () {
Expand Down Expand Up @@ -484,7 +494,9 @@ function* startBuyAudioFlow({
// Wait for the SOL funds to come through
const newBalance = yield* call(pollForSolBalanceChange, {
rootAccount: rootAccount.publicKey,
initialBalance
initialBalance,
retryDelay,
maxRetryCount
})

// Get the purchase transaction
Expand Down Expand Up @@ -579,7 +591,9 @@ function* startBuyAudioFlow({
// Wait for AUDIO funds to come through
const transferAmount = yield* call(pollForAudioBalanceChange, {
tokenAccount,
initialBalance: beforeSwapAudioBalance
initialBalance: beforeSwapAudioBalance,
retryDelay,
maxRetryCount
})

// Transfer AUDIO to userbank
Expand Down

0 comments on commit 3f32abc

Please sign in to comment.