@b3dotfun/sdk 0.0.26-alpha.5 → 0.0.26-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/anyspend/react/components/AnySpendCustom.d.ts +1 -0
- package/dist/cjs/anyspend/react/components/AnySpendCustom.js +3 -2
- package/dist/cjs/anyspend/react/components/AnySpendNFT.js +2 -2
- package/dist/cjs/global-account/react/components/B3DynamicModal.js +4 -0
- package/dist/cjs/global-account/react/components/LinkAccount/LinkAccount.d.ts +2 -0
- package/dist/cjs/global-account/react/components/LinkAccount/LinkAccount.js +228 -0
- package/dist/cjs/global-account/react/components/ManageAccount/ManageAccount.js +56 -3
- package/dist/cjs/global-account/react/components/custom/Button.d.ts +1 -1
- package/dist/cjs/global-account/react/components/ui/button.d.ts +1 -1
- package/dist/cjs/global-account/react/stores/useModalStore.d.ts +20 -1
- package/dist/cjs/global-account/react/stores/useModalStore.js +3 -0
- package/dist/cjs/global-account/react/utils/profileDisplay.d.ts +21 -0
- package/dist/cjs/global-account/react/utils/profileDisplay.js +63 -0
- package/dist/esm/anyspend/react/components/AnySpendCustom.d.ts +1 -0
- package/dist/esm/anyspend/react/components/AnySpendCustom.js +3 -2
- package/dist/esm/anyspend/react/components/AnySpendNFT.js +2 -2
- package/dist/esm/global-account/react/components/B3DynamicModal.js +4 -0
- package/dist/esm/global-account/react/components/LinkAccount/LinkAccount.d.ts +2 -0
- package/dist/esm/global-account/react/components/LinkAccount/LinkAccount.js +225 -0
- package/dist/esm/global-account/react/components/ManageAccount/ManageAccount.js +58 -5
- package/dist/esm/global-account/react/components/custom/Button.d.ts +1 -1
- package/dist/esm/global-account/react/components/ui/button.d.ts +1 -1
- package/dist/esm/global-account/react/stores/useModalStore.d.ts +20 -1
- package/dist/esm/global-account/react/stores/useModalStore.js +3 -0
- package/dist/esm/global-account/react/utils/profileDisplay.d.ts +21 -0
- package/dist/esm/global-account/react/utils/profileDisplay.js +60 -0
- package/dist/styles/index.css +1 -1
- package/dist/types/anyspend/react/components/AnySpendCustom.d.ts +1 -0
- package/dist/types/global-account/react/components/LinkAccount/LinkAccount.d.ts +2 -0
- package/dist/types/global-account/react/components/custom/Button.d.ts +1 -1
- package/dist/types/global-account/react/components/ui/button.d.ts +1 -1
- package/dist/types/global-account/react/stores/useModalStore.d.ts +20 -1
- package/dist/types/global-account/react/utils/profileDisplay.d.ts +21 -0
- package/package.json +1 -1
- package/src/anyspend/react/components/AnySpendCustom.tsx +7 -3
- package/src/anyspend/react/components/AnySpendNFT.tsx +2 -1
- package/src/global-account/react/components/B3DynamicModal.tsx +4 -0
- package/src/global-account/react/components/LinkAccount/LinkAccount.tsx +369 -0
- package/src/global-account/react/components/ManageAccount/ManageAccount.tsx +187 -5
- package/src/global-account/react/stores/useModalStore.ts +26 -1
- package/src/global-account/react/utils/profileDisplay.ts +87 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { client } from "@b3dotfun/sdk/shared/utils/thirdweb";
|
|
2
|
+
import { Loader2 } from "lucide-react";
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import { useLinkProfile, useProfiles } from "thirdweb/react";
|
|
6
|
+
import { preAuthenticate } from "thirdweb/wallets";
|
|
7
|
+
import { LinkAccountModalProps, useModalStore } from "../../stores/useModalStore";
|
|
8
|
+
import { getProfileDisplayInfo } from "../../utils/profileDisplay";
|
|
9
|
+
import { useB3 } from "../B3Provider/useB3";
|
|
10
|
+
import { Button } from "../ui/button";
|
|
11
|
+
type OTPStrategy = "email" | "phone";
|
|
12
|
+
type SocialStrategy = "google" | "x" | "discord" | "apple";
|
|
13
|
+
type Strategy = OTPStrategy | SocialStrategy;
|
|
14
|
+
|
|
15
|
+
interface AuthMethod {
|
|
16
|
+
id: Strategy;
|
|
17
|
+
label: string;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
icon?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const AUTH_METHODS: AuthMethod[] = [
|
|
23
|
+
{ id: "email", label: "Email", enabled: true },
|
|
24
|
+
{ id: "phone", label: "Phone", enabled: true },
|
|
25
|
+
{ id: "google", label: "Google", enabled: true },
|
|
26
|
+
{ id: "x", label: "X (Twitter)", enabled: true },
|
|
27
|
+
{ id: "discord", label: "Discord", enabled: true },
|
|
28
|
+
{ id: "apple", label: "Apple", enabled: true },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export function LinkAccount({
|
|
32
|
+
onSuccess: onSuccessCallback,
|
|
33
|
+
onError,
|
|
34
|
+
onClose,
|
|
35
|
+
chain,
|
|
36
|
+
partnerId,
|
|
37
|
+
}: LinkAccountModalProps) {
|
|
38
|
+
const { isLinking, linkingMethod, setLinkingState, navigateBack, setB3ModalContentType } = useModalStore();
|
|
39
|
+
const [selectedMethod, setSelectedMethod] = useState<Strategy | null>(null);
|
|
40
|
+
const [email, setEmail] = useState("");
|
|
41
|
+
const [phone, setPhone] = useState("");
|
|
42
|
+
const [otp, setOtp] = useState("");
|
|
43
|
+
const [otpSent, setOtpSent] = useState(false);
|
|
44
|
+
const [error, setError] = useState<string | null>(null);
|
|
45
|
+
const { data: profilesRaw = [] } = useProfiles({ client });
|
|
46
|
+
|
|
47
|
+
// Get connected auth methods
|
|
48
|
+
const connectedAuthMethods = profilesRaw
|
|
49
|
+
.filter((profile: any) => !["custom_auth_endpoint", "siwe"].includes(profile.type))
|
|
50
|
+
.map((profile: any) => profile.type as Strategy);
|
|
51
|
+
|
|
52
|
+
// Filter available auth methods
|
|
53
|
+
const availableAuthMethods = AUTH_METHODS.filter(
|
|
54
|
+
method => !connectedAuthMethods.includes(method.id) && method.enabled,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const profiles = profilesRaw
|
|
58
|
+
.filter((profile: any) => !["custom_auth_endpoint", "siwe"].includes(profile.type))
|
|
59
|
+
.map((profile: any) => ({
|
|
60
|
+
...getProfileDisplayInfo(profile),
|
|
61
|
+
originalProfile: profile,
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
const { account } = useB3();
|
|
65
|
+
const { mutate: linkProfile } = useLinkProfile();
|
|
66
|
+
|
|
67
|
+
const onSuccess = useCallback(async () => {
|
|
68
|
+
await onSuccessCallback?.();
|
|
69
|
+
}, [onSuccessCallback]);
|
|
70
|
+
|
|
71
|
+
// Reset linking state when component unmounts
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
return () => {
|
|
74
|
+
if (isLinking) {
|
|
75
|
+
setLinkingState(false);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}, [isLinking, setLinkingState]);
|
|
79
|
+
|
|
80
|
+
const mutationOptions = {
|
|
81
|
+
onError: (error: Error) => {
|
|
82
|
+
console.error("Error linking account:", error);
|
|
83
|
+
toast.error(error.message);
|
|
84
|
+
setLinkingState(false);
|
|
85
|
+
onError?.(error);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const validateInput = () => {
|
|
90
|
+
if (selectedMethod === "email") {
|
|
91
|
+
if (!email) {
|
|
92
|
+
setError("Please enter your email address");
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
96
|
+
setError("Please enter a valid email address");
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
} else if (selectedMethod === "phone") {
|
|
100
|
+
if (!phone) {
|
|
101
|
+
setError("Please enter your phone number");
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if (!/^\+?[\d\s-]{10,}$/.test(phone)) {
|
|
105
|
+
setError("Please enter a valid phone number");
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
setError(null);
|
|
110
|
+
return true;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleSendOTP = async () => {
|
|
114
|
+
if (!validateInput()) return;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
setLinkingState(true, selectedMethod);
|
|
118
|
+
setError(null);
|
|
119
|
+
|
|
120
|
+
if (selectedMethod === "email") {
|
|
121
|
+
await preAuthenticate({
|
|
122
|
+
client,
|
|
123
|
+
strategy: "email",
|
|
124
|
+
email,
|
|
125
|
+
});
|
|
126
|
+
} else if (selectedMethod === "phone") {
|
|
127
|
+
await preAuthenticate({
|
|
128
|
+
client,
|
|
129
|
+
strategy: "phone",
|
|
130
|
+
phoneNumber: phone,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
setOtpSent(true);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error("Error sending OTP:", error);
|
|
137
|
+
setError(error instanceof Error ? error.message : "Failed to send OTP");
|
|
138
|
+
onError?.(error as Error);
|
|
139
|
+
setLinkingState(false);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const handleLinkAccount = async () => {
|
|
144
|
+
if (!otp) {
|
|
145
|
+
setError("Please enter the verification code");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
setLinkingState(true, selectedMethod);
|
|
151
|
+
setError(null);
|
|
152
|
+
|
|
153
|
+
if (selectedMethod === "email") {
|
|
154
|
+
await linkProfile(
|
|
155
|
+
{
|
|
156
|
+
client,
|
|
157
|
+
strategy: "email",
|
|
158
|
+
email,
|
|
159
|
+
verificationCode: otp,
|
|
160
|
+
},
|
|
161
|
+
mutationOptions,
|
|
162
|
+
);
|
|
163
|
+
} else if (selectedMethod === "phone") {
|
|
164
|
+
await linkProfile(
|
|
165
|
+
{
|
|
166
|
+
client,
|
|
167
|
+
strategy: "phone",
|
|
168
|
+
phoneNumber: phone,
|
|
169
|
+
verificationCode: otp,
|
|
170
|
+
},
|
|
171
|
+
mutationOptions,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
onSuccess?.();
|
|
176
|
+
onClose?.();
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error("Error linking account:", error);
|
|
179
|
+
setError(error instanceof Error ? error.message : "Failed to link account");
|
|
180
|
+
onError?.(error as Error);
|
|
181
|
+
} finally {
|
|
182
|
+
setLinkingState(false);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const handleSocialLink = async (strategy: SocialStrategy) => {
|
|
187
|
+
try {
|
|
188
|
+
console.log("handleSocialLink", strategy);
|
|
189
|
+
setLinkingState(true, strategy);
|
|
190
|
+
setError(null);
|
|
191
|
+
|
|
192
|
+
const result = await linkProfile(
|
|
193
|
+
{
|
|
194
|
+
client,
|
|
195
|
+
strategy,
|
|
196
|
+
},
|
|
197
|
+
mutationOptions,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
console.log("result", result);
|
|
201
|
+
|
|
202
|
+
// Don't close the modal yet, wait for auth to complete
|
|
203
|
+
onSuccess?.();
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error("Error linking with social:", error);
|
|
206
|
+
setError(error instanceof Error ? error.message : "Failed to link social account");
|
|
207
|
+
onError?.(error as Error);
|
|
208
|
+
setLinkingState(false);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Add effect to handle social auth completion
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
if (isLinking && linkingMethod && !selectedMethod) {
|
|
215
|
+
// This means we're in a social auth flow
|
|
216
|
+
const checkAuthStatus = async () => {
|
|
217
|
+
try {
|
|
218
|
+
// Wait a bit to ensure auth is complete
|
|
219
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
220
|
+
onClose?.();
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error("Error checking auth status:", error);
|
|
223
|
+
setLinkingState(false);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
checkAuthStatus();
|
|
228
|
+
}
|
|
229
|
+
}, [isLinking, linkingMethod, selectedMethod, onClose, setLinkingState]);
|
|
230
|
+
|
|
231
|
+
const handleBack = useCallback(() => {
|
|
232
|
+
if (isLinking) return;
|
|
233
|
+
setSelectedMethod(null);
|
|
234
|
+
setEmail("");
|
|
235
|
+
setPhone("");
|
|
236
|
+
setOtp("");
|
|
237
|
+
setOtpSent(false);
|
|
238
|
+
setError(null);
|
|
239
|
+
setLinkingState(false);
|
|
240
|
+
}, [isLinking, setSelectedMethod, setEmail, setPhone, setOtp, setOtpSent, setError, setLinkingState]);
|
|
241
|
+
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
if (isLinking) {
|
|
244
|
+
setLinkingState(false);
|
|
245
|
+
navigateBack();
|
|
246
|
+
setB3ModalContentType({
|
|
247
|
+
type: "manageAccount",
|
|
248
|
+
activeTab: "settings",
|
|
249
|
+
setActiveTab: () => {},
|
|
250
|
+
chain,
|
|
251
|
+
partnerId,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
255
|
+
}, [profiles.length]);
|
|
256
|
+
|
|
257
|
+
if (!account) {
|
|
258
|
+
return <div className="text-b3-foreground-muted py-8 text-center">Please connect your account first</div>;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<div className="space-y-6 p-6">
|
|
263
|
+
<div className="flex items-center justify-between">
|
|
264
|
+
<h2 className="text-b3-grey font-neue-montreal-semibold text-2xl">Link New Account</h2>
|
|
265
|
+
{selectedMethod && (
|
|
266
|
+
<Button variant="ghost" className="text-b3-grey hover:text-b3-grey/80" onClick={handleBack}>
|
|
267
|
+
Backs
|
|
268
|
+
</Button>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
{!selectedMethod ? (
|
|
273
|
+
<div className="grid gap-3">
|
|
274
|
+
{availableAuthMethods.map(method => (
|
|
275
|
+
<Button
|
|
276
|
+
key={method.id}
|
|
277
|
+
className="bg-b3-primary-wash hover:bg-b3-primary-wash/70 text-b3-grey font-neue-montreal-semibold h-16 justify-start px-6 text-lg"
|
|
278
|
+
onClick={() => {
|
|
279
|
+
if (method.id === "email" || method.id === "phone") {
|
|
280
|
+
setSelectedMethod(method.id);
|
|
281
|
+
} else {
|
|
282
|
+
handleSocialLink(method.id as SocialStrategy);
|
|
283
|
+
}
|
|
284
|
+
}}
|
|
285
|
+
disabled={linkingMethod === method.id}
|
|
286
|
+
>
|
|
287
|
+
{isLinking && linkingMethod === method.id ? <Loader2 className="animate-spin" /> : method.label}
|
|
288
|
+
</Button>
|
|
289
|
+
))}
|
|
290
|
+
{availableAuthMethods.length === 0 && (
|
|
291
|
+
<div className="text-b3-foreground-muted py-8 text-center">
|
|
292
|
+
All available authentication methods have been connected
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
) : (
|
|
297
|
+
<div className="space-y-4">
|
|
298
|
+
{selectedMethod === "email" && (
|
|
299
|
+
<div className="space-y-2">
|
|
300
|
+
<label className="text-b3-grey font-neue-montreal-medium text-sm">Email Address</label>
|
|
301
|
+
<input
|
|
302
|
+
type="email"
|
|
303
|
+
placeholder="Enter your email"
|
|
304
|
+
className="bg-b3-line text-b3-grey font-neue-montreal-medium focus:ring-b3-primary-blue/20 w-full rounded-xl p-4 focus:outline-none focus:ring-2"
|
|
305
|
+
value={email}
|
|
306
|
+
onChange={e => setEmail(e.target.value)}
|
|
307
|
+
disabled={otpSent || (isLinking && linkingMethod === "email")}
|
|
308
|
+
/>
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
|
|
312
|
+
{selectedMethod === "phone" && (
|
|
313
|
+
<div className="space-y-2">
|
|
314
|
+
<label className="text-b3-grey font-neue-montreal-medium text-sm">Phone Number</label>
|
|
315
|
+
<input
|
|
316
|
+
type="tel"
|
|
317
|
+
placeholder="Enter your phone number"
|
|
318
|
+
className="bg-b3-line text-b3-grey font-neue-montreal-medium focus:ring-b3-primary-blue/20 w-full rounded-xl p-4 focus:outline-none focus:ring-2"
|
|
319
|
+
value={phone}
|
|
320
|
+
onChange={e => setPhone(e.target.value)}
|
|
321
|
+
disabled={otpSent || (isLinking && linkingMethod === "phone")}
|
|
322
|
+
/>
|
|
323
|
+
<p className="text-b3-foreground-muted font-neue-montreal-medium text-sm">
|
|
324
|
+
Include country code (e.g., +1 for US)
|
|
325
|
+
</p>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
|
|
329
|
+
{error && <div className="text-b3-negative font-neue-montreal-medium py-2 text-sm">{error}</div>}
|
|
330
|
+
|
|
331
|
+
{otpSent ? (
|
|
332
|
+
<div className="space-y-4">
|
|
333
|
+
<div className="space-y-2">
|
|
334
|
+
<label className="text-b3-grey font-neue-montreal-medium text-sm">Verification Code</label>
|
|
335
|
+
<input
|
|
336
|
+
type="text"
|
|
337
|
+
placeholder="Enter verification code"
|
|
338
|
+
className="bg-b3-line text-b3-grey font-neue-montreal-medium focus:ring-b3-primary-blue/20 w-full rounded-xl p-4 focus:outline-none focus:ring-2"
|
|
339
|
+
value={otp}
|
|
340
|
+
onChange={e => setOtp(e.target.value)}
|
|
341
|
+
disabled={isLinking && linkingMethod === selectedMethod}
|
|
342
|
+
/>
|
|
343
|
+
</div>
|
|
344
|
+
<Button
|
|
345
|
+
className="bg-b3-primary-blue hover:bg-b3-primary-blue/90 font-neue-montreal-semibold h-12 w-full text-white"
|
|
346
|
+
onClick={handleLinkAccount}
|
|
347
|
+
disabled={!otp || (isLinking && linkingMethod === selectedMethod)}
|
|
348
|
+
>
|
|
349
|
+
{isLinking && linkingMethod === selectedMethod ? <Loader2 className="animate-spin" /> : "Link Account"}
|
|
350
|
+
</Button>
|
|
351
|
+
</div>
|
|
352
|
+
) : (
|
|
353
|
+
<Button
|
|
354
|
+
className="bg-b3-primary-blue hover:bg-b3-primary-blue/90 font-neue-montreal-semibold h-12 w-full text-white"
|
|
355
|
+
onClick={handleSendOTP}
|
|
356
|
+
disabled={(!email && !phone) || (isLinking && linkingMethod === selectedMethod)}
|
|
357
|
+
>
|
|
358
|
+
{isLinking && linkingMethod === selectedMethod ? (
|
|
359
|
+
<Loader2 className="animate-spin" />
|
|
360
|
+
) : (
|
|
361
|
+
"Send Verification Code"
|
|
362
|
+
)}
|
|
363
|
+
</Button>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
)}
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Button,
|
|
3
3
|
CopyToClipboard,
|
|
4
|
+
ManageAccountModalProps,
|
|
4
5
|
TabsContentPrimitive,
|
|
5
6
|
TabsListPrimitive,
|
|
6
7
|
TabsPrimitive,
|
|
@@ -20,14 +21,18 @@ import { SignOutIcon } from "@b3dotfun/sdk/global-account/react/components/icons
|
|
|
20
21
|
import { SwapIcon } from "@b3dotfun/sdk/global-account/react/components/icons/SwapIcon";
|
|
21
22
|
import { formatUsername } from "@b3dotfun/sdk/shared/utils";
|
|
22
23
|
import { formatNumber } from "@b3dotfun/sdk/shared/utils/formatNumber";
|
|
23
|
-
import {
|
|
24
|
+
import { client } from "@b3dotfun/sdk/shared/utils/thirdweb";
|
|
25
|
+
import { LinkIcon, Loader2, Pencil, Triangle, UnlinkIcon } from "lucide-react";
|
|
24
26
|
import { useState } from "react";
|
|
25
27
|
import { Chain } from "thirdweb";
|
|
26
|
-
import { useActiveAccount } from "thirdweb/react";
|
|
28
|
+
import { useActiveAccount, useProfiles, useUnlinkProfile } from "thirdweb/react";
|
|
27
29
|
import { formatUnits } from "viem";
|
|
28
30
|
import useFirstEOA from "../../hooks/useFirstEOA";
|
|
31
|
+
import { getProfileDisplayInfo } from "../../utils/profileDisplay";
|
|
29
32
|
import { AccountAssets } from "../AccountAssets/AccountAssets";
|
|
30
33
|
|
|
34
|
+
type TabValue = "balance" | "assets" | "apps" | "settings";
|
|
35
|
+
|
|
31
36
|
interface ManageAccountProps {
|
|
32
37
|
onLogout?: () => void;
|
|
33
38
|
onSwap?: () => void;
|
|
@@ -50,7 +55,6 @@ export function ManageAccount({
|
|
|
50
55
|
chain,
|
|
51
56
|
partnerId,
|
|
52
57
|
}: ManageAccountProps) {
|
|
53
|
-
const [activeTab, setActiveTab] = useState("balance");
|
|
54
58
|
const [revokingSignerId, setRevokingSignerId] = useState<string | null>(null);
|
|
55
59
|
const account = useActiveAccount();
|
|
56
60
|
const { data: assets, isLoading } = useAccountAssets(account?.address);
|
|
@@ -67,7 +71,8 @@ export function ManageAccount({
|
|
|
67
71
|
chain,
|
|
68
72
|
accountAddress: account?.address,
|
|
69
73
|
});
|
|
70
|
-
const { setB3ModalOpen, setB3ModalContentType } = useModalStore();
|
|
74
|
+
const { setB3ModalOpen, setB3ModalContentType, contentType } = useModalStore();
|
|
75
|
+
const { activeTab = "balance", setActiveTab } = contentType as ManageAccountModalProps;
|
|
71
76
|
const { logout } = useAuthentication(partnerId);
|
|
72
77
|
const [logoutLoading, setLogoutLoading] = useState(false);
|
|
73
78
|
|
|
@@ -408,10 +413,177 @@ export function ManageAccount({
|
|
|
408
413
|
</div>
|
|
409
414
|
);
|
|
410
415
|
|
|
416
|
+
const SettingsContent = () => {
|
|
417
|
+
const [unlinkingAccountId, setUnlinkingAccountId] = useState<string | null>(null);
|
|
418
|
+
const { data: profilesRaw = [], isLoading: isLoadingProfiles } = useProfiles({ client });
|
|
419
|
+
const { mutate: unlinkProfile, isPending: isUnlinking } = useUnlinkProfile();
|
|
420
|
+
const { setB3ModalOpen, setB3ModalContentType, isLinking } = useModalStore();
|
|
421
|
+
|
|
422
|
+
const profiles = profilesRaw
|
|
423
|
+
.filter((profile: any) => !["custom_auth_endpoint", "siwe"].includes(profile.type))
|
|
424
|
+
.map((profile: any) => ({
|
|
425
|
+
...getProfileDisplayInfo(profile),
|
|
426
|
+
originalProfile: profile,
|
|
427
|
+
}));
|
|
428
|
+
|
|
429
|
+
const handleUnlink = async (profile: any) => {
|
|
430
|
+
setUnlinkingAccountId(profile.title);
|
|
431
|
+
try {
|
|
432
|
+
await unlinkProfile({
|
|
433
|
+
client,
|
|
434
|
+
profileToUnlink: profile.originalProfile,
|
|
435
|
+
});
|
|
436
|
+
} catch (error) {
|
|
437
|
+
console.error("Error unlinking account:", error);
|
|
438
|
+
} finally {
|
|
439
|
+
setUnlinkingAccountId(null);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const handleOpenLinkModal = () => {
|
|
444
|
+
setB3ModalOpen(true);
|
|
445
|
+
setB3ModalContentType({
|
|
446
|
+
type: "linkAccount",
|
|
447
|
+
showBackButton: true,
|
|
448
|
+
partnerId,
|
|
449
|
+
chain,
|
|
450
|
+
onSuccess: async () => {
|
|
451
|
+
// Let the LinkAccount component handle modal closing
|
|
452
|
+
},
|
|
453
|
+
onError: () => {
|
|
454
|
+
// Let the LinkAccount component handle errors
|
|
455
|
+
},
|
|
456
|
+
onClose: () => {
|
|
457
|
+
// Let the LinkAccount component handle closing
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
return (
|
|
463
|
+
<div className="space-y-8">
|
|
464
|
+
{/* Linked Accounts Section */}
|
|
465
|
+
<div className="space-y-4">
|
|
466
|
+
<div className="flex items-center justify-between">
|
|
467
|
+
<h3 className="text-b3-grey font-neue-montreal-semibold text-xl">Linked Accounts</h3>
|
|
468
|
+
<Button
|
|
469
|
+
className="bg-b3-primary-wash hover:bg-b3-primary-wash/70 flex items-center gap-2 rounded-full px-4 py-2"
|
|
470
|
+
onClick={handleOpenLinkModal}
|
|
471
|
+
disabled={isLinking}
|
|
472
|
+
>
|
|
473
|
+
{isLinking ? (
|
|
474
|
+
<Loader2 className="text-b3-primary-blue animate-spin" size={16} />
|
|
475
|
+
) : (
|
|
476
|
+
<LinkIcon size={16} className="text-b3-primary-blue" />
|
|
477
|
+
)}
|
|
478
|
+
<span className="text-b3-grey font-neue-montreal-semibold">
|
|
479
|
+
{isLinking ? "Linking..." : "Link New Account"}
|
|
480
|
+
</span>
|
|
481
|
+
</Button>
|
|
482
|
+
</div>
|
|
483
|
+
|
|
484
|
+
{isLoadingProfiles ? (
|
|
485
|
+
<div className="flex justify-center py-8">
|
|
486
|
+
<Loader2 className="text-b3-grey animate-spin" />
|
|
487
|
+
</div>
|
|
488
|
+
) : profiles.length > 0 ? (
|
|
489
|
+
<div className="space-y-4">
|
|
490
|
+
{profiles.map(profile => (
|
|
491
|
+
<div key={profile.title} className="bg-b3-line flex items-center justify-between rounded-xl p-4">
|
|
492
|
+
<div className="flex items-center gap-3">
|
|
493
|
+
{profile.imageUrl ? (
|
|
494
|
+
<img src={profile.imageUrl} alt={profile.title} className="size-10 rounded-full" />
|
|
495
|
+
) : (
|
|
496
|
+
<div className="bg-b3-primary-wash flex h-10 w-10 items-center justify-center rounded-full">
|
|
497
|
+
<span className="text-b3-grey font-neue-montreal-semibold text-sm uppercase">
|
|
498
|
+
{profile.initial}
|
|
499
|
+
</span>
|
|
500
|
+
</div>
|
|
501
|
+
)}
|
|
502
|
+
<div>
|
|
503
|
+
<div className="flex items-center gap-2">
|
|
504
|
+
<span className="text-b3-grey font-neue-montreal-semibold">{profile.title}</span>
|
|
505
|
+
<span className="text-b3-foreground-muted font-neue-montreal-medium bg-b3-primary-wash rounded px-2 py-0.5 text-xs">
|
|
506
|
+
{profile.type.toUpperCase()}
|
|
507
|
+
</span>
|
|
508
|
+
</div>
|
|
509
|
+
<div className="text-b3-foreground-muted font-neue-montreal-medium text-sm">
|
|
510
|
+
{profile.subtitle}
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
<Button
|
|
515
|
+
variant="ghost"
|
|
516
|
+
size="icon"
|
|
517
|
+
className="text-b3-grey hover:text-b3-negative"
|
|
518
|
+
onClick={() => handleUnlink(profile)}
|
|
519
|
+
disabled={unlinkingAccountId === profile.title || isUnlinking}
|
|
520
|
+
>
|
|
521
|
+
{unlinkingAccountId === profile.title || isUnlinking ? (
|
|
522
|
+
<Loader2 className="animate-spin" />
|
|
523
|
+
) : (
|
|
524
|
+
<UnlinkIcon size={16} />
|
|
525
|
+
)}
|
|
526
|
+
</Button>
|
|
527
|
+
</div>
|
|
528
|
+
))}
|
|
529
|
+
</div>
|
|
530
|
+
) : (
|
|
531
|
+
<div className="text-b3-foreground-muted py-8 text-center">No linked accounts found</div>
|
|
532
|
+
)}
|
|
533
|
+
</div>
|
|
534
|
+
|
|
535
|
+
{/* Additional Settings Sections */}
|
|
536
|
+
<div className="space-y-4">
|
|
537
|
+
<h3 className="text-b3-grey font-neue-montreal-semibold text-xl">Account Preferences</h3>
|
|
538
|
+
<div className="bg-b3-line rounded-xl p-4">
|
|
539
|
+
<div className="flex items-center justify-between">
|
|
540
|
+
<div>
|
|
541
|
+
<div className="text-b3-grey font-neue-montreal-semibold">Dark Mode</div>
|
|
542
|
+
<div className="text-b3-foreground-muted font-neue-montreal-medium text-sm">
|
|
543
|
+
Switch between light and dark theme
|
|
544
|
+
</div>
|
|
545
|
+
</div>
|
|
546
|
+
{/* Theme toggle placeholder - can be implemented later */}
|
|
547
|
+
<div className="bg-b3-primary-wash h-6 w-12 rounded-full"></div>
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
|
|
552
|
+
{/* Global Account Info */}
|
|
553
|
+
<div className="border-b3-line flex items-center justify-between rounded-2xl border p-4">
|
|
554
|
+
<div>
|
|
555
|
+
<div className="flex items-center gap-2">
|
|
556
|
+
<img src="https://cdn.b3.fun/b3_logo.svg" alt="B3" className="h-4" />
|
|
557
|
+
<h3 className="font-neue-montreal-semibold text-b3-grey">Global Account</h3>
|
|
558
|
+
</div>
|
|
559
|
+
|
|
560
|
+
<p className="text-b3-foreground-muted font-neue-montreal-medium mt-2 text-sm">
|
|
561
|
+
Your universal account for all B3-powered apps
|
|
562
|
+
</p>
|
|
563
|
+
</div>
|
|
564
|
+
<button
|
|
565
|
+
className="text-b3-grey hover:text-b3-grey/80 hover:bg-b3-line border-b3-line flex size-12 items-center justify-center rounded-full border"
|
|
566
|
+
onClick={onLogoutEnhanced}
|
|
567
|
+
>
|
|
568
|
+
{logoutLoading ? <Loader2 className="animate-spin" /> : <SignOutIcon size={16} className="text-b3-grey" />}
|
|
569
|
+
</button>
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
572
|
+
);
|
|
573
|
+
};
|
|
574
|
+
|
|
411
575
|
return (
|
|
412
576
|
<div className="b3-manage-account bg-b3-background flex flex-col rounded-xl">
|
|
413
577
|
<div className="flex-1">
|
|
414
|
-
<TabsPrimitive
|
|
578
|
+
<TabsPrimitive
|
|
579
|
+
defaultValue={activeTab}
|
|
580
|
+
onValueChange={value => {
|
|
581
|
+
const tab = value as TabValue;
|
|
582
|
+
if (["balance", "assets", "apps", "settings"].includes(tab)) {
|
|
583
|
+
setActiveTab?.(tab);
|
|
584
|
+
}
|
|
585
|
+
}}
|
|
586
|
+
>
|
|
415
587
|
<TabsListPrimitive className="font-neue-montreal-semibold text-b3-grey flex h-8 w-full items-start justify-start gap-8 border-0 text-xl md:p-4">
|
|
416
588
|
<TabTriggerPrimitive
|
|
417
589
|
value="balance"
|
|
@@ -431,6 +603,12 @@ export function ManageAccount({
|
|
|
431
603
|
>
|
|
432
604
|
Apps
|
|
433
605
|
</TabTriggerPrimitive>
|
|
606
|
+
<TabTriggerPrimitive
|
|
607
|
+
value="settings"
|
|
608
|
+
className="data-[state=active]:text-b3-primary-blue data-[state=active]:border-b-b3-primary-blue flex-none rounded-none border-0 p-0 pb-1 text-xl leading-none tracking-wide transition-colors data-[state=active]:border-b data-[state=active]:bg-white md:pb-4"
|
|
609
|
+
>
|
|
610
|
+
Settings
|
|
611
|
+
</TabTriggerPrimitive>
|
|
434
612
|
</TabsListPrimitive>
|
|
435
613
|
|
|
436
614
|
<TabsContentPrimitive value="balance" className="pt-4 md:p-4">
|
|
@@ -444,6 +622,10 @@ export function ManageAccount({
|
|
|
444
622
|
<TabsContentPrimitive value="apps" className="pt-4 md:p-4">
|
|
445
623
|
<AppsContent />
|
|
446
624
|
</TabsContentPrimitive>
|
|
625
|
+
|
|
626
|
+
<TabsContentPrimitive value="settings" className="pt-4 md:p-4">
|
|
627
|
+
<SettingsContent />
|
|
628
|
+
</TabsContentPrimitive>
|
|
447
629
|
</TabsPrimitive>
|
|
448
630
|
</div>
|
|
449
631
|
</div>
|
|
@@ -83,6 +83,10 @@ export interface ManageAccountModalProps extends BaseModalProps {
|
|
|
83
83
|
chain: Chain;
|
|
84
84
|
/** Partner ID */
|
|
85
85
|
partnerId: string;
|
|
86
|
+
/** Active Tab */
|
|
87
|
+
activeTab?: "balance" | "assets" | "apps" | "settings";
|
|
88
|
+
/** Function to set the active tab */
|
|
89
|
+
setActiveTab?: (tab: "balance" | "assets" | "apps" | "settings") => void;
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
/**
|
|
@@ -291,6 +295,16 @@ export interface AnySpendBondKitProps extends BaseModalProps {
|
|
|
291
295
|
onSuccess?: (txHash?: string) => void;
|
|
292
296
|
}
|
|
293
297
|
|
|
298
|
+
export interface LinkAccountModalProps extends BaseModalProps {
|
|
299
|
+
type: "linkAccount";
|
|
300
|
+
showBackButton?: boolean;
|
|
301
|
+
onSuccess?: () => void;
|
|
302
|
+
onError?: (error: Error) => void;
|
|
303
|
+
onClose?: () => void;
|
|
304
|
+
partnerId: string;
|
|
305
|
+
chain: Chain;
|
|
306
|
+
}
|
|
307
|
+
|
|
294
308
|
/**
|
|
295
309
|
* Union type of all possible modal content types
|
|
296
310
|
*/
|
|
@@ -308,7 +322,8 @@ export type ModalContentType =
|
|
|
308
322
|
| AnySpendStakeB3Props
|
|
309
323
|
| AnySpendBuySpinProps
|
|
310
324
|
| AnySpendSignatureMintProps
|
|
311
|
-
| AnySpendBondKitProps
|
|
325
|
+
| AnySpendBondKitProps
|
|
326
|
+
| LinkAccountModalProps;
|
|
312
327
|
// Add other modal types here like: | OtherModalProps | AnotherModalProps
|
|
313
328
|
|
|
314
329
|
/**
|
|
@@ -333,6 +348,12 @@ interface ModalState {
|
|
|
333
348
|
ecoSystemAccountAddress?: Address;
|
|
334
349
|
/** Function to set the ecosystem account address */
|
|
335
350
|
setEcoSystemAccountAddress: (address: Address) => void;
|
|
351
|
+
/** Whether an account linking operation is in progress */
|
|
352
|
+
isLinking: boolean;
|
|
353
|
+
/** The method currently being linked */
|
|
354
|
+
linkingMethod: string | null;
|
|
355
|
+
/** Function to set the linking state */
|
|
356
|
+
setLinkingState: (isLinking: boolean, method?: string | null) => void;
|
|
336
357
|
}
|
|
337
358
|
|
|
338
359
|
/**
|
|
@@ -368,4 +389,8 @@ export const useModalStore = create<ModalState>(set => ({
|
|
|
368
389
|
clearHistory: () => set({ history: [] }),
|
|
369
390
|
ecoSystemAccountAddress: undefined,
|
|
370
391
|
setEcoSystemAccountAddress: (address: Address) => set({ ecoSystemAccountAddress: address }),
|
|
392
|
+
isLinking: false,
|
|
393
|
+
linkingMethod: null,
|
|
394
|
+
setLinkingState: (isLinking: boolean, method: string | null = null) =>
|
|
395
|
+
set({ isLinking, linkingMethod: isLinking ? method : null }),
|
|
371
396
|
}));
|