@b3dotfun/sdk 0.0.26 → 0.0.27-alpha.0

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.
Files changed (59) hide show
  1. package/dist/cjs/anyspend/react/components/AnySpendCustom.d.ts +1 -0
  2. package/dist/cjs/anyspend/react/components/AnySpendCustom.js +3 -2
  3. package/dist/cjs/anyspend/react/components/AnySpendNFT.js +2 -2
  4. package/dist/cjs/anyspend/react/components/common/PaymentStripeWeb2.d.ts +2 -1
  5. package/dist/cjs/anyspend/react/components/common/PaymentStripeWeb2.js +3 -3
  6. package/dist/cjs/anyspend/react/components/common/PaymentVendorUI.js +2 -2
  7. package/dist/cjs/anyspend/utils/chain.js +2 -2
  8. package/dist/cjs/global-account/react/components/B3DynamicModal.js +4 -0
  9. package/dist/cjs/global-account/react/components/LinkAccount/LinkAccount.d.ts +2 -0
  10. package/dist/cjs/global-account/react/components/LinkAccount/LinkAccount.js +228 -0
  11. package/dist/cjs/global-account/react/components/ManageAccount/ManageAccount.js +56 -3
  12. package/dist/cjs/global-account/react/components/SignInWithB3/steps/LoginStep.js +1 -1
  13. package/dist/cjs/global-account/react/components/custom/Button.d.ts +1 -1
  14. package/dist/cjs/global-account/react/components/ui/button.d.ts +1 -1
  15. package/dist/cjs/global-account/react/hooks/useUnifiedChainSwitchAndExecute.js +8 -2
  16. package/dist/cjs/global-account/react/stores/useModalStore.d.ts +20 -1
  17. package/dist/cjs/global-account/react/stores/useModalStore.js +3 -0
  18. package/dist/cjs/global-account/react/utils/profileDisplay.d.ts +21 -0
  19. package/dist/cjs/global-account/react/utils/profileDisplay.js +63 -0
  20. package/dist/esm/anyspend/react/components/AnySpendCustom.d.ts +1 -0
  21. package/dist/esm/anyspend/react/components/AnySpendCustom.js +3 -2
  22. package/dist/esm/anyspend/react/components/AnySpendNFT.js +2 -2
  23. package/dist/esm/anyspend/react/components/common/PaymentStripeWeb2.d.ts +2 -1
  24. package/dist/esm/anyspend/react/components/common/PaymentStripeWeb2.js +3 -3
  25. package/dist/esm/anyspend/react/components/common/PaymentVendorUI.js +2 -2
  26. package/dist/esm/anyspend/utils/chain.js +2 -2
  27. package/dist/esm/global-account/react/components/B3DynamicModal.js +4 -0
  28. package/dist/esm/global-account/react/components/LinkAccount/LinkAccount.d.ts +2 -0
  29. package/dist/esm/global-account/react/components/LinkAccount/LinkAccount.js +225 -0
  30. package/dist/esm/global-account/react/components/ManageAccount/ManageAccount.js +58 -5
  31. package/dist/esm/global-account/react/components/SignInWithB3/steps/LoginStep.js +1 -1
  32. package/dist/esm/global-account/react/components/custom/Button.d.ts +1 -1
  33. package/dist/esm/global-account/react/components/ui/button.d.ts +1 -1
  34. package/dist/esm/global-account/react/hooks/useUnifiedChainSwitchAndExecute.js +8 -2
  35. package/dist/esm/global-account/react/stores/useModalStore.d.ts +20 -1
  36. package/dist/esm/global-account/react/stores/useModalStore.js +3 -0
  37. package/dist/esm/global-account/react/utils/profileDisplay.d.ts +21 -0
  38. package/dist/esm/global-account/react/utils/profileDisplay.js +60 -0
  39. package/dist/styles/index.css +1 -1
  40. package/dist/types/anyspend/react/components/AnySpendCustom.d.ts +1 -0
  41. package/dist/types/anyspend/react/components/common/PaymentStripeWeb2.d.ts +2 -1
  42. package/dist/types/global-account/react/components/LinkAccount/LinkAccount.d.ts +2 -0
  43. package/dist/types/global-account/react/components/custom/Button.d.ts +1 -1
  44. package/dist/types/global-account/react/components/ui/button.d.ts +1 -1
  45. package/dist/types/global-account/react/stores/useModalStore.d.ts +20 -1
  46. package/dist/types/global-account/react/utils/profileDisplay.d.ts +21 -0
  47. package/package.json +1 -1
  48. package/src/anyspend/react/components/AnySpendCustom.tsx +7 -3
  49. package/src/anyspend/react/components/AnySpendNFT.tsx +2 -1
  50. package/src/anyspend/react/components/common/PaymentStripeWeb2.tsx +5 -28
  51. package/src/anyspend/react/components/common/PaymentVendorUI.tsx +2 -2
  52. package/src/anyspend/utils/chain.ts +2 -2
  53. package/src/global-account/react/components/B3DynamicModal.tsx +4 -0
  54. package/src/global-account/react/components/LinkAccount/LinkAccount.tsx +369 -0
  55. package/src/global-account/react/components/ManageAccount/ManageAccount.tsx +187 -5
  56. package/src/global-account/react/components/SignInWithB3/steps/LoginStep.tsx +3 -1
  57. package/src/global-account/react/hooks/useUnifiedChainSwitchAndExecute.ts +9 -2
  58. package/src/global-account/react/stores/useModalStore.ts +26 -1
  59. package/src/global-account/react/utils/profileDisplay.ts +87 -0
