@b3dotfun/sdk 0.1.69-alpha.3 → 0.1.69-alpha.4

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.
@@ -3,7 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AuthButton = AuthButton;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const Button_1 = require("../../custom/Button");
6
+ const lucide_react_1 = require("lucide-react");
6
7
  const signInUtils_1 = require("../utils/signInUtils");
8
+ const fallbackIcons = {
9
+ github: lucide_react_1.Github,
10
+ email: lucide_react_1.Mail,
11
+ };
7
12
  function AuthButton({ strategy, onClick, isLoading, }) {
8
- return ((0, jsx_runtime_1.jsx)(Button_1.Button, { onClick: onClick, disabled: isLoading, className: "flex w-full items-center justify-center bg-gray-100 px-2 py-3 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700", children: (0, jsx_runtime_1.jsx)("img", { src: signInUtils_1.strategyIcons[strategy], className: "h-9 w-9" }) }, strategy));
13
+ const strategyIcon = signInUtils_1.strategyIcons[strategy];
14
+ const strategyLabel = signInUtils_1.strategyLabels[strategy] || strategy;
15
+ const FallbackIcon = fallbackIcons[strategy];
16
+ const buttonLabel = `Sign in with ${strategyLabel}`;
17
+ return ((0, jsx_runtime_1.jsx)(Button_1.Button, { onClick: onClick, disabled: isLoading, "aria-label": buttonLabel, title: buttonLabel, className: "flex w-full items-center justify-center bg-gray-100 px-2 py-3 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700", children: strategyIcon ? ((0, jsx_runtime_1.jsx)("img", { src: strategyIcon, alt: `${strategyLabel} icon`, className: "h-9 w-9" })) : FallbackIcon ? ((0, jsx_runtime_1.jsx)(FallbackIcon, { className: "h-9 w-9 text-gray-900 dark:text-gray-100" })) : ((0, jsx_runtime_1.jsx)("span", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: strategyLabel.charAt(0) })) }, strategy));
9
18
  }
@@ -3,39 +3,53 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LoginStepCustom = LoginStepCustom;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("../../../../../global-account/react");
6
+ const constants_1 = require("../../../../../shared/constants");
6
7
  const debug_1 = require("../../../../../shared/utils/debug");
7
8
  const thirdweb_1 = require("../../../../../shared/utils/thirdweb");
8
9
  const react_2 = require("react");
9
10
  const react_3 = require("thirdweb/react");
10
11
  const wallets_1 = require("thirdweb/wallets");
11
12
  const debug = (0, debug_1.debugB3React)("LoginStepCustom");
