diff --git a/packages/common/src/hooks/useUSDCBalance.ts b/packages/common/src/hooks/useUSDCBalance.ts
index 257f7b89c10..364fae7032c 100644
--- a/packages/common/src/hooks/useUSDCBalance.ts
+++ b/packages/common/src/hooks/useUSDCBalance.ts
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react'
import BN from 'bn.js'
import { useDispatch, useSelector } from 'react-redux'
+import { useInterval } from 'react-use'
import { Status } from 'models/Status'
import { BNUSDC, StringUSDC } from 'models/Wallet'
@@ -19,7 +20,13 @@ import { setUSDCBalance } from 'store/wallet/slice'
* stale balance. If absolute latest balance value is needed, defer use until
* Status.SUCCESS.
*/
-export const useUSDCBalance = () => {
+export const useUSDCBalance = ({
+ isPolling,
+ pollingInterval = 1000
+}: {
+ isPolling?: boolean
+ pollingInterval?: number
+} = {}) => {
const { audiusBackend } = useAppContext()
const dispatch = useDispatch()
@@ -51,5 +58,11 @@ export const useUSDCBalance = () => {
refresh()
}, [refresh])
+ useInterval(() => {
+ if (isPolling) {
+ refresh()
+ }
+ }, pollingInterval)
+
return { balanceStatus, recoveryStatus, data, refresh }
}
diff --git a/packages/mobile/src/app/Drawers.tsx b/packages/mobile/src/app/Drawers.tsx
index 5201bdeb7b0..8f2572f5b95 100644
--- a/packages/mobile/src/app/Drawers.tsx
+++ b/packages/mobile/src/app/Drawers.tsx
@@ -37,6 +37,7 @@ import { StripeOnrampDrawer } from 'app/components/stripe-onramp-drawer'
import { SupportersInfoDrawer } from 'app/components/supporters-info-drawer'
import { TransferAudioMobileDrawer } from 'app/components/transfer-audio-mobile-drawer'
import { TrendingRewardsDrawer } from 'app/components/trending-rewards-drawer'
+import { USDCManualTransferDrawer } from 'app/components/usdc-manual-transfer-drawer'
import { TrendingFilterDrawer } from 'app/screens/trending-screen'
import { useDrawerState } from '../components/drawer'
@@ -134,7 +135,8 @@ const nativeDrawersMap: { [DrawerName in Drawer]?: ComponentType } = {
BlockMessages: BlockMessagesDrawer,
DeleteChat: DeleteChatDrawer,
SupportersInfo: SupportersInfoDrawer,
- PremiumTrackPurchase: PremiumTrackPurchaseDrawer
+ PremiumTrackPurchase: PremiumTrackPurchaseDrawer,
+ USDCManualTransfer: USDCManualTransferDrawer
}
const commonDrawers = Object.entries(commonDrawersMap) as [
diff --git a/packages/mobile/src/assets/images/iconError.svg b/packages/mobile/src/assets/images/iconError.svg
index 4f5469ba9c2..78edc11fbdb 100644
--- a/packages/mobile/src/assets/images/iconError.svg
+++ b/packages/mobile/src/assets/images/iconError.svg
@@ -1,3 +1,3 @@
diff --git a/packages/mobile/src/assets/images/logoUSDC.svg b/packages/mobile/src/assets/images/logoUSDC.svg
new file mode 100644
index 00000000000..9b64f9c9026
--- /dev/null
+++ b/packages/mobile/src/assets/images/logoUSDC.svg
@@ -0,0 +1,10 @@
+
diff --git a/packages/mobile/src/components/core/AddressTile.tsx b/packages/mobile/src/components/core/AddressTile.tsx
new file mode 100644
index 00000000000..b1a97dede56
--- /dev/null
+++ b/packages/mobile/src/components/core/AddressTile.tsx
@@ -0,0 +1,81 @@
+import { useCallback, type ReactNode } from 'react'
+
+import Clipboard from '@react-native-clipboard/clipboard'
+import { View } from 'react-native'
+import { TouchableOpacity } from 'react-native-gesture-handler'
+
+import IconCopy2 from 'app/assets/images/iconCopy2.svg'
+import { Text } from 'app/components/core'
+import { useToast } from 'app/hooks/useToast'
+import { flexRowCentered, makeStyles } from 'app/styles'
+import { spacing } from 'app/styles/spacing'
+import { useColor } from 'app/utils/theme'
+
+const messages = {
+ copied: 'Copied to Clipboard!'
+}
+
+const useStyles = makeStyles(({ spacing, palette, typography }) => ({
+ addressContainer: {
+ ...flexRowCentered(),
+ borderWidth: 1,
+ borderColor: palette.borderDefault,
+ borderRadius: spacing(1),
+ backgroundColor: palette.backgroundSurface
+ },
+ rightContainer: {
+ paddingVertical: spacing(4),
+ paddingHorizontal: spacing(6)
+ },
+ middleContainer: {
+ paddingHorizontal: spacing(6),
+ paddingVertical: spacing(4),
+ borderLeftWidth: 1,
+ borderRightWidth: 1,
+ borderColor: palette.borderDefault,
+ flexShrink: 1
+ },
+ leftContainer: {
+ paddingVertical: spacing(4),
+ paddingHorizontal: spacing(6)
+ }
+}))
+
+type AddressTileProps = {
+ address: string
+ left?: ReactNode
+ right?: ReactNode
+}
+
+export const AddressTile = ({ address, left, right }: AddressTileProps) => {
+ const styles = useStyles()
+ const { toast } = useToast()
+ const textSubdued = useColor('textIconSubdued')
+
+ const handleCopyPress = useCallback(() => {
+ Clipboard.setString(address)
+ toast({ content: messages.copied, type: 'info' })
+ }, [address, toast])
+
+ return (
+
+ {left}
+
+
+ {address}
+
+
+
+ {right ?? (
+
+
+
+ )}
+
+
+ )
+}
diff --git a/packages/mobile/src/components/drawer/NativeDrawer.tsx b/packages/mobile/src/components/drawer/NativeDrawer.tsx
index 6532ca49cb7..a614f0e4a9a 100644
--- a/packages/mobile/src/components/drawer/NativeDrawer.tsx
+++ b/packages/mobile/src/components/drawer/NativeDrawer.tsx
@@ -17,7 +17,12 @@ type NativeDrawerProps = SetOptional & {
* opening and closing.
*/
export const NativeDrawer = (props: NativeDrawerProps) => {
- const { drawerName, onClose: onCloseProp, ...other } = props
+ const {
+ drawerName,
+ onClose: onCloseProp,
+ onClosed: onClosedProp,
+ ...other
+ } = props
const { isOpen, onClose, onClosed, visibleState } = useDrawer(drawerName)
@@ -26,13 +31,18 @@ export const NativeDrawer = (props: NativeDrawerProps) => {
onClose()
}, [onCloseProp, onClose])
+ const handleClosed = useCallback(() => {
+ onClosedProp?.()
+ onClosed()
+ }, [onClosed, onClosedProp])
+
if (visibleState === false) return null
return (
)
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 986b10ec1a5..7b256000b56 100644
--- a/packages/mobile/src/components/premium-track-purchase-drawer/PremiumTrackPurchaseDrawer.tsx
+++ b/packages/mobile/src/components/premium-track-purchase-drawer/PremiumTrackPurchaseDrawer.tsx
@@ -40,6 +40,7 @@ import { useIsUSDCEnabled } from 'app/hooks/useIsUSDCEnabled'
import { useNavigation } from 'app/hooks/useNavigation'
import { useFeatureFlag, useRemoteVar } from 'app/hooks/useRemoteConfig'
import { make, track as trackEvent } from 'app/services/analytics'
+import { setVisibility } from 'app/store/drawers/slice'
import { flexRowCentered, makeStyles } from 'app/styles'
import { spacing } from 'app/styles/spacing'
import { useThemeColors } from 'app/utils/theme'
@@ -80,7 +81,8 @@ const messages = {
}
>
),
- termsOfUse: 'Terms of Use.'
+ termsOfUse: 'Terms of Use.',
+ manualTransfer: '(Advanced) Manual Crypto Transfer'
}
const useStyles = makeStyles(({ spacing, typography, palette }) => ({
@@ -145,7 +147,13 @@ const useStyles = makeStyles(({ spacing, typography, palette }) => ({
...flexRowCentered()
},
disclaimer: {
- lineHeight: 20
+ lineHeight: typography.fontSize.medium * 1.25
+ },
+ bottomSection: {
+ gap: spacing(2)
+ },
+ manualTransfer: {
+ lineHeight: typography.fontSize.medium * 1.25
}
}))
@@ -206,9 +214,10 @@ const getButtonText = (isUnlocking: boolean, amountDue: number) =>
// to the FormikContext, which can only be used in a component which is a descendant
// of the `` component
const RenderForm = ({ track }: { track: PurchasableTrackMetadata }) => {
+ const dispatch = useDispatch()
const navigation = useNavigation()
const styles = useStyles()
- const { specialLightGreen, secondary } = useThemeColors()
+ const { specialLightGreen, primary } = useThemeColors()
const presetValues = usePayExtraPresets(useRemoteVar)
const { isEnabled: isIOSUSDCPurchaseEnabled } = useFeatureFlag(
FeatureFlags.IOS_USDC_PURCHASE_ENABLED
@@ -245,6 +254,10 @@ const RenderForm = ({ track }: { track: PurchasableTrackMetadata }) => {
trackEvent(make({ eventName: Name.PURCHASE_CONTENT_TOS_CLICKED }))
}, [])
+ const handleManualTransferPress = useCallback(() => {
+ dispatch(setVisibility({ drawer: 'USDCManualTransfer', visible: true }))
+ }, [dispatch])
+
return (
<>
@@ -261,10 +274,22 @@ const RenderForm = ({ track }: { track: PurchasableTrackMetadata }) => {
disabled={isInProgress}
/>
)}
-
+
+
+ {isIOSDisabled ? null : (
+
+ {messages.manualTransfer}
+
+ )}
+
{isIOSDisabled ? (
) : isPurchaseSuccessful ? (
@@ -279,7 +304,7 @@ const RenderForm = ({ track }: { track: PurchasableTrackMetadata }) => {
{messages.disclaimer(
-
+
{messages.termsOfUse}
)}
diff --git a/packages/mobile/src/components/premium-track-purchase-drawer/hooks/usePurchaseContentFormState.ts b/packages/mobile/src/components/premium-track-purchase-drawer/hooks/usePurchaseContentFormState.ts
index 8f574de4b7a..5a872bda22b 100644
--- a/packages/mobile/src/components/premium-track-purchase-drawer/hooks/usePurchaseContentFormState.ts
+++ b/packages/mobile/src/components/premium-track-purchase-drawer/hooks/usePurchaseContentFormState.ts
@@ -17,7 +17,11 @@ export const usePurchaseContentFormState = ({ price }: { price: number }) => {
const error = useSelector(getPurchaseContentError)
const isUnlocking = !error && isContentPurchaseInProgress(stage)
- const { data: currentBalance, recoveryStatus, refresh } = useUSDCBalance()
+ const {
+ data: currentBalance,
+ recoveryStatus,
+ refresh
+ } = useUSDCBalance({ isPolling: true })
// Refresh balance on successful recovery
useEffect(() => {
diff --git a/packages/mobile/src/components/usdc-manual-transfer-drawer/USDCManualTransferDrawer.tsx b/packages/mobile/src/components/usdc-manual-transfer-drawer/USDCManualTransferDrawer.tsx
new file mode 100644
index 00000000000..a4d8e41cb68
--- /dev/null
+++ b/packages/mobile/src/components/usdc-manual-transfer-drawer/USDCManualTransferDrawer.tsx
@@ -0,0 +1,171 @@
+import { useCallback } from 'react'
+
+import Clipboard from '@react-native-clipboard/clipboard'
+import { View } from 'react-native'
+import { useDispatch } from 'react-redux'
+import { useAsync } from 'react-use'
+
+import IconError from 'app/assets/images/iconError.svg'
+import LogoUSDC from 'app/assets/images/logoUSDC.svg'
+import { Button, Text, useLink } from 'app/components/core'
+import { NativeDrawer } from 'app/components/drawer'
+import { useToast } from 'app/hooks/useToast'
+import { getUSDCUserBank } from 'app/services/buyCrypto'
+import { setVisibility } from 'app/store/drawers/slice'
+import { flexRowCentered, makeStyles } from 'app/styles'
+import { spacing } from 'app/styles/spacing'
+
+import { AddressTile } from '../core/AddressTile'
+
+const USDCLearnMore =
+ 'https://support.audius.co/help/Understanding-USDC-on-Audius'
+
+const messages = {
+ title: 'Manual Crypto Transfer',
+ explainer:
+ 'You can add funds manually by transferring USDC tokens to your Audius Wallet.\n\n\n Use caution to avoid errors and lost funds.',
+ splOnly: 'You can only send Solana based (SPL) USDC tokens to this address.',
+ copy: 'Copy Wallet Address',
+ goBack: 'Go Back',
+ learnMore: 'Learn More',
+ copied: 'Copied to Clipboard!'
+}
+
+const useStyles = makeStyles(({ spacing, palette, typography }) => ({
+ drawer: {
+ marginVertical: spacing(6),
+ marginHorizontal: spacing(4),
+ gap: spacing(6)
+ },
+ titleContainer: {
+ ...flexRowCentered(),
+ justifyContent: 'center',
+ width: '100%',
+ paddingBottom: spacing(4),
+ borderBottomWidth: 1,
+ borderBottomColor: palette.neutralLight8
+ },
+ disclaimerContainer: {
+ display: 'flex',
+ alignItems: 'flex-start',
+ flexDirection: 'row',
+ paddingHorizontal: spacing(4),
+ paddingVertical: spacing(3),
+ backgroundColor: palette.backgroundSurface2,
+ borderColor: palette.borderStrong,
+ borderWidth: 1,
+ borderRadius: spacing(2),
+ gap: spacing(4)
+ },
+ disclaimer: {
+ lineHeight: typography.fontSize.medium * 1.25
+ },
+ icon: {
+ marginTop: spacing(2)
+ },
+ buttonContainer: {
+ gap: spacing(2)
+ },
+ learnMore: {
+ textDecorationLine: 'underline'
+ },
+ explainer: {
+ textAlign: 'center',
+ lineHeight: typography.fontSize.medium * 1.25
+ },
+ splContainer: {
+ gap: spacing(3),
+ flexShrink: 1
+ },
+ shrink: {
+ flexShrink: 1
+ }
+}))
+
+export const USDCManualTransferDrawer = () => {
+ const styles = useStyles()
+ const dispatch = useDispatch()
+ const { toast } = useToast()
+ const { onPress: onPressLearnMore } = useLink(USDCLearnMore)
+
+ const { value: USDCUserBank } = useAsync(async () => {
+ const USDCUserBankPubKey = await getUSDCUserBank()
+ return USDCUserBankPubKey?.toString() ?? ''
+ })
+
+ const handleConfirmPress = useCallback(() => {
+ Clipboard.setString(USDCUserBank ?? '')
+ toast({ content: messages.copied, type: 'info' })
+ }, [USDCUserBank, toast])
+
+ const handleCancelPress = useCallback(() => {
+ dispatch(
+ setVisibility({
+ drawer: 'USDCManualTransfer',
+ visible: 'closing'
+ })
+ )
+ }, [dispatch])
+
+ const handleLearnMorePress = useCallback(() => {
+ onPressLearnMore()
+ }, [onPressLearnMore])
+
+ return (
+
+
+
+
+ {messages.title}
+
+
+ {messages.explainer}
+ }
+ />
+
+
+
+
+ {messages.splOnly}
+
+
+ {messages.learnMore}
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/mobile/src/components/usdc-manual-transfer-drawer/index.ts b/packages/mobile/src/components/usdc-manual-transfer-drawer/index.ts
new file mode 100644
index 00000000000..c93e7a9e885
--- /dev/null
+++ b/packages/mobile/src/components/usdc-manual-transfer-drawer/index.ts
@@ -0,0 +1 @@
+export { USDCManualTransferDrawer } from './USDCManualTransferDrawer'
diff --git a/packages/mobile/src/services/buyCrypto.ts b/packages/mobile/src/services/buyCrypto.ts
index 4316de6f80f..0ef64c9d645 100644
--- a/packages/mobile/src/services/buyCrypto.ts
+++ b/packages/mobile/src/services/buyCrypto.ts
@@ -1,6 +1,6 @@
import { audiusLibs, waitForLibsInit } from './libs'
-export const getUSDCUserBank = async (ethWallet: string) => {
+export const getUSDCUserBank = async (ethWallet?: string) => {
await waitForLibsInit()
return await audiusLibs?.solanaWeb3Manager?.deriveUserBank({
ethAddress: ethWallet,
diff --git a/packages/mobile/src/store/drawers/slice.ts b/packages/mobile/src/store/drawers/slice.ts
index 0c2c30fe2e7..a668ea8ba30 100644
--- a/packages/mobile/src/store/drawers/slice.ts
+++ b/packages/mobile/src/store/drawers/slice.ts
@@ -31,6 +31,7 @@ export type Drawer =
| 'PremiumTrackPurchase'
| 'StripeOnramp'
| 'OfflineListening'
+ | 'USDCManualTransfer'
export type DrawerData = {
EnablePushNotifications: undefined
@@ -72,6 +73,7 @@ export type DrawerData = {
InboxUnavailable: { userId: number; shouldOpenChat: boolean }
PremiumTrackPurchase: { trackId: ID }
StripeOnramp: { clientSecret: string }
+ USDCManualTransfer: undefined
}
export type DrawersState = { [drawer in Drawer]: boolean | 'closing' } & {
@@ -105,6 +107,7 @@ const initialState: DrawersState = {
SupportersInfo: false,
PremiumTrackPurchase: false,
StripeOnramp: false,
+ USDCManualTransfer: false,
data: {}
}
diff --git a/packages/mobile/src/utils/theme.ts b/packages/mobile/src/utils/theme.ts
index b4fda3ca7c0..6c70bc99201 100644
--- a/packages/mobile/src/utils/theme.ts
+++ b/packages/mobile/src/utils/theme.ts
@@ -72,7 +72,9 @@ export const defaultTheme = {
skeletonHighlight: '#F2F2F4',
statTileText: '#C675FF',
progressBackground: '#D9D9D9',
- accentBlue: '#1ba1f1'
+ accentBlue: '#1ba1f1',
+ textIconSubdued: '#C2C0CC',
+ focus: '#7E1BCC'
}
export const darkTheme = {
@@ -136,7 +138,9 @@ export const darkTheme = {
skeletonHighlight: '#3F415B',
statTileText: '#C675FF',
progressBackground: '#D9D9D9',
- accentBlue: '#1ba1f1'
+ accentBlue: '#1ba1f1',
+ textIconSubdued: '#777C96',
+ focus: '#9147CC'
}
export const matrixTheme = {
@@ -192,7 +196,9 @@ export const matrixTheme = {
skeletonHighlight: '#1C5610',
statTileText: '#184F17',
progressBackground: '#D9D9D9',
- accentBlue: '#1ba1f1'
+ accentBlue: '#1ba1f1',
+ textIconSubdued: '#1D660E',
+ focus: '#184F17'
}
export type ThemeColors = {
@@ -257,6 +263,8 @@ export type ThemeColors = {
statTileText: string
progressBackground: string
accentBlue: string
+ textIconSubdued: string
+ focus: string
}
const themeColorsByThemeVariant: Record<