Skip to content

Commit ce8a00e

Browse files
committed
feat: Add send and receive functionality to WalletSheet, including destination validation and transaction submission.
1 parent 160188b commit ce8a00e

2 files changed

Lines changed: 944 additions & 315 deletions

File tree

components/wallet/FamilyWalletDrawer.tsx

Lines changed: 98 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ import {
4444
} from '@/components/ui/select';
4545
import { Checkbox } from '@/components/ui/checkbox';
4646
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
47+
import {
48+
AlertDialog,
49+
AlertDialogAction,
50+
AlertDialogCancel,
51+
AlertDialogContent,
52+
AlertDialogDescription,
53+
AlertDialogFooter,
54+
AlertDialogHeader,
55+
AlertDialogTitle,
56+
AlertDialogTrigger,
57+
} from '@/components/ui/alert-dialog';
4758
import type { ApiError } from '@/lib/api/api';
4859
import { AssetIcon } from './AssetIcon';
4960
import { cn } from '@/lib/utils';
@@ -88,8 +99,6 @@ export function FamilyWalletDrawer({
8899
const [sendDestination, setSendDestination] = useState('');
89100
const [sendCurrency, setSendCurrency] = useState('');
90101
const [sendAmount, setSendAmount] = useState('');
91-
const [sendMemo, setSendMemo] = useState('');
92-
const [sendMemoRequired, setSendMemoRequired] = useState(false);
93102
const [validateLoading, setValidateLoading] = useState(false);
94103
const [validateResult, setValidateResult] = useState<
95104
'idle' | 'valid' | 'invalid'
@@ -127,8 +136,6 @@ export function FamilyWalletDrawer({
127136
const resetSendForm = useCallback(() => {
128137
setSendDestination('');
129138
setSendAmount('');
130-
setSendMemo('');
131-
setSendMemoRequired(false);
132139
setValidateResult('idle');
133140
setValidateError('');
134141
setValidateErrorDetails([]);
@@ -297,24 +304,13 @@ export function FamilyWalletDrawer({
297304
);
298305
return;
299306
}
300-
if (sendMemoRequired && !sendMemo.trim()) {
301-
setSendError('Memo is required by the recipient');
302-
return;
303-
}
304-
const memoBytes = new TextEncoder().encode(sendMemo).length;
305-
if (sendMemo && memoBytes > 28) {
306-
setSendError('Memo must be 28 bytes or less (UTF-8)');
307-
return;
308-
}
309307
setSendLoading(true);
310308
setSendError('');
311309
try {
312310
await sendFunds({
313311
destinationPublicKey: dest,
314312
amount,
315313
currency,
316-
memo: sendMemo.trim() || undefined,
317-
memoRequired: sendMemoRequired || undefined,
318314
idempotencyKey: crypto.randomUUID(),
319315
});
320316
toast.success('Send submitted successfully');
@@ -332,8 +328,6 @@ export function FamilyWalletDrawer({
332328
sendDestination,
333329
sendCurrency,
334330
sendAmount,
335-
sendMemo,
336-
sendMemoRequired,
337331
validateResult,
338332
balances,
339333
refreshWallet,
@@ -863,6 +857,30 @@ export function FamilyWalletDrawer({
863857
</div>
864858

865859
<div className='space-y-4'>
860+
<Alert className='mx-1 border-orange-500/20 bg-orange-500/10 text-orange-600'>
861+
<AlertCircle className='h-4 w-4 text-orange-600' />
862+
<AlertTitle>Important Withdrawal Notice</AlertTitle>
863+
<AlertDescription className='text-xs leading-relaxed'>
864+
<p className='mt-1 text-orange-700 dark:text-orange-400'>
865+
Do not withdraw directly to a Centralized Exchange
866+
(e.g., Binance, Coinbase) wallet. This wallet does
867+
not support memos, and your funds will be lost if
868+
you use one. You must use a self-custodial wallet
869+
that does not require a memo.
870+
</p>
871+
<p className='mt-2'>
872+
<a
873+
href='https://docs.boundlessfi.xyz/how-to-guides/withdraw-funds'
874+
target='_blank'
875+
rel='noopener noreferrer'
876+
className='font-semibold underline underline-offset-2 hover:text-orange-800 dark:hover:text-orange-300'
877+
>
878+
Read our withdrawal guide here.
879+
</a>
880+
</p>
881+
</AlertDescription>
882+
</Alert>
883+
866884
<div className='space-y-2'>
867885
<Label htmlFor='send-destination'>
868886
Destination (Stellar G...)
@@ -983,33 +1001,6 @@ export function FamilyWalletDrawer({
9831001
})()}
9841002
</div>
9851003

986-
<div className='space-y-2'>
987-
<Label htmlFor='send-memo'>
988-
Memo (optional, max 28 bytes)
989-
</Label>
990-
<Input
991-
id='send-memo'
992-
placeholder='Memo for exchange/deposit'
993-
value={sendMemo}
994-
onChange={e => setSendMemo(e.target.value)}
995-
/>
996-
<div className='flex items-center gap-2'>
997-
<Checkbox
998-
id='send-memo-required'
999-
checked={sendMemoRequired}
1000-
onCheckedChange={c =>
1001-
setSendMemoRequired(c === true)
1002-
}
1003-
/>
1004-
<Label
1005-
htmlFor='send-memo-required'
1006-
className='text-muted-foreground cursor-pointer text-sm font-normal'
1007-
>
1008-
Memo required by recipient (e.g. exchange)
1009-
</Label>
1010-
</div>
1011-
</div>
1012-
10131004
{sendError && (
10141005
<Alert variant='destructive' className='mt-2'>
10151006
<AlertCircle className='h-4 w-4' />
@@ -1029,28 +1020,69 @@ export function FamilyWalletDrawer({
10291020
</Alert>
10301021
)}
10311022

1032-
<Button
1033-
className='w-full'
1034-
onClick={handleSendSubmit}
1035-
disabled={
1036-
sendLoading ||
1037-
validateResult !== 'valid' ||
1038-
!sendAmount ||
1039-
parseFloat(sendAmount) <= 0
1040-
}
1041-
>
1042-
{sendLoading ? (
1043-
<>
1044-
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
1045-
Sending…
1046-
</>
1047-
) : (
1048-
<>
1049-
<ArrowUpRight className='mr-2 h-4 w-4' />
1050-
Send
1051-
</>
1052-
)}
1053-
</Button>
1023+
<AlertDialog>
1024+
<AlertDialogTrigger asChild>
1025+
<Button
1026+
className='w-full'
1027+
disabled={
1028+
sendLoading ||
1029+
validateResult !== 'valid' ||
1030+
!sendAmount ||
1031+
parseFloat(sendAmount) <= 0
1032+
}
1033+
>
1034+
{sendLoading ? (
1035+
<>
1036+
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
1037+
Sending…
1038+
</>
1039+
) : (
1040+
<>
1041+
<ArrowUpRight className='mr-2 h-4 w-4' />
1042+
Send
1043+
</>
1044+
)}
1045+
</Button>
1046+
</AlertDialogTrigger>
1047+
<AlertDialogContent className='z-100 max-w-[90vw] sm:max-w-md'>
1048+
<AlertDialogHeader>
1049+
<AlertDialogTitle>
1050+
Confirm Withdrawal
1051+
</AlertDialogTitle>
1052+
<AlertDialogDescription className='space-y-3 text-left'>
1053+
<span className='block text-sm'>
1054+
You are about to send{' '}
1055+
<strong className='text-foreground'>
1056+
{sendAmount} {sendCurrency}
1057+
</strong>{' '}
1058+
to{' '}
1059+
<strong className='text-foreground font-mono break-all'>
1060+
{sendDestination}
1061+
</strong>
1062+
.
1063+
</span>
1064+
<span className='text-destructive mt-4 block text-sm font-semibold'>
1065+
⚠️ WARNING: Do not withdraw to Centralized
1066+
Exchanges (e.g. Binance, Coinbase).
1067+
</span>
1068+
<span className='block text-sm'>
1069+
Exchanges require a memo, which this wallet does
1070+
NOT support. Doing so will result in permanent
1071+
loss of your funds.
1072+
</span>
1073+
</AlertDialogDescription>
1074+
</AlertDialogHeader>
1075+
<AlertDialogFooter className='mt-4 gap-2 sm:gap-0'>
1076+
<AlertDialogCancel>Cancel</AlertDialogCancel>
1077+
<AlertDialogAction
1078+
onClick={handleSendSubmit}
1079+
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
1080+
>
1081+
Yes, I confirm this is a self-custodial wallet
1082+
</AlertDialogAction>
1083+
</AlertDialogFooter>
1084+
</AlertDialogContent>
1085+
</AlertDialog>
10541086
</div>
10551087
</motion.div>
10561088
)}

0 commit comments

Comments
 (0)