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} + + + + +