Skip to content

Commit 27a788f

Browse files
committed
feat: Improve wallet selection UX and sign-in flow
- Add wallet selection dialog that appears after clicking sign-in - Support multiple wallet providers (MetaMask, Coinbase, etc.) with proper detection - Remember last selected wallet for convenience - Center "Sign in to continue" vertically/horizontally with no-scroll layout - Fix wallet detection after sign-out to show all available wallets - Remove "Switch Wallet" functionality for simpler UX New components: - WalletSelectionDialog: Modal for choosing between detected wallets - ShareItemDialog: Dialog for sharing items between users (from previous commit) - UserSearchCombobox: User search functionality (from previous commit) - WalletSelector: Wallet selection component (unused)
1 parent 0674d55 commit 27a788f

15 files changed

Lines changed: 781 additions & 30 deletions

src/components/FormView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export default function FormView({ lang, id, setData, setId, setNewTask, classNa
3636
);
3737
if (!user) {
3838
return (
39-
<div className="justify-center w-full">
39+
<div className="flex items-center justify-center h-[calc(100vh-100px)]">
4040
<SignIn
4141
label="Sign in to continue"
4242
/>

src/components/SettingsForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default function Example() {
1616
const { user } = useGraffiticodeAuth();
1717
if (!user) {
1818
return (
19-
<div className="justify-center w-full">
19+
<div className="flex items-center justify-center h-[calc(100vh-100px)]">
2020
<SignIn
2121
className="rounded-none border-2 px-3 py-2 text-center hover:border-gray-400 focus:outline-none"
2222
label={<span className="block font-medium">Sign in to continue</span>}

src/components/ShareItemDialog.tsx

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { Fragment, useState } from 'react';
2+
import { Dialog, Transition } from '@headlessui/react';
3+
import { XMarkIcon } from '@heroicons/react/24/outline';
4+
import { ShareIcon, CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/20/solid';
5+
import UserSearchCombobox from './UserSearchCombobox';
6+
import { shareItem } from '../utils/swr/fetchers';
7+
import useGraffiticodeAuth from '../hooks/use-graffiticode-auth';
8+
9+
interface ShareItemDialogProps {
10+
isOpen: boolean;
11+
onClose: () => void;
12+
itemId: string;
13+
itemName: string;
14+
sharedWith?: string[];
15+
onShareSuccess?: () => void;
16+
}
17+
18+
export default function ShareItemDialog({
19+
isOpen,
20+
onClose,
21+
itemId,
22+
itemName,
23+
sharedWith = [],
24+
onShareSuccess
25+
}: ShareItemDialogProps) {
26+
const { user } = useGraffiticodeAuth();
27+
const [selectedUser, setSelectedUser] = useState<any>(null);
28+
const [isSharing, setIsSharing] = useState(false);
29+
const [shareResult, setShareResult] = useState<{ success: boolean; message: string } | null>(null);
30+
31+
const handleShare = async () => {
32+
if (!selectedUser || !user) return;
33+
34+
setIsSharing(true);
35+
setShareResult(null);
36+
37+
try {
38+
const result = await shareItem({
39+
user,
40+
itemId,
41+
targetUserId: selectedUser.id
42+
});
43+
44+
setShareResult({
45+
success: result.success,
46+
message: result.message || (result.success ? 'Item shared successfully!' : 'Failed to share item')
47+
});
48+
49+
if (result.success) {
50+
// Clear selection for next share
51+
setSelectedUser(null);
52+
// Notify parent of success
53+
if (onShareSuccess) {
54+
onShareSuccess();
55+
}
56+
// Auto-close after success
57+
setTimeout(() => {
58+
onClose();
59+
// Reset state after dialog closes
60+
setTimeout(() => {
61+
setShareResult(null);
62+
}, 300);
63+
}, 1500);
64+
}
65+
} catch (error) {
66+
setShareResult({
67+
success: false,
68+
message: 'An error occurred while sharing the item'
69+
});
70+
} finally {
71+
setIsSharing(false);
72+
}
73+
};
74+
75+
const handleClose = () => {
76+
// Reset state when closing
77+
setSelectedUser(null);
78+
setShareResult(null);
79+
onClose();
80+
};
81+
82+
return (
83+
<Transition.Root show={isOpen} as={Fragment}>
84+
<Dialog as="div" className="relative z-50" onClose={handleClose}>
85+
<Transition.Child
86+
as={Fragment}
87+
enter="ease-out duration-300"
88+
enterFrom="opacity-0"
89+
enterTo="opacity-100"
90+
leave="ease-in duration-200"
91+
leaveFrom="opacity-100"
92+
leaveTo="opacity-0"
93+
>
94+
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
95+
</Transition.Child>
96+
97+
<div className="fixed inset-0 z-10 overflow-y-auto">
98+
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
99+
<Transition.Child
100+
as={Fragment}
101+
enter="ease-out duration-300"
102+
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
103+
enterTo="opacity-100 translate-y-0 sm:scale-100"
104+
leave="ease-in duration-200"
105+
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
106+
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
107+
>
108+
<Dialog.Panel className="relative transform overflow-hidden rounded-none bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
109+
<div className="absolute right-0 top-0 hidden pr-4 pt-4 sm:block">
110+
<button
111+
type="button"
112+
className="rounded-none bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
113+
onClick={handleClose}
114+
>
115+
<span className="sr-only">Close</span>
116+
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
117+
</button>
118+
</div>
119+
<div className="sm:flex sm:items-start">
120+
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-none bg-gray-100 sm:mx-0 sm:h-10 sm:w-10">
121+
<ShareIcon className="h-6 w-6 text-gray-600" aria-hidden="true" />
122+
</div>
123+
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left w-full">
124+
<Dialog.Title as="h3" className="text-base font-semibold leading-6 text-gray-900">
125+
Share Item
126+
</Dialog.Title>
127+
<div className="mt-2">
128+
<p className="text-sm text-gray-500">
129+
Share &ldquo;{itemName}&rdquo; with another user. The item will be copied to their account.
130+
</p>
131+
</div>
132+
133+
{sharedWith.length > 0 && (
134+
<div className="mt-4">
135+
<p className="text-xs font-semibold text-gray-600 mb-1">Already shared with:</p>
136+
<div className="text-xs text-gray-500">
137+
{sharedWith.map((userId, index) => (
138+
<span key={userId}>
139+
{userId}
140+
{index < sharedWith.length - 1 && ', '}
141+
</span>
142+
))}
143+
</div>
144+
</div>
145+
)}
146+
147+
<div className="mt-4">
148+
<label className="block text-sm font-medium text-gray-700 mb-2">
149+
Select User
150+
</label>
151+
<UserSearchCombobox
152+
selectedUser={selectedUser}
153+
onSelectUser={setSelectedUser}
154+
placeholder="Search by user ID or email..."
155+
/>
156+
</div>
157+
158+
{shareResult && (
159+
<div className={`mt-4 p-3 rounded-none ${shareResult.success ? 'bg-green-50' : 'bg-red-50'}`}>
160+
<div className="flex">
161+
<div className="flex-shrink-0">
162+
{shareResult.success ? (
163+
<CheckCircleIcon className="h-5 w-5 text-green-400" aria-hidden="true" />
164+
) : (
165+
<ExclamationCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
166+
)}
167+
</div>
168+
<div className="ml-3">
169+
<p className={`text-sm ${shareResult.success ? 'text-green-800' : 'text-red-800'}`}>
170+
{shareResult.message}
171+
</p>
172+
</div>
173+
</div>
174+
</div>
175+
)}
176+
</div>
177+
</div>
178+
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
179+
<button
180+
type="button"
181+
disabled={!selectedUser || isSharing}
182+
onClick={handleShare}
183+
className="inline-flex w-full justify-center rounded-none bg-gray-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-700 disabled:bg-gray-300 disabled:cursor-not-allowed sm:ml-3 sm:w-auto"
184+
>
185+
{isSharing ? 'Sharing...' : 'Share'}
186+
</button>
187+
<button
188+
type="button"
189+
onClick={handleClose}
190+
className="mt-3 inline-flex w-full justify-center rounded-none bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
191+
>
192+
Cancel
193+
</button>
194+
</div>
195+
</Dialog.Panel>
196+
</Transition.Child>
197+
</div>
198+
</div>
199+
</Dialog>
200+
</Transition.Root>
201+
);
202+
}

src/components/SignIn.tsx

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,61 @@
1+
import { useState } from "react";
12
import useGraffiticodeAuth from "../hooks/use-graffiticode-auth";
3+
import WalletSelectionDialog from "./WalletSelectionDialog";
24

35
interface SignInComponentProps {
46
label?: string | React.ReactNode;
57
className?: string;
68
}
79

10+
interface Wallet {
11+
name: string;
12+
provider: any;
13+
}
14+
815
export default function SignInComponent({ label = "Sign in", className }: SignInComponentProps) {
916
const { loading, user, signInWithEthereum, signOut } = useGraffiticodeAuth();
17+
const [showWalletDialog, setShowWalletDialog] = useState(false);
18+
19+
const handleSignInClick = () => {
20+
// Always show wallet selection dialog
21+
setShowWalletDialog(true);
22+
};
1023

11-
const handleSignIn = async () => {
24+
const handleSignIn = async (wallet: Wallet | null) => {
1225
try {
13-
await signInWithEthereum();
26+
await signInWithEthereum(wallet);
1427
} catch (err) {
1528
console.error(err);
1629
}
1730
};
1831

32+
const handleWalletSelect = (wallet: Wallet) => {
33+
handleSignIn(wallet);
34+
};
35+
1936
if (user) {
20-
return <button className={className} disabled={loading} onClick={signOut}>{`${user.uid.slice(0, 7)}...${user.uid.slice(33)}`} (Sign out)</button>;
37+
return (
38+
<button className={className} disabled={loading} onClick={signOut}>
39+
{`${user.uid.slice(0, 7)}...${user.uid.slice(33)}`} (Sign out)
40+
</button>
41+
);
2142
} else {
22-
return <button className={className} disabled={loading} onClick={handleSignIn}>{label}</button>;
43+
return (
44+
<>
45+
<button
46+
className={className}
47+
disabled={loading}
48+
onClick={handleSignInClick}
49+
>
50+
{label}
51+
</button>
52+
53+
<WalletSelectionDialog
54+
isOpen={showWalletDialog}
55+
onClose={() => setShowWalletDialog(false)}
56+
onSelectWallet={handleWalletSelect}
57+
/>
58+
</>
59+
);
2360
}
2461
}

0 commit comments

Comments
 (0)