13
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
12
14
  function LoginStepCustom({ onSuccess, onError, chain, strategies, maxInitialWallets = 2, automaticallySetFirstEoa, }) {
13
15
  const { partnerId } = (0, react_1.useB3Config)();
14
16
  const [isLoading, setIsLoading] = (0, react_2.useState)(false);
15
17
  const [showAllWallets, setShowAllWallets] = (0, react_2.useState)(false);
18
+ const [showEmailFlow, setShowEmailFlow] = (0, react_2.useState)(false);
19
+ const [email, setEmail] = (0, react_2.useState)("");
20
+ const [verificationCode, setVerificationCode] = (0, react_2.useState)("");
21
+ const [emailCodeSent, setEmailCodeSent] = (0, react_2.useState)(false);
22
+ const [emailError, setEmailError] = (0, react_2.useState)(null);
16
23
  const { connect } = (0, react_1.useConnect)(partnerId, chain);
17
24
  const setIsAuthenticating = (0, react_1.useAuthStore)(state => state.setIsAuthenticating);
18
- const setIsAuthenticated = (0, react_1.useAuthStore)(state => state.setIsAuthenticated);
19
- const { logout } = (0, react_1.useAuthentication)(partnerId, { skipAutoConnect: true });
25
+ const { connect: onAuthConnect, logout } = (0, react_1.useAuthentication)(partnerId, { skipAutoConnect: true });
20
26
  const { connect: connectTW } = (0, react_3.useConnect)();
27
+ const connectedWallets = (0, react_3.useConnectedWallets)();
21
28
  // Split strategies into auth and wallet types
22
29
  const authStrategies = strategies.filter(s => !(0, react_1.isWalletType)(s));
23
30
  const walletStrategies = strategies.filter(react_1.isWalletType);
24
31
  const initialWallets = walletStrategies.slice(0, maxInitialWallets);
25
32
  const additionalWallets = walletStrategies.slice(maxInitialWallets);
26
- const handleConnect = async (strategy) => {
33
+ const authGridColumns = Math.max(1, Math.min(authStrategies.length, 4));
34
+ const resetEmailFlow = () => {
35
+ setShowEmailFlow(false);
36
+ setEmailCodeSent(false);
37
+ setVerificationCode("");
38
+ setEmailError(null);
39
+ };
40
+ const connectWithOptions = async (strategy, options) => {
27
41
  try {
28
42
  setIsLoading(true);
29
43
  debug("setIsAuthenticating:true:3");
30
44
  setIsAuthenticating(true);
31
- const options = (0, react_1.getConnectOptionsFromStrategy)(strategy);
32
45
  let connectResult;
33
- if (automaticallySetFirstEoa) {
34
- if (!options.wallet?.id) {
46
+ if (automaticallySetFirstEoa && (0, react_1.isWalletType)(strategy) && options.strategy === "wallet") {
47
+ const walletId = options.wallet?.id;
48
+ if (!walletId) {
35
49
  throw new Error("Wallet ID is required");
36
50
  }
37
51
  connectResult = await connectTW(async () => {
38
- const wallet = (0, wallets_1.createWallet)(options.wallet?.id);
52
+ const wallet = (0, wallets_1.createWallet)(walletId);
39
53
  await wallet.connect({
40
54
  client: thirdweb_1.client,
41
55
  });
@@ -43,20 +57,27 @@ function LoginStepCustom({ onSuccess, onError, chain, strategies, maxInitialWall
43
57
  });
44
58
  }
45
59
  else {
46
- // @ts-expect-error we have custom strategies too and we also get things like "apple" isn't assignable to "wallet"
47
60
  connectResult = await connect(options);
48
61
  }
49
62
  const account = connectResult?.getAccount();
50
63
  debug("@@connectResult", { connectResult, account, options });
51
- if (!account)
64
+ if (!account || !connectResult)
52
65
  throw new Error("Failed to connect");
66
+ const allConnectedWallets = connectedWallets.length > 0 && connectedWallets.some(wallet => wallet.id === connectResult.id)
67
+ ? connectedWallets
68
+ : [connectResult, ...connectedWallets.filter(wallet => wallet.id !== connectResult.id)];
69
+ await onAuthConnect(connectResult, allConnectedWallets);
53
70
  await onSuccess(account);
54
- setIsAuthenticated(true);
71
+ if (strategy === "email") {
72
+ resetEmailFlow();
73
+ }
55
74
  }
56
75
  catch (error) {
76
+ if (strategy === "email") {
77
+ setEmailError(error instanceof Error ? error.message : "Failed to sign in with email");
78
+ }
57
79
  await onError?.(error);
58
80
  await logout();
59
- setIsAuthenticated(false);
60
81
  }
61
82
  finally {
62
83
  setIsLoading(false);
@@ -64,8 +85,68 @@ function LoginStepCustom({ onSuccess, onError, chain, strategies, maxInitialWall
64
85
  setIsAuthenticating(false);
65
86
  }
66
87
  };
67
- return ((0, jsx_runtime_1.jsxs)(react_1.LoginStepContainer, { partnerId: partnerId, children: [authStrategies.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: `mb-6 w-full ${authStrategies.length <= 3 ? "space-y-3 px-3" : "grid grid-cols-4 gap-4"}`, children: authStrategies.map(strategy => {
68
- console.log("strategy", strategy);
69
- return ((0, jsx_runtime_1.jsx)(react_1.AuthButton, { strategy: strategy, onClick: () => handleConnect(strategy), isLoading: isLoading }, strategy));
70
- }) })), (0, jsx_runtime_1.jsx)("div", { className: "mb-4 w-full space-y-2", children: initialWallets.map(walletId => ((0, jsx_runtime_1.jsx)(react_1.WalletRow, { walletId: walletId, onClick: () => handleConnect(walletId), isLoading: isLoading }, walletId))) }), additionalWallets.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "w-full", children: [(0, jsx_runtime_1.jsx)(react_1.Button, { onClick: () => setShowAllWallets(!showAllWallets), className: "mb-2 w-full bg-transparent text-gray-600 hover:bg-gray-100", children: showAllWallets ? "Show less" : "More options" }), showAllWallets && ((0, jsx_runtime_1.jsx)("div", { className: "max-h-60 space-y-2 overflow-y-auto", children: additionalWallets.map(walletId => ((0, jsx_runtime_1.jsx)(react_1.WalletRow, { walletId: walletId, onClick: () => handleConnect(walletId), isLoading: isLoading }, walletId))) }))] }))] }));
88
+ const handleConnect = async (strategy) => {
89
+ if (strategy === "email") {
90
+ setShowEmailFlow(true);
91
+ setEmailCodeSent(false);
92
+ setVerificationCode("");
93
+ setEmailError(null);
94
+ return;
95
+ }
96
+ const options = (0, react_1.getConnectOptionsFromStrategy)(strategy);
97
+ await connectWithOptions(strategy, options);
98
+ };
99
+ const handleSendEmailCode = async () => {
100
+ const normalizedEmail = email.trim().toLowerCase();
101
+ if (!normalizedEmail) {
102
+ setEmailError("Please enter your email address");
103
+ return;
104
+ }
105
+ if (!EMAIL_REGEX.test(normalizedEmail)) {
106
+ setEmailError("Please enter a valid email address");
107
+ return;
108
+ }
109
+ try {
110
+ setIsLoading(true);
111
+ setEmailError(null);
112
+ await (0, wallets_1.preAuthenticate)({
113
+ client: thirdweb_1.client,
114
+ strategy: "email",
115
+ email: normalizedEmail,
116
+ ecosystem: {
117
+ id: constants_1.ecosystemWalletId,
118
+ partnerId,
119
+ },
120
+ });
121
+ setEmail(normalizedEmail);
122
+ setEmailCodeSent(true);
123
+ }
124
+ catch (error) {
125
+ setEmailError(error instanceof Error ? error.message : "Failed to send verification code");
126
+ await onError?.(error);
127
+ }
128
+ finally {
129
+ setIsLoading(false);
130
+ }
131
+ };
132
+ const handleEmailLogin = async () => {
133
+ const normalizedEmail = email.trim().toLowerCase();
134
+ const normalizedCode = verificationCode.trim();
135
+ if (!EMAIL_REGEX.test(normalizedEmail)) {
136
+ setEmailError("Please enter a valid email address");
137
+ return;
138
+ }
139
+ if (!normalizedCode) {
140
+ setEmailError("Please enter your verification code");
141
+ return;
142
+ }
143
+ await connectWithOptions("email", {
144
+ strategy: "email",
145
+ email: normalizedEmail,
146
+ verificationCode: normalizedCode,
147
+ });
148
+ };
149
+ return ((0, jsx_runtime_1.jsx)(react_1.LoginStepContainer, { partnerId: partnerId, children: showEmailFlow ? ((0, jsx_runtime_1.jsxs)("div", { className: "mb-6 w-full space-y-3 px-3", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-center text-sm font-medium text-gray-900 dark:text-gray-100", children: "Sign in with email" }), (0, jsx_runtime_1.jsx)(react_1.Input, { type: "email", placeholder: "you@example.com", value: email, onChange: event => setEmail(event.target.value), disabled: isLoading || emailCodeSent }), emailCodeSent && ((0, jsx_runtime_1.jsx)(react_1.Input, { type: "text", placeholder: "Enter verification code", value: verificationCode, onChange: event => setVerificationCode(event.target.value), disabled: isLoading })), emailError && (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-red-500", children: emailError }), (0, jsx_runtime_1.jsx)(react_1.Button, { onClick: emailCodeSent ? handleEmailLogin : handleSendEmailCode, disabled: isLoading, className: "w-full", children: isLoading ? "Loading..." : emailCodeSent ? "Verify code" : "Send code" }), emailCodeSent && ((0, jsx_runtime_1.jsx)(react_1.Button, { variant: "outline", onClick: handleSendEmailCode, disabled: isLoading, className: "w-full", children: "Resend code" })), (0, jsx_runtime_1.jsx)(react_1.Button, { variant: "outline", onClick: resetEmailFlow, disabled: isLoading, className: "w-full", children: "Back" })] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [authStrategies.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: `mb-6 grid w-full gap-4 px-3 ${authStrategies.length > 4 ? "grid-cols-4" : ""}`, style: authStrategies.length <= 4
150
+ ? { gridTemplateColumns: `repeat(${authGridColumns}, minmax(0, 1fr))` }
151
+ : undefined, children: authStrategies.map(strategy => ((0, jsx_runtime_1.jsx)(react_1.AuthButton, { strategy: strategy, onClick: () => handleConnect(strategy), isLoading: isLoading }, strategy))) })), (0, jsx_runtime_1.jsx)("div", { className: "mb-4 w-full space-y-2", children: initialWallets.map(walletId => ((0, jsx_runtime_1.jsx)(react_1.WalletRow, { walletId: walletId, onClick: () => handleConnect(walletId), isLoading: isLoading }, walletId))) }), additionalWallets.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "w-full", children: [(0, jsx_runtime_1.jsx)(react_1.Button, { onClick: () => setShowAllWallets(!showAllWallets), className: "mb-2 w-full bg-transparent text-gray-600 hover:bg-gray-100", children: showAllWallets ? "Show less" : "More options" }), showAllWallets && ((0, jsx_runtime_1.jsx)("div", { className: "max-h-60 space-y-2 overflow-y-auto", children: additionalWallets.map(walletId => ((0, jsx_runtime_1.jsx)(react_1.WalletRow, { walletId: walletId, onClick: () => handleConnect(walletId), isLoading: isLoading }, walletId))) }))] }))] })) }));
71
152
  }
@@ -3,15 +3,17 @@ import { SingleStepAuthArgsType, Wallet } from "thirdweb/wallets";
3
3
  type WalletType = Wallet["id"];
4
4
  type StrategyType = SingleStepAuthArgsType["strategy"];
5
5
  type CustomStrategyType = "basement" | "privy";
