Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions packages/common/src/services/audius-backend/AudiusBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
} from '../../store'
import { getErrorMessage, uuid, Maybe, Nullable } from '../../utils'

import { MintName } from './solana'
import { MintName, createUserBankIfNeeded } from './solana'

type DisplayEncoding = 'utf8' | 'hex'
type PhantomEvent = 'disconnect' | 'connect' | 'accountChanged'
Expand Down Expand Up @@ -914,13 +914,13 @@ export const audiusBackend = ({
let tokenAccountAddress: PublicKey

if (recipientEthAddress) {
// When sending to a user, derive their user-bank ATA for this Solana mint
// The user-bank is a PDA derived from their Ethereum address and the mint
tokenAccountAddress =
await sdk.services.claimableTokensClient.deriveUserBank({
ethWallet: recipientEthAddress,
mint
})
// When sending to a user, ensure their user-bank account exists
// This will create it if needed (in a separate transaction)
tokenAccountAddress = await createUserBankIfNeeded(sdk, {
ethAddress: recipientEthAddress,
mint: mint as any,
recordAnalytics: () => {} // Analytics handled elsewhere
})
} else {
// When sending to a Solana wallet address directly, use regular ATA logic
tokenAccountAddress = await getOrCreateAssociatedTokenAccount({
Expand Down
16 changes: 15 additions & 1 deletion packages/mobile/src/components/core/Screen/Screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export type ScreenProps = {
variant?: ScreenVariant
as?: ComponentType<ViewProps>
header?: () => ReactElement
// Callback called when user presses back button
onBack?: () => void
}

export const Screen = (props: ScreenProps) => {
Expand All @@ -73,14 +75,26 @@ export const Screen = (props: ScreenProps) => {
variant = 'primary',
style,
as: RootComponent = View,
header
header,
onBack
} = props
const palette = useThemePalette()
const styles = useStyles()
const backgroundColor = getBackgroundColor(variant, palette)
const navigation = useNavigation()
const isSecondary = variant === 'secondary' || variant === 'white'

// Handle back button press
useEffect(() => {
if (!onBack) return

const unsubscribe = navigation.addListener('beforeRemove', () => {
onBack()
})

return unsubscribe
}, [navigation, onBack])

// Record screen view
useEffect(() => {
if (url) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ export const SendTokensDrawer = () => {
})
}

const handleUserChange = (user: User | null) => {
// Update state when user is selected/reselected
setState((prev) => ({
...prev,
selectedUser: user,
destinationAddress: user?.spl_wallet ?? prev.destinationAddress
}))
}

const handleConfirm = async () => {
setState((prev) => ({ ...prev, step: 'progress' }))
setError('')
Expand All @@ -97,8 +106,11 @@ export const SendTokensDrawer = () => {
errorString.includes('0x3') ||
errorString.includes('custom program error: 0x3')
) {
// This error should no longer occur as we now automatically create
// user-bank accounts in the transaction. If it still occurs, it's likely
// a different issue or the account creation failed.
errorMessage =
'The recipient wallet does not have a token account for this coin. They may need to receive tokens of this type first, or the transaction needs to create the account automatically.'
'Failed to create recipient token account. Please try again.'
}

setError(errorMessage)
Expand Down Expand Up @@ -141,6 +153,11 @@ export const SendTokensDrawer = () => {
setError('')
}

const handleBeforeUserSelectionNavigate = () => {
// Close the drawer but preserve state so it can be restored when user selects
onClose()
}

const renderHeader = () => {
return (
<Flex pv='l' ph='xl' gap='m' mb='m'>
Expand All @@ -161,7 +178,8 @@ export const SendTokensDrawer = () => {
initialDestinationAddress={state.destinationAddress}
initialSelectedUser={state.selectedUser}
initialRecipientType={state.recipientType}
onBeforeUserSelectionNavigate={handleClose}
onBeforeUserSelectionNavigate={handleBeforeUserSelectionNavigate}
onUserChange={handleUserChange}
/>
) : null}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type SendTokensInputProps = {
initialSelectedUser?: User | null
initialRecipientType?: RecipientType
onBeforeUserSelectionNavigate?: () => void
onUserChange?: (user: User | null) => void
}

const messages = {
Expand All @@ -48,7 +49,7 @@ const messages = {
insufficientBalance: 'Insufficient balance',
validWalletAddressRequired: 'A valid wallet address is required.',
amountRequired: 'Amount is required',
amountTooLow: 'Amount is too low to send',
amountTooLow: 'Amount must be at least $0.50',
walletAddress: 'Wallet Address',
userRequired: 'Please select a user',
userNoWallet:
Expand All @@ -70,7 +71,8 @@ export const SendTokensInput = ({
initialDestinationAddress = '',
initialSelectedUser = null,
initialRecipientType = 'user',
onBeforeUserSelectionNavigate
onBeforeUserSelectionNavigate,
onUserChange
}: SendTokensInputProps) => {
const [recipientType, setRecipientType] =
useState<RecipientType>(initialRecipientType)
Expand Down Expand Up @@ -129,6 +131,20 @@ export const SendTokensInput = ({
})
const tokenInfo = coin ? transformArtistCoinToTokenInfo(coin) : undefined

// Calculate USD value for display
const usdValueInfo = useMemo(() => {
if (!amount || parseFloat(amount) <= 0 || !coin) return null
const price =
coin.price === 0 ? coin.dynamicBondingCurve?.priceUSD : coin.price
if (!price || price <= 0) return null
const amountNum = parseFloat(amount)
const usdValue = amountNum * price
return {
usdValue,
isBelowMinimum: usdValue < 0.5
}
}, [amount, coin])

// Find the selected token in owned coins for the dropdown
const selectedToken = useMemo(() => {
const ownedToken = ownedCoins.find(
Expand Down Expand Up @@ -161,7 +177,13 @@ export const SendTokensInput = ({
}
}, [])
const handleAmountChange = useCallback((value: string) => {
setAmount(value)
// Only allow numbers and a single decimal point
const numericValue = value.replace(/[^0-9.]/g, '')
// Ensure only one decimal point
const parts = numericValue.split('.')
const filteredValue =
parts.length > 2 ? parts[0] + '.' + parts.slice(1).join('') : numericValue
setAmount(filteredValue)
setAmountError(null)
}, [])

Expand All @@ -170,17 +192,22 @@ export const SendTokensInput = ({
setAddressError(null)
}, [])

const handleUserChange = useCallback((user: User | null) => {
setSelectedUser(user)
setAddressError(null)
// When sending to a user, we derive their user-bank ATA from their ETH address on the backend
// But we still set spl_wallet for display purposes in the UI
if (user?.spl_wallet) {
setDestinationAddress(user.spl_wallet)
} else {
setDestinationAddress('')
}
}, [])
const handleUserChange = useCallback(
(user: User | null) => {
setSelectedUser(user)
setAddressError(null)
// When sending to a user, we derive their user-bank ATA from their ETH address on the backend
// But we still set spl_wallet for display purposes in the UI
if (user?.spl_wallet) {
setDestinationAddress(user.spl_wallet)
} else {
setDestinationAddress('')
}
// Notify parent component of user change
onUserChange?.(user)
},
[onUserChange]
)

const handleRecipientTypeChange = useCallback((type: RecipientType) => {
setRecipientType(type)
Expand All @@ -204,10 +231,22 @@ export const SendTokensInput = ({
if (amountWei > currentBalance) {
setAmountError('INSUFFICIENT_BALANCE')
isValid = false
} else if (amountWei < BigInt(1000)) {
// Minimum amount
setAmountError('AMOUNT_TOO_LOW')
isValid = false
} else {
// Check minimum USD value ($0.50)
const price =
coin?.price === 0 ? coin?.dynamicBondingCurve?.priceUSD : coin?.price
if (price && price > 0) {
const amountNum = parseFloat(amount)
const usdValue = amountNum * price
if (usdValue < 0.5) {
setAmountError('AMOUNT_TOO_LOW')
isValid = false
}
} else if (amountWei < BigInt(1000)) {
// Fallback to minimum token amount if price is not available
setAmountError('AMOUNT_TOO_LOW')
isValid = false
}
}
}

Expand Down Expand Up @@ -360,6 +399,16 @@ export const SendTokensInput = ({
tokenInfo?.symbol ? `$${tokenInfo.symbol}` : undefined
}
/>
{usdValueInfo && (
<Text
variant='body'
size='s'
color={usdValueInfo.isBelowMinimum ? 'danger' : 'subdued'}
>
≈ ${usdValueInfo.usdValue.toFixed(2)} USD
{usdValueInfo.isBelowMinimum && ' (minimum $0.50)'}
</Text>
)}
</Flex>
</Flex>

Expand Down
Loading