@b3dotfun/sdk 0.0.26 → 0.0.27-alpha.1
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/anyspend/react/components/common/PaymentStripeWeb2.d.ts +2 -1
- package/dist/cjs/anyspend/react/components/common/PaymentStripeWeb2.js +3 -3
- package/dist/cjs/anyspend/react/components/common/PaymentVendorUI.js +2 -2
- package/dist/cjs/anyspend/utils/chain.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/SignInWithB3/steps/LoginStep.js +1 -1
- 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/hooks/useUnifiedChainSwitchAndExecute.js +8 -2
- 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/anyspend/react/components/common/PaymentStripeWeb2.d.ts +2 -1
- package/dist/esm/anyspend/react/components/common/PaymentStripeWeb2.js +3 -3
- package/dist/esm/anyspend/react/components/common/PaymentVendorUI.js +2 -2
- package/dist/esm/anyspend/utils/chain.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/SignInWithB3/steps/LoginStep.js +1 -1
- 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/hooks/useUnifiedChainSwitchAndExecute.js +8 -2
- 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/anyspend/react/components/common/PaymentStripeWeb2.d.ts +2 -1
- 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 +2 -2
- package/src/anyspend/react/components/AnySpendCustom.tsx +7 -3
- package/src/anyspend/react/components/AnySpendNFT.tsx +2 -1
- package/src/anyspend/react/components/common/PaymentStripeWeb2.tsx +5 -28
- package/src/anyspend/react/components/common/PaymentVendorUI.tsx +2 -2
- package/src/anyspend/utils/chain.ts +2 -2
- 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/components/SignInWithB3/steps/LoginStep.tsx +3 -1
- package/src/global-account/react/hooks/useUnifiedChainSwitchAndExecute.ts +9 -2
- package/src/global-account/react/stores/useModalStore.ts +26 -1
- 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.
|
|
3
|
+
"version": "0.0.27-alpha.1",
|
|
4
4
|
"source": "src/index.ts",
|
|
5
5
|
"main": "./dist/cjs/index.js",
|
|
6
6
|
"react-native": "./dist/cjs/index.native.js",
|
|
@@ -226,7 +226,7 @@
|
|
|
226
226
|
],
|
|
227
227
|
"dependencies": {
|
|
228
228
|
"@amplitude/analytics-browser": "2.14.0",
|
|
229
|
-
"@b3dotfun/b3-api": "0.0.
|
|
229
|
+
"@b3dotfun/b3-api": "0.0.42",
|
|
230
230
|
"@b3dotfun/basement-api": "0.0.11",
|
|
231
231
|
"@chakra-ui/react": "2.10.7",
|
|
232
232
|
"@feathersjs/authentication-client": "5.0.33",
|
|
@@ -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">(
|
|
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 } =
|
|
27
|
-
|
|
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: "
|
|
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.
|
|
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.
|
|
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
|
+
}
|