6
- type AllowedStrategies = StrategyType | WalletType | CustomStrategyType;
7
- export declare const allowedStrategies: readonly ["apple", "google", "x", "discord", "guest", "walletConnect", "io.metamask", "com.coinbase.wallet", "basement", "privy"];
6
+ type AllowedStrategies = StrategyType | WalletType | CustomStrategyType | "email";
7
+ type NonWalletStrategyType = Exclude<AllowedStrategies, WalletType>;
8
+ export declare const allowedStrategies: readonly ["apple", "google", "github", "x", "discord", "email", "guest", "walletConnect", "io.metamask", "com.coinbase.wallet", "basement", "privy"];
8
9
  export type AllowedStrategy = (typeof allowedStrategies)[number];
9
10
  export declare function isWalletType(strategy: AllowedStrategies): strategy is WalletType;
10
- export declare function isStrategyType(strategy: AllowedStrategies): strategy is StrategyType;
11
+ export declare function isStrategyType(strategy: AllowedStrategies): strategy is NonWalletStrategyType;
11
12
  export declare function getConnectOptionsFromStrategy(strategy: AllowedStrategy): {
12
13
  strategy: StrategyType;
13
14
  wallet?: Wallet;
14
15
  chain?: Chain;
15
16
  };
16
17
  export declare const strategyIcons: Record<string, string>;
18
+ export declare const strategyLabels: Record<string, string>;
17
19
  export {};
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.strategyIcons = exports.allowedStrategies = void 0;
3
+ exports.strategyLabels = exports.strategyIcons = exports.allowedStrategies = void 0;
4
4
  exports.isWalletType = isWalletType;
5
5
  exports.isStrategyType = isStrategyType;
6
6
  exports.getConnectOptionsFromStrategy = getConnectOptionsFromStrategy;
@@ -11,9 +11,10 @@ exports.allowedStrategies = [
11
11
  // Auth strategies
12
12
  "apple",
13
13
  "google",
14
+ "github",
14
15
  "x",
15
16
  "discord",
16
- // "github",
17
+ "email",
17
18
  "guest",
18
19
  // Wallet IDs
19
20
  "walletConnect",
@@ -35,6 +36,9 @@ function getConnectOptionsFromStrategy(strategy) {
35
36
  if (!exports.allowedStrategies.includes(strategy)) {
36
37
  throw new Error(`Invalid strategy: ${strategy}`);
37
38
  }
39
+ if (strategy === "email") {
40
+ throw new Error("Email strategy requires OTP flow and cannot be connected in a single step");
41
+ }
38
42
  if (isWalletType(strategy)) {
39
43
  return { strategy: "wallet", wallet: (0, wallets_1.createWallet)(strategy) };
40
44
  }
@@ -51,6 +55,15 @@ exports.strategyIcons = {
51
55
  guest: "https://cdn.b3.fun/incognito.svg",
52
56
  // Add more strategies as needed
53
57
  };
58
+ exports.strategyLabels = {
59
+ google: "Google",
60
+ x: "X",
61
+ discord: "Discord",
62
+ apple: "Apple",
63
+ guest: "Guest",
64
+ github: "GitHub",
65
+ email: "Email",
66
+ };
54
67
  // Test it
55
68
  // console.log(getConnectOptionsFromStrategy("io.metamask"));
56
69
  // console.log(getConnectOptionsFromStrategy("google"));
@@ -1,9 +1,9 @@
1
1
  import { Chain } from "thirdweb";
2
- import { SingleStepAuthArgsType } from "thirdweb/wallets";
2
+ import { MultiStepAuthArgsType, SingleStepAuthArgsType } from "thirdweb/wallets";
3
3
  /**
4
4
  * This hook is used to connect to a wallet using the thirdweb client.
5
5
  */
6
6
  export declare function useConnect(partnerId: string, chain?: Chain): {
7
- connect: (strategyOptions?: SingleStepAuthArgsType) => Promise<import("thirdweb/wallets").Wallet | null>;
7
+ connect: (strategyOptions?: MultiStepAuthArgsType | SingleStepAuthArgsType) => Promise<import("thirdweb/wallets").Wallet | null>;
8
8
  isLoading: boolean;
9
9
  };
@@ -1,6 +1,15 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Button } from "../../custom/Button.js";
3
- import { strategyIcons } from "../utils/signInUtils.js";
3
+ import { Github, Mail } from "lucide-react";
4
+ import { strategyIcons, strategyLabels } from "../utils/signInUtils.js";
5
+ const fallbackIcons = {
6
+ github: Github,
7
+ email: Mail,
8
+ };
4
9
  export function AuthButton({ strategy, onClick, isLoading, }) {
5
- return (_jsx(Button, { onClick: onClick, disabled: isLoading, className: "flex w-full items-center justify-center bg-gray-100 px-2 py-3 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700", children: _jsx("img", { src: strategyIcons[strategy], className: "h-9 w-9" }) }, strategy));
10
+ const strategyIcon = strategyIcons[strategy];
11
+ const strategyLabel = strategyLabels[strategy] || strategy;
12
+ const FallbackIcon = fallbackIcons[strategy];
13
+ const buttonLabel = `Sign in with ${strategyLabel}`;
14
+ return (_jsx(Button, { onClick: onClick, disabled: isLoading, "aria-label": buttonLabel, title: buttonLabel, className: "flex w-full items-center justify-center bg-gray-100 px-2 py-3 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700", children: strategyIcon ? (_jsx("img", { src: strategyIcon, alt: `${strategyLabel} icon`, className: "h-9 w-9" })) : FallbackIcon ? (_jsx(FallbackIcon, { className: "h-9 w-9 text-gray-900 dark:text-gray-100" })) : (_jsx("span", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: strategyLabel.charAt(0) })) }, strategy));
6
15
  }
@@ -1,38 +1,52 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { AuthButton, Button, getConnectOptionsFromStrategy, isWalletType, LoginStepContainer, useAuthentication, useAuthStore, useB3Config, useConnect, WalletRow, } from "../../../../../global-account/react/index.js";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { AuthButton, Button, getConnectOptionsFromStrategy, Input, isWalletType, LoginStepContainer, useAuthentication, useAuthStore, useB3Config, useConnect, WalletRow, } from "../../../../../global-account/react/index.js";
3
+ import { ecosystemWalletId } from "../../../../../shared/constants/index.js";
3
4
  import { debugB3React } from "../../../../../shared/utils/debug.js";
4
5
  import { client } from "../../../../../shared/utils/thirdweb.js";
5
6
  import { useState } from "react";
6
- import { useConnect as useConnectTW } from "thirdweb/react";
7
- import { createWallet } from "thirdweb/wallets";
7
+ import { useConnectedWallets, useConnect as useConnectTW } from "thirdweb/react";
8
+ import { createWallet, preAuthenticate, } from "thirdweb/wallets";
8
9
  const debug = debugB3React("LoginStepCustom");