@@ -0,0 +1,21 @@
1
+ import { type Profile } from "thirdweb/wallets";
2
+ export interface ExtendedProfileDetails {
3
+ id?: string;
4
+ email?: string;
5
+ phone?: string;
6
+ address?: string;
7
+ name?: string;
8
+ username?: string;
9
+ profileImageUrl?: string;
10
+ }
11
+ export interface ExtendedProfile extends Omit<Profile, "details"> {
12
+ details: ExtendedProfileDetails;
13
+ }
14
+ export interface ProfileDisplayInfo {
15
+ title: string;
16
+ subtitle: string;
17
+ imageUrl: string | null;
18
+ initial: string;
19
+ type: Profile["type"];
20
+ }
21
+ export declare function getProfileDisplayInfo(profile: ExtendedProfile): ProfileDisplayInfo;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b3dotfun/sdk",
3
- "version": "0.0.26",
3
+ "version": "0.0.27-alpha.0",
4
4
  "source": "src/index.ts",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "react-native": "./dist/cjs/index.native.js",
@@ -88,6 +88,7 @@ function generateGetRelayQuoteRequest({
88
88
  }): GetQuoteRequest {
89
89
  switch (orderType) {
90
90
  case "mint_nft": {
91
+ invariant(contractType, "Contract type is required");
91
92
  return {
92
93
  type: "mint_nft",
93
94
  srcChain: srcChainId,
@@ -96,8 +97,8 @@ function generateGetRelayQuoteRequest({
96
97
  dstTokenAddress: dstToken.address,
97
98
  price: dstAmount,
98
99
  contractAddress: contractAddress,
99
- tokenId: tokenId!,
100
- contractType: contractType!,
100
+ tokenId: tokenId,
101
+ contractType: contractType,
101
102
  };
102
103
  }
103
104
  case "join_tournament": {
@@ -146,6 +147,7 @@ function generateGetRelayQuoteRequest({
146
147
  export function AnySpendCustom(props: {
147
148
  loadOrder?: string;
148
149
  mode?: "modal" | "page";
150
+ activeTab?: "crypto" | "fiat";
149
151
  recipientAddress?: string;
150
152
  spenderAddress?: string;
151
153
  orderType: components["schemas"]["Order"]["type"];
@@ -177,6 +179,7 @@ export function AnySpendCustom(props: {
177
179
  function AnySpendCustomInner({
178
180
  loadOrder,
179
181
  mode = "modal",
182
+ activeTab: activeTabProps = "crypto",
180
183
  recipientAddress: recipientAddressProps,
181
184
  spenderAddress,
182
185
  orderType,
@@ -192,6 +195,7 @@ function AnySpendCustomInner({
192
195
  }: {
193
196
  loadOrder?: string;
194
197
  mode?: "modal" | "page";
198
+ activeTab?: "crypto" | "fiat";
195
199
  recipientAddress?: string;
196
200
  spenderAddress?: string;
197
201
  orderType: components["schemas"]["Order"]["type"];
@@ -219,7 +223,7 @@ function AnySpendCustomInner({
219
223
  const [activePanel, setActivePanel] = useState<PanelView>(
220
224
  loadOrder ? PanelView.ORDER_DETAILS : PanelView.CONFIRM_ORDER,
221
225
  );
222
- const [activeTab, setActiveTab] = useState<"crypto" | "fiat">("crypto");
226
+ const [activeTab, setActiveTab] = useState<"crypto" | "fiat">(activeTabProps);
223
227
 
224
228
  // Add state for selected payment methods
225
229
  const [selectedCryptoPaymentMethod, setSelectedCryptoPaymentMethod] = useState<CryptoPaymentMethodType>(
@@ -100,7 +100,7 @@ export function AnySpendNFT({
100
100
  }
101
101
 
102
102
  fetchContractMetadata();
103
- }, [nftContract.contractAddress, nftContract.chainId, nftContract.imageUrl, nftContract.tokenId]);
103
+ }, [nftContract.contractAddress, nftContract.chainId, nftContract.imageUrl, nftContract.tokenId, isLoadingFallback]);
104
104
 
105
105
  const header = ({
106
106
  anyspendPrice,
@@ -150,6 +150,7 @@ export function AnySpendNFT({
150
150
  <AnySpendCustom
151
151
  loadOrder={loadOrder}
152
152
  mode={mode}
153
+ activeTab="fiat"
153
154
  recipientAddress={recipientAddress}
154
155
  orderType={"mint_nft"}
155
156
  dstChainId={nftContract.chainId}
@@ -16,16 +16,16 @@ const stripePromise = loadStripe(STRIPE_CONFIG.publishableKey);
16
16
 
17
17
  interface PaymentStripeWeb2Props {
18
18
  order: components["schemas"]["Order"];
19
+ stripePaymentIntentId: string;
19
20
  onPaymentSuccess?: (paymentIntent: any) => void;
20
21
  }
21
22
 
22
- export default function PaymentStripeWeb2({ order, onPaymentSuccess }: PaymentStripeWeb2Props) {
23
+ export default function PaymentStripeWeb2({ order, stripePaymentIntentId, onPaymentSuccess }: PaymentStripeWeb2Props) {
23
24
  const { theme } = useB3();
24
25
  const fingerprintConfig = getFingerprintConfig();
25
26
 
26
- const { clientSecret, isLoadingStripeClientSecret, stripeClientSecretError } = useStripeClientSecret(
27
- order.stripePaymentIntentId!,
28
- );
27
+ const { clientSecret, isLoadingStripeClientSecret, stripeClientSecretError } =
28
+ useStripeClientSecret(stripePaymentIntentId);
29
29
 
30
30
  if (isLoadingStripeClientSecret) {
31
31
  return <StripeLoadingState />;
@@ -264,7 +264,7 @@ function StripePaymentForm({
264
264
  // Validation
265
265
  validation: {
266
266
  phone: {
267
- required: "auto", // or 'always', 'never'
267
+ required: "always", // or 'always', 'never'
268
268
  },
269
269
  },
270
270
  }}
@@ -331,26 +331,3 @@ function StripePaymentForm({
331
331
  </div>
332
332
  );
333
333
  }
334
-
335
- // Add tooltip component
336
- // function Tooltip({ children, content }: { children: React.ReactNode; content: string }) {
337
- // const [isVisible, setIsVisible] = useState(false);
338
-
339
- // return (
340
- // <div className="relative inline-block">
341
- // <div onMouseEnter={() => setIsVisible(true)} onMouseLeave={() => setIsVisible(false)} className="cursor-help">
342
- // {children}
343
- // </div>
344
- // {isVisible && (
345
- // <div className="absolute bottom-full left-1/2 z-50 mb-2 w-80 -translate-x-1/2">
346
- // <div className="bg-as-on-surface-1 border-as-stroke text-as-primary rounded-lg border p-3 text-sm shadow-lg">
347
- // <div className="whitespace-pre-line">{content}</div>
348
- // <div className="absolute left-1/2 top-full -translate-x-1/2">
349
- // <div className="border-t-as-on-surface-1 border-l-4 border-r-4 border-t-4 border-transparent"></div>
350
- // </div>
351
- // </div>
352
- // </div>
353
- // )}
354
- // </div>
355
- // );
356
- // }
@@ -16,8 +16,8 @@ export default function PaymentVendorUI({ order, dstTokenSymbol }: PaymentVendor
16
16
  }
17
17
 
18
18
  // Handle Stripe Web2 payment flow
19
- if (vendor === "stripe-web2") {
20
- return <PaymentStripeWeb2 order={order} />;
19
+ if (vendor === "stripe-web2" && order.stripePaymentIntentId) {
20
+ return <PaymentStripeWeb2 order={order} stripePaymentIntentId={order.stripePaymentIntentId} />;
21
21
  }
22
22
 
23
23
  // Return null for unsupported vendors
@@ -132,7 +132,7 @@ export const EVM_MAINNET: Record<number, IEVMChain> = {
132
132
  name: avalanche.name,
133
133
  logoUrl: "https://assets.relay.link/icons/square/43114/light.png",
134
134
  type: ChainType.EVM,
135
- nativeRequired: parseEther("0.005"),
135
+ nativeRequired: parseEther("0.01"),
136
136
  canDepositNative: true,
137
137
  defaultToken: getAvaxToken(),
138
138
  nativeToken: getAvaxToken(),
@@ -149,7 +149,7 @@ export const EVM_MAINNET: Record<number, IEVMChain> = {
149
149
  name: bsc.name,
150
150
  logoUrl: "https://avatars.githubusercontent.com/u/45615063?s=280&v=4",
151
151
  type: ChainType.EVM,
152
- nativeRequired: parseEther("0.00001"),
152
+ nativeRequired: parseEther("0.000025"),
153
153
  canDepositNative: true,
154
154
  defaultToken: getBnbToken(),
155
155
  nativeToken: getBnbToken(),
@@ -12,6 +12,7 @@ import { useIsMobile, useModalStore } from "@b3dotfun/sdk/global-account/react";
12
12
  import { cn } from "@b3dotfun/sdk/shared/utils/cn";
13
13
  import { debugB3React } from "@b3dotfun/sdk/shared/utils/debug";
14
14
  import { useB3 } from "./B3Provider/useB3";
15
+ import { LinkAccount } from "./LinkAccount/LinkAccount";
15
16
  import { ManageAccount } from "./ManageAccount/ManageAccount";
16
17
  import { RequestPermissions } from "./RequestPermissions/RequestPermissions";
17
18
  import { SignInWithB3Flow } from "./SignInWithB3/SignInWithB3Flow";
@@ -40,6 +41,7 @@ export function B3DynamicModal() {
40
41
  "signInWithB3",
41
42
  "anySpendSignatureMint",
42
43
  "anySpendBondKit",
44
+ "linkAccount",
43
45
  ];
44
46
 
45
47
  const freestyleTypes = [
@@ -103,6 +105,8 @@ export function B3DynamicModal() {
103
105
  return <AnyspendSignatureMint {...contentType} mode="modal" />;
104
106
  case "anySpendBondKit":
105
107
  return <AnySpendBondKit {...contentType} />;
108
+ case "linkAccount":
109
+ return <LinkAccount {...contentType} />;
106
110
  // Add other modal types here
107
111
  default:
108
112
  return null;
@@ -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
+ }