|
1 | 1 | import { useEffect, useState } from 'react'; |
2 | 2 | import { useNavigate } from 'react-router-dom'; |
3 | | -import { Stack, Typography } from '@mui/material'; |
| 3 | +import { Stack, Typography, Alert, Box } from '@mui/material'; |
4 | 4 | import { AppCard, AppLayout } from '../ui'; |
5 | 5 | import { CreateGroupForm } from '../components/CreateGroupForm'; |
6 | | -import { createGroup } from '../utils/groupApi'; |
| 6 | +import { ToastProvider } from '../components/Toast/ToastProvider'; |
| 7 | +import { useToast } from '../components/Toast/useToast'; |
| 8 | +import { useWallet } from '../hooks/useWallet'; |
| 9 | +import { createGroup, parseContractError } from '../lib/contractClient'; |
7 | 10 | import type { GroupData } from '../utils/groupApi'; |
8 | 11 | import { ROUTES, buildRoute } from '../routing/constants'; |
9 | 12 |
|
10 | 13 | type SubmitStatus = 'idle' | 'loading' | 'success' | 'error'; |
11 | 14 |
|
12 | | -interface PageState { |
13 | | - status: SubmitStatus; |
14 | | - groupId: string | null; |
15 | | - errorMessage: string | null; |
16 | | - groupName: string | null; |
17 | | -} |
18 | | - |
19 | | -export default function CreateGroupPage() { |
| 15 | +function CreateGroupContent() { |
20 | 16 | const navigate = useNavigate(); |
21 | | - const [pageState, setPageState] = useState<PageState>({ |
22 | | - status: 'idle', |
23 | | - groupId: null, |
24 | | - errorMessage: null, |
25 | | - groupName: null, |
26 | | - }); |
27 | | - |
28 | | - // Task 10: redirect after success |
29 | | - useEffect(() => { |
30 | | - if (pageState.status !== 'success') return; |
31 | | - const timer = setTimeout(() => { |
32 | | - if (pageState.groupId) { |
33 | | - navigate(buildRoute.groupDetail(pageState.groupId)); |
34 | | - } else { |
35 | | - navigate(ROUTES.GROUPS); |
36 | | - } |
37 | | - }, 2000); |
38 | | - return () => clearTimeout(timer); |
39 | | - }, [pageState.status, pageState.groupId, navigate]); |
| 17 | + const { addToast } = useToast(); |
| 18 | + const { status: walletStatus, activeAddress, connect } = useWallet(); |
40 | 19 |
|
41 | | - const handleCancel = () => { |
42 | | - navigate(ROUTES.GROUPS); |
43 | | - }; |
| 20 | + const [submitStatus, setSubmitStatus] = useState<SubmitStatus>('idle'); |
| 21 | + const [errorMessage, setErrorMessage] = useState<string | null>(null); |
| 22 | + const [createdGroupId, setCreatedGroupId] = useState<string | null>(null); |
| 23 | + |
| 24 | + // Redirect to group detail after successful creation |
| 25 | + useEffect(() => { |
| 26 | + if (submitStatus !== 'success') return; |
| 27 | + const t = setTimeout(() => { |
| 28 | + navigate(createdGroupId ? buildRoute.groupDetail(createdGroupId) : ROUTES.GROUPS); |
| 29 | + }, 2500); |
| 30 | + return () => clearTimeout(t); |
| 31 | + }, [submitStatus, createdGroupId, navigate]); |
44 | 32 |
|
45 | 33 | const handleSubmit = async (data: GroupData) => { |
46 | | - setPageState(prev => ({ ...prev, status: 'loading', errorMessage: null })); |
| 34 | + // Wallet must be connected |
| 35 | + if (walletStatus !== 'connected' || !activeAddress) { |
| 36 | + setErrorMessage('Please connect your Freighter wallet before creating a group.'); |
| 37 | + return; |
| 38 | + } |
| 39 | + |
| 40 | + setSubmitStatus('loading'); |
| 41 | + setErrorMessage(null); |
| 42 | + |
47 | 43 | try { |
48 | | - const groupId = await createGroup(data); |
49 | | - setPageState({ |
50 | | - status: 'success', |
51 | | - groupId, |
52 | | - errorMessage: null, |
53 | | - groupName: data.name, |
| 44 | + const groupId = await createGroup({ |
| 45 | + creator: activeAddress, |
| 46 | + contributionAmount: BigInt(data.contribution_amount), // already in stroops |
| 47 | + cycleDuration: BigInt(data.cycle_duration), |
| 48 | + maxMembers: data.max_members, |
| 49 | + }); |
| 50 | + |
| 51 | + const groupIdStr = groupId.toString(); |
| 52 | + setCreatedGroupId(groupIdStr); |
| 53 | + setSubmitStatus('success'); |
| 54 | + |
| 55 | + addToast({ |
| 56 | + message: `Group "${data.name}" created! Group ID: ${groupIdStr}`, |
| 57 | + type: 'success', |
| 58 | + duration: 6000, |
54 | 59 | }); |
55 | 60 | } catch (err) { |
56 | | - const errorMessage = |
57 | | - err instanceof Error && err.message |
58 | | - ? err.message |
59 | | - : 'Failed to create group. Please try again.'; |
60 | | - setPageState(prev => ({ ...prev, status: 'error', errorMessage })); |
| 61 | + const contractErr = parseContractError(err); |
| 62 | + |
| 63 | + // Map known rejection/funds errors to friendly messages |
| 64 | + let msg = contractErr.message; |
| 65 | + if (msg.toLowerCase().includes('user declined') || msg.toLowerCase().includes('rejected')) { |
| 66 | + msg = 'Transaction rejected. You cancelled the signing request in Freighter.'; |
| 67 | + } else if (msg.toLowerCase().includes('insufficient')) { |
| 68 | + msg = 'Insufficient funds. Please ensure your wallet has enough XLM to cover the transaction fee.'; |
| 69 | + } |
| 70 | + |
| 71 | + setErrorMessage(msg); |
| 72 | + setSubmitStatus('error'); |
| 73 | + addToast({ message: msg, type: 'error', duration: 6000 }); |
61 | 74 | } |
62 | 75 | }; |
63 | 76 |
|
| 77 | + const isWalletConnected = walletStatus === 'connected' && Boolean(activeAddress); |
| 78 | + |
64 | 79 | return ( |
65 | | - <AppLayout |
66 | | - title="Create Group" |
67 | | - subtitle="Set up your savings circle" |
68 | | - footerText="Stellar Save - Built for transparent, on-chain savings" |
69 | | - > |
70 | | - <AppCard> |
71 | | - <Stack spacing={2}> |
72 | | - {/* aria-live region for status announcements */} |
73 | | - <div aria-live="polite" aria-atomic="true"> |
74 | | - {pageState.status === 'success' && ( |
75 | | - <Typography color="success.main"> |
76 | | - Group created successfully! Redirecting... |
77 | | - </Typography> |
78 | | - )} |
79 | | - {pageState.status === 'error' && pageState.errorMessage && ( |
80 | | - <Typography color="error.main">{pageState.errorMessage}</Typography> |
81 | | - )} |
82 | | - </div> |
| 80 | + <AppCard> |
| 81 | + <Stack spacing={3}> |
| 82 | + {/* Wallet connection warning */} |
| 83 | + {!isWalletConnected && ( |
| 84 | + <Alert |
| 85 | + severity="warning" |
| 86 | + action={ |
| 87 | + <Box |
| 88 | + component="button" |
| 89 | + onClick={connect} |
| 90 | + sx={{ cursor: 'pointer', fontWeight: 'bold', background: 'none', border: 'none', color: 'warning.dark', fontSize: '0.85rem' }} |
| 91 | + > |
| 92 | + Connect Wallet |
| 93 | + </Box> |
| 94 | + } |
| 95 | + > |
| 96 | + Connect your Freighter wallet to deploy a group on-chain. |
| 97 | + </Alert> |
| 98 | + )} |
83 | 99 |
|
84 | | - {pageState.status === 'success' ? ( |
85 | | - <Typography variant="h6"> |
86 | | - "{pageState.groupName}" has been created! You will be redirected shortly. |
| 100 | + {/* Success state */} |
| 101 | + {submitStatus === 'success' ? ( |
| 102 | + <Stack spacing={1} alignItems="center" sx={{ py: 4 }}> |
| 103 | + <Typography variant="h5" fontWeight="bold" color="success.main"> |
| 104 | + Group Created! |
| 105 | + </Typography> |
| 106 | + <Typography variant="body2" color="text.secondary"> |
| 107 | + Redirecting to your group... |
87 | 108 | </Typography> |
88 | | - ) : ( |
| 109 | + </Stack> |
| 110 | + ) : ( |
| 111 | + <> |
| 112 | + {/* Inline error (also shown as toast) */} |
| 113 | + {submitStatus === 'error' && errorMessage && ( |
| 114 | + <Alert severity="error" onClose={() => setErrorMessage(null)}> |
| 115 | + {errorMessage} |
| 116 | + </Alert> |
| 117 | + )} |
| 118 | + |
89 | 119 | <CreateGroupForm |
90 | 120 | onSubmit={handleSubmit} |
91 | | - onCancel={handleCancel} |
92 | | - isSubmitting={pageState.status === 'loading'} |
| 121 | + onCancel={() => navigate(ROUTES.GROUPS)} |
| 122 | + isSubmitting={submitStatus === 'loading'} |
93 | 123 | /> |
94 | | - )} |
95 | | - </Stack> |
96 | | - </AppCard> |
97 | | - </AppLayout> |
| 124 | + </> |
| 125 | + )} |
| 126 | + </Stack> |
| 127 | + </AppCard> |
| 128 | + ); |
| 129 | +} |
| 130 | + |
| 131 | +export default function CreateGroupPage() { |
| 132 | + return ( |
| 133 | + <ToastProvider> |
| 134 | + <AppLayout |
| 135 | + title="Create Group" |
| 136 | + subtitle="Deploy a new savings circle on Stellar" |
| 137 | + footerText="Stellar Save - Built for transparent, on-chain savings" |
| 138 | + > |
| 139 | + <CreateGroupContent /> |
| 140 | + </AppLayout> |
| 141 | + </ToastProvider> |
98 | 142 | ); |
99 | 143 | } |
0 commit comments