10
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
9
11
  export function LoginStepCustom({ onSuccess, onError, chain, strategies, maxInitialWallets = 2, automaticallySetFirstEoa, }) {
10
12
  const { partnerId } = useB3Config();
11
13
  const [isLoading, setIsLoading] = useState(false);
12
14
  const [showAllWallets, setShowAllWallets] = useState(false);
15
+ const [showEmailFlow, setShowEmailFlow] = useState(false);
16
+ const [email, setEmail] = useState("");
17
+ const [verificationCode, setVerificationCode] = useState("");
18
+ const [emailCodeSent, setEmailCodeSent] = useState(false);
19
+ const [emailError, setEmailError] = useState(null);
13
20
  const { connect } = useConnect(partnerId, chain);
14
21
  const setIsAuthenticating = useAuthStore(state => state.setIsAuthenticating);
15
- const setIsAuthenticated = useAuthStore(state => state.setIsAuthenticated);
16
- const { logout } = useAuthentication(partnerId, { skipAutoConnect: true });
22
+ const { connect: onAuthConnect, logout } = useAuthentication(partnerId, { skipAutoConnect: true });
17
23
  const { connect: connectTW } = useConnectTW();
24
+ const connectedWallets = useConnectedWallets();
18
25
  // Split strategies into auth and wallet types
19
26
  const authStrategies = strategies.filter(s => !isWalletType(s));
20
27
  const walletStrategies = strategies.filter(isWalletType);
21
28
  const initialWallets = walletStrategies.slice(0, maxInitialWallets);
22
29
  const additionalWallets = walletStrategies.slice(maxInitialWallets);
23
- const handleConnect = async (strategy) => {
30
+ const authGridColumns = Math.max(1, Math.min(authStrategies.length, 4));
31
+ const resetEmailFlow = () => {
32
+ setShowEmailFlow(false);
33
+ setEmailCodeSent(false);
34
+ setVerificationCode("");
35
+ setEmailError(null);
36
+ };
37
+ const connectWithOptions = async (strategy, options) => {
24
38
  try {
25
39
  setIsLoading(true);
26
40
  debug("setIsAuthenticating:true:3");
27
41
  setIsAuthenticating(true);
28
- const options = getConnectOptionsFromStrategy(strategy);
29
42
  let connectResult;
30
- if (automaticallySetFirstEoa) {
31
- if (!options.wallet?.id) {
43
+ if (automaticallySetFirstEoa && isWalletType(strategy) && options.strategy === "wallet") {
44
+ const walletId = options.wallet?.id;
45
+ if (!walletId) {
32
46
  throw new Error("Wallet ID is required");
33
47
  }
34
48
  connectResult = await connectTW(async () => {
35
- const wallet = createWallet(options.wallet?.id);
49
+ const wallet = createWallet(walletId);
36
50
  await wallet.connect({
37
51
  client,
38
52
  });
@@ -40,20 +54,27 @@ export function LoginStepCustom({ onSuccess, onError, chain, strategies, maxInit
40
54
  });
41
55
  }
42
56
  else {
43
- // @ts-expect-error we have custom strategies too and we also get things like "apple" isn't assignable to "wallet"
44
57
  connectResult = await connect(options);
45
58
  }
46
59
  const account = connectResult?.getAccount();
47
60
  debug("@@connectResult", { connectResult, account, options });
48
- if (!account)
61
+ if (!account || !connectResult)
49
62
  throw new Error("Failed to connect");
63
+ const allConnectedWallets = connectedWallets.length > 0 && connectedWallets.some(wallet => wallet.id === connectResult.id)
64
+ ? connectedWallets
65
+ : [connectResult, ...connectedWallets.filter(wallet => wallet.id !== connectResult.id)];
66
+ await onAuthConnect(connectResult, allConnectedWallets);
50
67
  await onSuccess(account);
51
- setIsAuthenticated(true);
68
+ if (strategy === "email") {
69
+ resetEmailFlow();
70
+ }
52
71
  }
53
72
  catch (error) {
73
+ if (strategy === "email") {
74
+ setEmailError(error instanceof Error ? error.message : "Failed to sign in with email");
75
+ }
54
76
  await onError?.(error);
55
77
  await logout();
56
- setIsAuthenticated(false);
57
78
  }
58
79
  finally {
59
80
  setIsLoading(false);
@@ -61,8 +82,68 @@ export function LoginStepCustom({ onSuccess, onError, chain, strategies, maxInit
61
82
  setIsAuthenticating(false);
62
83
  }
63
84
  };
64
- return (_jsxs(LoginStepContainer, { partnerId: partnerId, children: [authStrategies.length > 0 && (_jsx("div", { className: `mb-6 w-full ${authStrategies.length <= 3 ? "space-y-3 px-3" : "grid grid-cols-4 gap-4"}`, children: authStrategies.map(strategy => {
65
- console.log("strategy", strategy);
66
- return (_jsx(AuthButton, { strategy: strategy, onClick: () => handleConnect(strategy), isLoading: isLoading }, strategy));
67
- }) })), _jsx("div", { className: "mb-4 w-full space-y-2", children: initialWallets.map(walletId => (_jsx(WalletRow, { walletId: walletId, onClick: () => handleConnect(walletId), isLoading: isLoading }, walletId))) }), additionalWallets.length > 0 && (_jsxs("div", { className: "w-full", children: [_jsx(Button, { onClick: () => setShowAllWallets(!showAllWallets), className: "mb-2 w-full bg-transparent text-gray-600 hover:bg-gray-100", children: showAllWallets ? "Show less" : "More options" }), showAllWallets && (_jsx("div", { className: "max-h-60 space-y-2 overflow-y-auto", children: additionalWallets.map(walletId => (_jsx(WalletRow, { walletId: walletId, onClick: () => handleConnect(walletId), isLoading: isLoading }, walletId))) }))] }))] }));
85
+ const handleConnect = async (strategy) => {
86
+ if (strategy === "email") {
87
+ setShowEmailFlow(true);
88
+ setEmailCodeSent(false);
89
+ setVerificationCode("");
90
+ setEmailError(null);
91
+ return;
92
+ }
93
+ const options = getConnectOptionsFromStrategy(strategy);
94
+ await connectWithOptions(strategy, options);
95
+ };
96
+ const handleSendEmailCode = async () => {
97
+ const normalizedEmail = email.trim().toLowerCase();
98
+ if (!normalizedEmail) {
99
+ setEmailError("Please enter your email address");
100
+ return;
101
+ }
102
+ if (!EMAIL_REGEX.test(normalizedEmail)) {
103
+ setEmailError("Please enter a valid email address");
104
+ return;
105
+ }
106
+ try {
107
+ setIsLoading(true);
108
+ setEmailError(null);
109
+ await preAuthenticate({
110
+ client,
111
+ strategy: "email",
112
+ email: normalizedEmail,
113
+ ecosystem: {
114
+ id: ecosystemWalletId,
115
+ partnerId,
116
+ },
117
+ });
118
+ setEmail(normalizedEmail);
119
+ setEmailCodeSent(true);
120
+ }
121
+ catch (error) {
122
+ setEmailError(error instanceof Error ? error.message : "Failed to send verification code");
123
+ await onError?.(error);
124
+ }
125
+ finally {
126
+ setIsLoading(false);
127
+ }
128
+ };
129
+ const handleEmailLogin = async () => {
130
+ const normalizedEmail = email.trim().toLowerCase();
131
+ const normalizedCode = verificationCode.trim();
132
+ if (!EMAIL_REGEX.test(normalizedEmail)) {
133
+ setEmailError("Please enter a valid email address");
134
+ return;
135
+ }
136
+ if (!normalizedCode) {
137
+ setEmailError("Please enter your verification code");
138
+ return;
139
+ }
140
+ await connectWithOptions("email", {
141
+ strategy: "email",
142
+ email: normalizedEmail,
143
+ verificationCode: normalizedCode,
144
+ });
145
+ };
146
+ return (_jsx(LoginStepContainer, { partnerId: partnerId, children: showEmailFlow ? (_jsxs("div", { className: "mb-6 w-full space-y-3 px-3", children: [_jsx("p", { className: "text-center text-sm font-medium text-gray-900 dark:text-gray-100", children: "Sign in with email" }), _jsx(Input, { type: "email", placeholder: "you@example.com", value: email, onChange: event => setEmail(event.target.value), disabled: isLoading || emailCodeSent }), emailCodeSent && (_jsx(Input, { type: "text", placeholder: "Enter verification code", value: verificationCode, onChange: event => setVerificationCode(event.target.value), disabled: isLoading })), emailError && _jsx("p", { className: "text-sm text-red-500", children: emailError }), _jsx(Button, { onClick: emailCodeSent ? handleEmailLogin : handleSendEmailCode, disabled: isLoading, className: "w-full", children: isLoading ? "Loading..." : emailCodeSent ? "Verify code" : "Send code" }), emailCodeSent && (_jsx(Button, { variant: "outline", onClick: handleSendEmailCode, disabled: isLoading, className: "w-full", children: "Resend code" })), _jsx(Button, { variant: "outline", onClick: resetEmailFlow, disabled: isLoading, className: "w-full", children: "Back" })] })) : (_jsxs(_Fragment, { children: [authStrategies.length > 0 && (_jsx("div", { className: `mb-6 grid w-full gap-4 px-3 ${authStrategies.length > 4 ? "grid-cols-4" : ""}`, style: authStrategies.length <= 4
147
+ ? { gridTemplateColumns: `repeat(${authGridColumns}, minmax(0, 1fr))` }
148
+ : undefined, children: authStrategies.map(strategy => (_jsx(AuthButton, { strategy: strategy, onClick: () => handleConnect(strategy), isLoading: isLoading }, strategy))) })), _jsx("div", { className: "mb-4 w-full space-y-2", children: initialWallets.map(walletId => (_jsx(WalletRow, { walletId: walletId, onClick: () => handleConnect(walletId), isLoading: isLoading }, walletId))) }), additionalWallets.length > 0 && (_jsxs("div", { className: "w-full", children: [_jsx(Button, { onClick: () => setShowAllWallets(!showAllWallets), className: "mb-2 w-full bg-transparent text-gray-600 hover:bg-gray-100", children: showAllWallets ? "Show less" : "More options" }), showAllWallets && (_jsx("div", { className: "max-h-60 space-y-2 overflow-y-auto", children: additionalWallets.map(walletId => (_jsx(WalletRow, { walletId: walletId, onClick: () => handleConnect(walletId), isLoading: isLoading }, walletId))) }))] }))] })) }));
68
149
  }
@@ -3,15 +3,17 @@ import { SingleStepAuthArgsType, Wallet } from "thirdweb/wallets";
3
3
  type WalletType = Wallet["id"];
4
4
  type StrategyType = SingleStepAuthArgsType["strategy"];
5
5
  type CustomStrategyType = "basement" | "privy";
6
- type AllowedStrategies = StrategyType | WalletType | CustomStrategyType;
7
- export declare const allowedStrategies: readonly ["apple", "google", "x", "discord", "guest", "walletConnect", "io.metamask", "com.coinbase.wallet", "basement", "privy"];
6
+ type AllowedStrategies = StrategyType | WalletType | CustomStrategyType | "email";
7
+ type NonWalletStrategyType = Exclude<AllowedStrategies, WalletType>;
8
+ export declare const allowedStrategies: readonly ["apple", "google", "github", "x", "discord", "email", "guest", "walletConnect", "io.metamask", "com.coinbase.wallet", "basement", "privy"];
8
9
  export type AllowedStrategy = (typeof allowedStrategies)[number];
9
10
  export declare function isWalletType(strategy: AllowedStrategies): strategy is WalletType;
10
- export declare function isStrategyType(strategy: AllowedStrategies): strategy is StrategyType;
11
+ export declare function isStrategyType(strategy: AllowedStrategies): strategy is NonWalletStrategyType;
11
12
  export declare function getConnectOptionsFromStrategy(strategy: AllowedStrategy): {
12
13
  strategy: StrategyType;
13
14
  wallet?: Wallet;
14
15
  chain?: Chain;
15
16
  };
16
17
  export declare const strategyIcons: Record<string, string>;
18
+ export declare const strategyLabels: Record<string, string>;
17
19
  export {};
@@ -5,9 +5,10 @@ export const allowedStrategies = [
5
5
  // Auth strategies
6
6
  "apple",
7
7
  "google",
8
+ "github",
8
9
  "x",
9
10
  "discord",
10
- // "github",
11
+ "email",
11
12
  "guest",
12
13
  // Wallet IDs
13
14
  "walletConnect",
@@ -29,6 +30,9 @@ export function getConnectOptionsFromStrategy(strategy) {
29
30
  if (!allowedStrategies.includes(strategy)) {
30
31
  throw new Error(`Invalid strategy: ${strategy}`);
31
32
  }
33
+ if (strategy === "email") {
34
+ throw new Error("Email strategy requires OTP flow and cannot be connected in a single step");
35
+ }
32
36
  if (isWalletType(strategy)) {
33
37
  return { strategy: "wallet", wallet: createWallet(strategy) };
34
38
  }
@@ -45,6 +49,15 @@ export const strategyIcons = {
45
49
  guest: "https://cdn.b3.fun/incognito.svg",
46
50
  // Add more strategies as needed
47
51
  };
52
+ export const strategyLabels = {
53
+ google: "Google",
54
+ x: "X",
55
+ discord: "Discord",
56
+ apple: "Apple",
57
+ guest: "Guest",
58
+ github: "GitHub",
59
+ email: "Email",
60
+ };
48
61
  // Test it
49
62
  // console.log(getConnectOptionsFromStrategy("io.metamask"));
50
63
  // console.log(getConnectOptionsFromStrategy("google"));
@@ -1,9 +1,9 @@
1
1
  import { Chain } from "thirdweb";
2
- import { SingleStepAuthArgsType } from "thirdweb/wallets";
2
+ import { MultiStepAuthArgsType, SingleStepAuthArgsType } from "thirdweb/wallets";
3
3
  /**
4
4
  * This hook is used to connect to a wallet using the thirdweb client.
5
5
  */
6
6
  export declare function useConnect(partnerId: string, chain?: Chain): {
7
- connect: (strategyOptions?: SingleStepAuthArgsType) => Promise<import("thirdweb/wallets").Wallet | null>;
7
+ connect: (strategyOptions?: MultiStepAuthArgsType | SingleStepAuthArgsType) => Promise<import("thirdweb/wallets").Wallet | null>;
8
8
  isLoading: boolean;
9
9
  };
@@ -3,15 +3,17 @@ import { SingleStepAuthArgsType, Wallet } from "thirdweb/wallets";
3
3
  type WalletType = Wallet["id"];
4
4
  type StrategyType = SingleStepAuthArgsType["strategy"];
5
5
  type CustomStrategyType = "basement" | "privy";
6
- type AllowedStrategies = StrategyType | WalletType | CustomStrategyType;
7
- export declare const allowedStrategies: readonly ["apple", "google", "x", "discord", "guest", "walletConnect", "io.metamask", "com.coinbase.wallet", "basement", "privy"];
6
+ type AllowedStrategies = StrategyType | WalletType | CustomStrategyType | "email";
7
+ type NonWalletStrategyType = Exclude<AllowedStrategies, WalletType>;
8
+ export declare const allowedStrategies: readonly ["apple", "google", "github", "x", "discord", "email", "guest", "walletConnect", "io.metamask", "com.coinbase.wallet", "basement", "privy"];
8
9
  export type AllowedStrategy = (typeof allowedStrategies)[number];
9
10
  export declare function isWalletType(strategy: AllowedStrategies): strategy is WalletType;
10
- export declare function isStrategyType(strategy: AllowedStrategies): strategy is StrategyType;
11
+ export declare function isStrategyType(strategy: AllowedStrategies): strategy is NonWalletStrategyType;
11
12
  export declare function getConnectOptionsFromStrategy(strategy: AllowedStrategy): {
12
13
  strategy: StrategyType;
13
14
  wallet?: Wallet;
14
15
  chain?: Chain;
15
16
  };
16
17
  export declare const strategyIcons: Record<string, string>;
18
+ export declare const strategyLabels: Record<string, string>;
17
19
  export {};
@@ -1,9 +1,9 @@
1
1
  import { Chain } from "thirdweb";
2
- import { SingleStepAuthArgsType } from "thirdweb/wallets";
2
+ import { MultiStepAuthArgsType, SingleStepAuthArgsType } from "thirdweb/wallets";
3
3
  /**
4
4
  * This hook is used to connect to a wallet using the thirdweb client.
5
5
  */
6
6
  export declare function useConnect(partnerId: string, chain?: Chain): {
7
- connect: (strategyOptions?: SingleStepAuthArgsType) => Promise<import("thirdweb/wallets").Wallet | null>;
7
+ connect: (strategyOptions?: MultiStepAuthArgsType | SingleStepAuthArgsType) => Promise<import("thirdweb/wallets").Wallet | null>;
8
8
  isLoading: boolean;
9
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b3dotfun/sdk",
3
- "version": "0.1.69-alpha.3",
3
+ "version": "0.1.69-alpha.4",
4
4
  "source": "src/index.ts",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "react-native": "./dist/cjs/index.native.js",
@@ -1,5 +1,11 @@
1
1
  import { Button } from "../../custom/Button";
2
- import { strategyIcons } from "../utils/signInUtils";
2
+ import { Github, Mail } from "lucide-react";
3
+ import { strategyIcons, strategyLabels } from "../utils/signInUtils";
4
+
5
+ const fallbackIcons = {
6
+ github: Github,
7
+ email: Mail,
8
+ } as const;
3
9
 
4
10
  export function AuthButton({
5
11
  strategy,
@@ -10,14 +16,27 @@ export function AuthButton({
10
16
  onClick: () => void;
11
17
  isLoading: boolean;
12
18
  }) {
19
+ const strategyIcon = strategyIcons[strategy];
20
+ const strategyLabel = strategyLabels[strategy] || strategy;
21
+ const FallbackIcon = fallbackIcons[strategy as keyof typeof fallbackIcons];
22
+ const buttonLabel = `Sign in with ${strategyLabel}`;
23
+
13
24
  return (
14
25
  <Button
15
26
  key={strategy}
16
27
  onClick={onClick}
17
28
  disabled={isLoading}
29
+ aria-label={buttonLabel}
30
+ title={buttonLabel}
18
31
  className="flex w-full items-center justify-center bg-gray-100 px-2 py-3 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700"
19
32
  >
20
- <img src={strategyIcons[strategy]} className="h-9 w-9" />
33
+ {strategyIcon ? (
34
+ <img src={strategyIcon} alt={`${strategyLabel} icon`} className="h-9 w-9" />
35
+ ) : FallbackIcon ? (
36
+ <FallbackIcon className="h-9 w-9 text-gray-900 dark:text-gray-100" />
37
+ ) : (
38
+ <span className="text-sm font-semibold text-gray-900 dark:text-gray-100">{strategyLabel.charAt(0)}</span>
39
+ )}
21
40
  </Button>
22
41
  );
23
42
  }
@@ -3,6 +3,7 @@ import {
3
3
  AuthButton,
4
4
  Button,
5
5
  getConnectOptionsFromStrategy,
6
+ Input,
6
7
  isWalletType,
7
8
  LoginStepContainer,
8
9
  useAuthentication,
@@ -11,12 +12,21 @@ import {
11
12
  useConnect,
12
13
  WalletRow,
13
14
  } from "@b3dotfun/sdk/global-account/react";
15
+ import { ecosystemWalletId } from "@b3dotfun/sdk/shared/constants";
14
16
  import { debugB3React } from "@b3dotfun/sdk/shared/utils/debug";
15
17
  import { client } from "@b3dotfun/sdk/shared/utils/thirdweb";
16
18
  import { useState } from "react";
17
19
  import { Chain } from "thirdweb";
18
- import { useConnect as useConnectTW } from "thirdweb/react";
19
- import { Account, createWallet, Wallet, WalletId } from "thirdweb/wallets";
20
+ import { useConnectedWallets, useConnect as useConnectTW } from "thirdweb/react";
21
+ import {
22
+ Account,
23
+ createWallet,
24
+ MultiStepAuthArgsType,
25
+ preAuthenticate,
26
+ SingleStepAuthArgsType,
27
+ Wallet,
28
+ WalletId,
29
+ } from "thirdweb/wallets";
20
30
 
21
31
  interface LoginStepCustomProps {
22
32
  automaticallySetFirstEoa: boolean;
@@ -28,6 +38,7 @@ interface LoginStepCustomProps {
28
38
  }
29
39
 
30
40
  const debug = debugB3React("LoginStepCustom");
41
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
31
42
 
32
43
  export function LoginStepCustom({
33
44
  onSuccess,
@@ -40,32 +51,49 @@ export function LoginStepCustom({
40
51
  const { partnerId } = useB3Config();
41
52
  const [isLoading, setIsLoading] = useState(false);
42
53
  const [showAllWallets, setShowAllWallets] = useState(false);
54
+ const [showEmailFlow, setShowEmailFlow] = useState(false);
55
+ const [email, setEmail] = useState("");
56
+ const [verificationCode, setVerificationCode] = useState("");
57
+ const [emailCodeSent, setEmailCodeSent] = useState(false);
58
+ const [emailError, setEmailError] = useState<string | null>(null);
43
59
  const { connect } = useConnect(partnerId, chain);
44
60
  const setIsAuthenticating = useAuthStore(state => state.setIsAuthenticating);
45
- const setIsAuthenticated = useAuthStore(state => state.setIsAuthenticated);
46
- const { logout } = useAuthentication(partnerId, { skipAutoConnect: true });
61
+ const { connect: onAuthConnect, logout } = useAuthentication(partnerId, { skipAutoConnect: true });
47
62
  const { connect: connectTW } = useConnectTW();
63
+ const connectedWallets = useConnectedWallets();
48
64
 
49
65
  // Split strategies into auth and wallet types
50
66
  const authStrategies = strategies.filter(s => !isWalletType(s));
51
67
  const walletStrategies = strategies.filter(isWalletType);
52
68
  const initialWallets = walletStrategies.slice(0, maxInitialWallets);
53
69
  const additionalWallets = walletStrategies.slice(maxInitialWallets);
70
+ const authGridColumns = Math.max(1, Math.min(authStrategies.length, 4));
54
71
 
55
- const handleConnect = async (strategy: AllowedStrategy) => {
72
+ const resetEmailFlow = () => {
73
+ setShowEmailFlow(false);
74
+ setEmailCodeSent(false);
75
+ setVerificationCode("");
76
+ setEmailError(null);
77
+ };
78
+
79
+ const connectWithOptions = async (
80
+ strategy: AllowedStrategy,
81
+ options: MultiStepAuthArgsType | SingleStepAuthArgsType,
82
+ ) => {
56
83
  try {
57
84
  setIsLoading(true);
58
85
  debug("setIsAuthenticating:true:3");
59
86
  setIsAuthenticating(true);
60
- const options = getConnectOptionsFromStrategy(strategy);
61
87
  let connectResult: Wallet | null;
62
88
 
63
- if (automaticallySetFirstEoa) {
64
- if (!options.wallet?.id) {
89
+ if (automaticallySetFirstEoa && isWalletType(strategy) && options.strategy === "wallet") {
90
+ const walletId = options.wallet?.id as WalletId | undefined;
91
+ if (!walletId) {
65
92
  throw new Error("Wallet ID is required");
66
93
  }
94
+
67
95
  connectResult = await connectTW(async () => {
68
- const wallet = createWallet(options.wallet?.id as WalletId);
96
+ const wallet = createWallet(walletId);
69
97
  await wallet.connect({
70
98
  client,
71
99
  });
@@ -73,19 +101,27 @@ export function LoginStepCustom({
73
101
  return wallet;
74
102
  });
75
103
  } else {
76
- // @ts-expect-error we have custom strategies too and we also get things like "apple" isn't assignable to "wallet"
77
104
  connectResult = await connect(options);
78
105
  }
79
106
 
80
107
  const account = connectResult?.getAccount();
81
108
  debug("@@connectResult", { connectResult, account, options });
82
- if (!account) throw new Error("Failed to connect");
109
+ if (!account || !connectResult) throw new Error("Failed to connect");
110
+ const allConnectedWallets =
111
+ connectedWallets.length > 0 && connectedWallets.some(wallet => wallet.id === connectResult.id)
112
+ ? connectedWallets
113
+ : [connectResult, ...connectedWallets.filter(wallet => wallet.id !== connectResult.id)];
114
+ await onAuthConnect(connectResult, allConnectedWallets);
83
115
  await onSuccess(account);
84
- setIsAuthenticated(true);
116
+ if (strategy === "email") {
117
+ resetEmailFlow();
118
+ }
85
119
  } catch (error) {
120
+ if (strategy === "email") {
121
+ setEmailError(error instanceof Error ? error.message : "Failed to sign in with email");
122
+ }
86
123
  await onError?.(error as Error);
87
124
  await logout();
88
- setIsAuthenticated(false);
89
125
  } finally {
90
126
  setIsLoading(false);
91
127
  debug("setIsAuthenticating:false:3");
@@ -93,60 +129,177 @@ export function LoginStepCustom({
93
129
  }
94
130
  };
95
131
 
132
+ const handleConnect = async (strategy: AllowedStrategy) => {
133
+ if (strategy === "email") {
134
+ setShowEmailFlow(true);
135
+ setEmailCodeSent(false);
136
+ setVerificationCode("");
137
+ setEmailError(null);
138
+ return;
139
+ }
140
+
141
+ const options = getConnectOptionsFromStrategy(strategy);
142
+ await connectWithOptions(strategy, options as SingleStepAuthArgsType);
143
+ };
144
+
145
+ const handleSendEmailCode = async () => {
146
+ const normalizedEmail = email.trim().toLowerCase();
147
+ if (!normalizedEmail) {
148
+ setEmailError("Please enter your email address");
149
+ return;
150
+ }
151
+
152
+ if (!EMAIL_REGEX.test(normalizedEmail)) {
153
+ setEmailError("Please enter a valid email address");
154
+ return;
155
+ }
156
+
157
+ try {
158
+ setIsLoading(true);
159
+ setEmailError(null);
160
+ await preAuthenticate({
161
+ client,
162
+ strategy: "email",
163
+ email: normalizedEmail,
164
+ ecosystem: {
165
+ id: ecosystemWalletId,
166
+ partnerId,
167
+ },
168
+ });
169
+ setEmail(normalizedEmail);
170
+ setEmailCodeSent(true);
171
+ } catch (error) {
172
+ setEmailError(error instanceof Error ? error.message : "Failed to send verification code");
173
+ await onError?.(error as Error);
174
+ } finally {
175
+ setIsLoading(false);
176
+ }
177
+ };
178
+
179
+ const handleEmailLogin = async () => {
180
+ const normalizedEmail = email.trim().toLowerCase();
181
+ const normalizedCode = verificationCode.trim();
182
+
183
+ if (!EMAIL_REGEX.test(normalizedEmail)) {
184
+ setEmailError("Please enter a valid email address");
185
+ return;
186
+ }
187
+
188
+ if (!normalizedCode) {
189
+ setEmailError("Please enter your verification code");
190
+ return;
191
+ }
192
+
193
+ await connectWithOptions("email", {
194
+ strategy: "email",
195
+ email: normalizedEmail,
196
+ verificationCode: normalizedCode,
197
+ });
198
+ };
199
+
96
200
  return (
97
201
  <LoginStepContainer partnerId={partnerId}>
98
- {/* Auth Strategies */}
99
- {authStrategies.length > 0 && (
100
- <div className={`mb-6 w-full ${authStrategies.length <= 3 ? "space-y-3 px-3" : "grid grid-cols-4 gap-4"}`}>
101
- {authStrategies.map(strategy => {
102
- console.log("strategy", strategy);
103
- return (
104
- <AuthButton
105
- key={strategy}
106
- strategy={strategy}
107
- onClick={() => handleConnect(strategy)}
108
- isLoading={isLoading}
109
- />
110
- );
111
- })}
112
- </div>
113
- )}
114
-
115
- {/* Initial Wallet List */}
116
- <div className="mb-4 w-full space-y-2">
117
- {initialWallets.map(walletId => (
118
- <WalletRow
119
- key={walletId}
120
- walletId={walletId as WalletId}
121
- onClick={() => handleConnect(walletId)}
122
- isLoading={isLoading}
202
+ {showEmailFlow ? (
203
+ <div className="mb-6 w-full space-y-3 px-3">
204
+ <p className="text-center text-sm font-medium text-gray-900 dark:text-gray-100">Sign in with email</p>
205
+ <Input
206
+ type="email"
207
+ placeholder="you@example.com"
208
+ value={email}
209
+ onChange={event => setEmail(event.target.value)}
210
+ disabled={isLoading || emailCodeSent}
123
211
  />
124
- ))}
125
- </div>
126
212
 
127
- {/* Additional Wallets Section */}
128
- {additionalWallets.length > 0 && (
129
- <div className="w-full">
213
+ {emailCodeSent && (
214
+ <Input
215
+ type="text"
216
+ placeholder="Enter verification code"
217
+ value={verificationCode}
218
+ onChange={event => setVerificationCode(event.target.value)}
219
+ disabled={isLoading}
220
+ />
221
+ )}
222
+
223
+ {emailError && <p className="text-sm text-red-500">{emailError}</p>}
224
+
130
225
  <Button
131
- onClick={() => setShowAllWallets(!showAllWallets)}
132
- className="mb-2 w-full bg-transparent text-gray-600 hover:bg-gray-100"
226
+ onClick={emailCodeSent ? handleEmailLogin : handleSendEmailCode}
227
+ disabled={isLoading}
228
+ className="w-full"
133
229
  >
134
- {showAllWallets ? "Show less" : "More options"}
230
+ {isLoading ? "Loading..." : emailCodeSent ? "Verify code" : "Send code"}
135
231
  </Button>
136
232
 
137
- {showAllWallets && (
138
- <div className="max-h-60 space-y-2 overflow-y-auto">
139
- {additionalWallets.map(walletId => (
140
- <WalletRow
141
- key={walletId}
142
- walletId={walletId as WalletId}
143
- onClick={() => handleConnect(walletId)}
233
+ {emailCodeSent && (
234
+ <Button variant="outline" onClick={handleSendEmailCode} disabled={isLoading} className="w-full">
235
+ Resend code
236
+ </Button>
237
+ )}
238
+
239
+ <Button variant="outline" onClick={resetEmailFlow} disabled={isLoading} className="w-full">
240
+ Back
241
+ </Button>
242
+ </div>
243
+ ) : (
244
+ <>
245
+ {/* Auth Strategies */}
246
+ {authStrategies.length > 0 && (
247
+ <div
248
+ className={`mb-6 grid w-full gap-4 px-3 ${authStrategies.length > 4 ? "grid-cols-4" : ""}`}
249
+ style={
250
+ authStrategies.length <= 4
251
+ ? { gridTemplateColumns: `repeat(${authGridColumns}, minmax(0, 1fr))` }
252
+ : undefined
253
+ }
254
+ >
255
+ {authStrategies.map(strategy => (
256
+ <AuthButton
257
+ key={strategy}
258
+ strategy={strategy}
259
+ onClick={() => handleConnect(strategy)}
144
260
  isLoading={isLoading}
145
261
  />
146
262
  ))}
147
263
  </div>
148
264
  )}
149
- </div>
265
+
266
+ {/* Initial Wallet List */}
267
+ <div className="mb-4 w-full space-y-2">
268
+ {initialWallets.map(walletId => (
269
+ <WalletRow
270
+ key={walletId}
271
+ walletId={walletId as WalletId}
272
+ onClick={() => handleConnect(walletId)}
273
+ isLoading={isLoading}
274
+ />
275
+ ))}
276
+ </div>
277
+
278
+ {/* Additional Wallets Section */}
279
+ {additionalWallets.length > 0 && (
280
+ <div className="w-full">
281
+ <Button
282
+ onClick={() => setShowAllWallets(!showAllWallets)}
283
+ className="mb-2 w-full bg-transparent text-gray-600 hover:bg-gray-100"
284
+ >
285
+ {showAllWallets ? "Show less" : "More options"}
286
+ </Button>
287
+
288
+ {showAllWallets && (
289
+ <div className="max-h-60 space-y-2 overflow-y-auto">
290
+ {additionalWallets.map(walletId => (
291
+ <WalletRow
292
+ key={walletId}
293
+ walletId={walletId as WalletId}
294
+ onClick={() => handleConnect(walletId)}
295
+ isLoading={isLoading}
296
+ />
297
+ ))}
298
+ </div>
299
+ )}
300
+ </div>
301
+ )}
302
+ </>
150
303
  )}
151
304
  </LoginStepContainer>
152
305
  );
@@ -5,7 +5,8 @@ type WalletType = Wallet["id"];
5
5
  type StrategyType = SingleStepAuthArgsType["strategy"];
6
6
  type CustomStrategyType = "basement" | "privy";
7
7
 
8
- type AllowedStrategies = StrategyType | WalletType | CustomStrategyType;
8
+ type AllowedStrategies = StrategyType | WalletType | CustomStrategyType | "email";
9
+ type NonWalletStrategyType = Exclude<AllowedStrategies, WalletType>;
9
10
  const customStrategies = ["basement", "privy"] as const;
10
11
  // type CustomStrategy = (typeof customStrategies)[number];
11
12
 
@@ -13,9 +14,10 @@ export const allowedStrategies = [
13
14
  // Auth strategies
14
15
  "apple",
15
16
  "google",
17
+ "github",
16
18
  "x",
17
19
  "discord",
18
- // "github",
20
+ "email",
19
21
  "guest",
20
22
 
21
23
  // Wallet IDs
@@ -36,7 +38,7 @@ export function isWalletType(strategy: AllowedStrategies): strategy is WalletTyp
36
38
  return strategy === "walletConnect" || walletIdPattern.test(strategy);
37
39
  }
38
40
 
39
- export function isStrategyType(strategy: AllowedStrategies): strategy is StrategyType {
41
+ export function isStrategyType(strategy: AllowedStrategies): strategy is NonWalletStrategyType {
40
42
  return !isWalletType(strategy);
41
43
  }
42
44
 
@@ -49,6 +51,10 @@ export function getConnectOptionsFromStrategy(strategy: AllowedStrategy): {
49
51
  throw new Error(`Invalid strategy: ${strategy}`);
50
52
  }
51
53
 
54
+ if (strategy === "email") {
55
+ throw new Error("Email strategy requires OTP flow and cannot be connected in a single step");
56
+ }
57
+
52
58
  if (isWalletType(strategy)) {
53
59
  return { strategy: "wallet" as const, wallet: createWallet(strategy) };
54
60
  } else {
@@ -65,6 +71,16 @@ export const strategyIcons: Record<string, string> = {
65
71
  guest: "https://cdn.b3.fun/incognito.svg",
66
72
  // Add more strategies as needed
67
73
  };
74
+
75
+ export const strategyLabels: Record<string, string> = {
76
+ google: "Google",
77
+ x: "X",
78
+ discord: "Discord",
79
+ apple: "Apple",
80
+ guest: "Guest",
81
+ github: "GitHub",
82
+ email: "Email",
83
+ };
68
84
  // Test it
69
85
  // console.log(getConnectOptionsFromStrategy("io.metamask"));
70
86
  // console.log(getConnectOptionsFromStrategy("google"));
@@ -4,7 +4,7 @@ import { client } from "@b3dotfun/sdk/shared/utils/thirdweb";
4
4
  import { useCallback, useState } from "react";
5
5
  import { Chain } from "thirdweb";
6
6
  import { useConnect as useConnectTW } from "thirdweb/react";
7
- import { ecosystemWallet, SingleStepAuthArgsType } from "thirdweb/wallets";
7
+ import { ecosystemWallet, MultiStepAuthArgsType, SingleStepAuthArgsType } from "thirdweb/wallets";
8
8
  const debug = debugB3React("useConnect");
9
9
 
10
10
  /**
@@ -23,7 +23,7 @@ export function useConnect(partnerId: string, chain?: Chain) {
23
23
  * It is used to connect to a wallet using the thirdweb client.
24
24
  */
25
25
  const connectTw = useCallback(
26
- async (strategyOptions?: SingleStepAuthArgsType) => {
26
+ async (strategyOptions?: MultiStepAuthArgsType | SingleStepAuthArgsType) => {
27
27
  setIsLoading(true);
28
28
  return await connect(async () => {
29
29
  if (!strategyOptions) throw new Error("Strategy options are required");