@b3dotfun/sdk 0.0.77-alpha.1 → 0.0.77-alpha.2

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 (60) hide show
  1. package/dist/cjs/global-account/react/components/B3DynamicModal.js +17 -3
  2. package/dist/cjs/global-account/react/components/B3Provider/B3Provider.d.ts +4 -2
  3. package/dist/cjs/global-account/react/components/B3Provider/B3Provider.js +6 -3
  4. package/dist/cjs/global-account/react/components/B3Provider/types.d.ts +1 -0
  5. package/dist/cjs/global-account/react/components/B3Provider/types.js +1 -0
  6. package/dist/cjs/global-account/react/components/SignInWithB3/SignInWithB3Flow.js +162 -46
  7. package/dist/cjs/global-account/react/components/TurnkeyAuthModal.d.ts +8 -0
  8. package/dist/cjs/global-account/react/components/TurnkeyAuthModal.js +84 -0
  9. package/dist/cjs/global-account/react/components/index.d.ts +1 -0
  10. package/dist/cjs/global-account/react/components/index.js +6 -3
  11. package/dist/cjs/global-account/react/hooks/index.d.ts +1 -0
  12. package/dist/cjs/global-account/react/hooks/index.js +3 -1
  13. package/dist/cjs/global-account/react/hooks/useAuthentication.d.ts +7 -0
  14. package/dist/cjs/global-account/react/hooks/useTurnkeyAuth.d.ts +20 -0
  15. package/dist/cjs/global-account/react/hooks/useTurnkeyAuth.js +112 -0
  16. package/dist/cjs/global-account/react/hooks/useUserQuery.d.ts +7 -0
  17. package/dist/cjs/global-account/react/stores/useAuthStore.d.ts +2 -0
  18. package/dist/cjs/global-account/react/stores/useAuthStore.js +2 -0
  19. package/dist/cjs/global-account/react/stores/useModalStore.d.ts +21 -1
  20. package/dist/esm/global-account/react/components/B3DynamicModal.js +17 -3
  21. package/dist/esm/global-account/react/components/B3Provider/B3Provider.d.ts +4 -2
  22. package/dist/esm/global-account/react/components/B3Provider/B3Provider.js +6 -3
  23. package/dist/esm/global-account/react/components/B3Provider/types.d.ts +1 -0
  24. package/dist/esm/global-account/react/components/B3Provider/types.js +1 -0
  25. package/dist/esm/global-account/react/components/SignInWithB3/SignInWithB3Flow.js +162 -46
  26. package/dist/esm/global-account/react/components/TurnkeyAuthModal.d.ts +8 -0
  27. package/dist/esm/global-account/react/components/TurnkeyAuthModal.js +81 -0
  28. package/dist/esm/global-account/react/components/index.d.ts +1 -0
  29. package/dist/esm/global-account/react/components/index.js +2 -0
  30. package/dist/esm/global-account/react/hooks/index.d.ts +1 -0
  31. package/dist/esm/global-account/react/hooks/index.js +1 -0
  32. package/dist/esm/global-account/react/hooks/useAuthentication.d.ts +7 -0
  33. package/dist/esm/global-account/react/hooks/useTurnkeyAuth.d.ts +20 -0
  34. package/dist/esm/global-account/react/hooks/useTurnkeyAuth.js +106 -0
  35. package/dist/esm/global-account/react/hooks/useUserQuery.d.ts +7 -0
  36. package/dist/esm/global-account/react/stores/useAuthStore.d.ts +2 -0
  37. package/dist/esm/global-account/react/stores/useAuthStore.js +2 -0
  38. package/dist/esm/global-account/react/stores/useModalStore.d.ts +21 -1
  39. package/dist/styles/index.css +1 -1
  40. package/dist/types/global-account/react/components/B3Provider/B3Provider.d.ts +4 -2
  41. package/dist/types/global-account/react/components/B3Provider/types.d.ts +1 -0
  42. package/dist/types/global-account/react/components/TurnkeyAuthModal.d.ts +8 -0
  43. package/dist/types/global-account/react/components/index.d.ts +1 -0
  44. package/dist/types/global-account/react/hooks/index.d.ts +1 -0
  45. package/dist/types/global-account/react/hooks/useAuthentication.d.ts +7 -0
  46. package/dist/types/global-account/react/hooks/useTurnkeyAuth.d.ts +20 -0
  47. package/dist/types/global-account/react/hooks/useUserQuery.d.ts +7 -0
  48. package/dist/types/global-account/react/stores/useAuthStore.d.ts +2 -0
  49. package/dist/types/global-account/react/stores/useModalStore.d.ts +21 -1
  50. package/package.json +2 -2
  51. package/src/global-account/react/components/B3DynamicModal.tsx +20 -1
  52. package/src/global-account/react/components/B3Provider/B3Provider.tsx +9 -0
  53. package/src/global-account/react/components/B3Provider/types.ts +2 -0
  54. package/src/global-account/react/components/SignInWithB3/SignInWithB3Flow.tsx +170 -48
  55. package/src/global-account/react/components/TurnkeyAuthModal.tsx +240 -0
  56. package/src/global-account/react/components/index.ts +3 -0
  57. package/src/global-account/react/hooks/index.ts +1 -0
  58. package/src/global-account/react/hooks/useTurnkeyAuth.ts +138 -0
  59. package/src/global-account/react/stores/useAuthStore.ts +4 -0
  60. package/src/global-account/react/stores/useModalStore.ts +22 -0
@@ -0,0 +1,20 @@
1
+ import { TurnkeyAuthInitResponse } from "@b3dotfun/b3-api";
2
+ interface UseTurnkeyAuthReturn {
3
+ initiateLogin: (_email: string) => Promise<TurnkeyAuthInitResponse>;
4
+ verifyOtp: (_otpId: string, _otpCode: string) => Promise<{
5
+ user: any;
6
+ }>;
7
+ isLoading: boolean;
8
+ error: string | null;
9
+ clearError: () => void;
10
+ }
11
+ /**
12
+ * Hook for Turnkey email-based OTP authentication
13
+ *
14
+ * Usage:
15
+ * 1. Call initiateLogin(email) → User receives OTP email
16
+ * 2. Call verifyOtp(...) → Verifies OTP and authenticates with b3-api
17
+ * 3. User is authenticated, JWT stored in cookies automatically
18
+ */
19
+ export declare function useTurnkeyAuth(): UseTurnkeyAuthReturn;
20
+ export {};
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.useTurnkeyAuth = useTurnkeyAuth;
7
+ const app_1 = __importDefault(require("../../app"));
8
+ const stores_1 = require("../stores");
9
+ const react_1 = require("react");
10
+ const useB3_1 = require("../components/B3Provider/useB3");
11
+ const debug_1 = require("../../../shared/utils/debug");
12
+ const debug = (0, debug_1.debugB3React)("useTurnkeyAuth");
13
+ /**
14
+ * Hook for Turnkey email-based OTP authentication
15
+ *
16
+ * Usage:
17
+ * 1. Call initiateLogin(email) → User receives OTP email
18
+ * 2. Call verifyOtp(...) → Verifies OTP and authenticates with b3-api
19
+ * 3. User is authenticated, JWT stored in cookies automatically
20
+ */
21
+ function useTurnkeyAuth() {
22
+ const [isLoading, setIsLoading] = (0, react_1.useState)(false);
23
+ const [error, setError] = (0, react_1.useState)(null);
24
+ const setIsAuthenticating = (0, stores_1.useAuthStore)(state => state.setIsAuthenticating);
25
+ const setIsAuthenticated = (0, stores_1.useAuthStore)(state => state.setIsAuthenticated);
26
+ const { user } = (0, useB3_1.useB3)();
27
+ /**
28
+ * Step 1: Initiate login with email
29
+ * - Calls backend to create sub-org (if needed) and send OTP
30
+ * - Returns otpId to use in verification step
31
+ */
32
+ const initiateLogin = (0, react_1.useCallback)(async (email) => {
33
+ setIsLoading(true);
34
+ setError(null);
35
+ setIsAuthenticating(true);
36
+ try {
37
+ if (!user?.userId) {
38
+ throw new Error("User ID is required to initiate Turnkey login.");
39
+ }
40
+ debug(`Initiating login for: ${email}`);
41
+ // Call FeathersJS service to initialize OTP
42
+ const data = await app_1.default.service("turnkey-auth").init({ email, userId: user.userId });
43
+ debug(`OTP initialized successfully. OtpId: ${data.otpId}`);
44
+ return data;
45
+ }
46
+ catch (err) {
47
+ debug("Error initiating login:", err);
48
+ const errorMessage = err.message || "Failed to send OTP email. Please try again.";
49
+ setError(errorMessage);
50
+ throw err;
51
+ }
52
+ finally {
53
+ setIsLoading(false);
54
+ setIsAuthenticating(false);
55
+ }
56
+ }, [user, setIsAuthenticating]);
57
+ /**
58
+ * Step 2: Verify OTP and authenticate
59
+ * - Verifies OTP with backend
60
+ * - Gets Turnkey session JWT
61
+ * - Authenticates with b3-api using "turnkey-jwt" strategy
62
+ * - JWT automatically stored in cookies by SDK
63
+ */
64
+ const verifyOtp = (0, react_1.useCallback)(async (otpId, otpCode) => {
65
+ setIsLoading(true);
66
+ setError(null);
67
+ setIsAuthenticating(true);
68
+ try {
69
+ debug(`Verifying OTP...`, { userId: user?.userId });
70
+ // Step 1: Verify OTP and get Turnkey session JWT
71
+ const { turnkeySessionJwt } = await app_1.default.service("turnkey-auth").verify({
72
+ otpId,
73
+ otpCode,
74
+ });
75
+ debug(`OTP verified! Authenticating with b3-api...`);
76
+ // Step 2: Authenticate with b3-api using Turnkey JWT
77
+ // The SDK will automatically store the b3-api JWT in cookies
78
+ const authResult = await app_1.default.authenticate({
79
+ strategy: "turnkey-jwt",
80
+ accessToken: turnkeySessionJwt,
81
+ });
82
+ debug(`Successfully authenticated with b3-api!`, authResult);
83
+ // Update auth store to reflect authenticated state
84
+ setIsAuthenticated(true);
85
+ // Return user data
86
+ return {
87
+ user: authResult.user,
88
+ };
89
+ }
90
+ catch (err) {
91
+ debug("Error verifying OTP:", err);
92
+ const errorMessage = err.message || "Failed to verify OTP. Please try again.";
93
+ setError(errorMessage);
94
+ setIsAuthenticated(false);
95
+ throw err;
96
+ }
97
+ finally {
98
+ setIsLoading(false);
99
+ setIsAuthenticating(false);
100
+ }
101
+ }, [user, setIsAuthenticating, setIsAuthenticated]);
102
+ const clearError = (0, react_1.useCallback)(() => {
103
+ setError(null);
104
+ }, []);
105
+ return {
106
+ initiateLogin,
107
+ verifyOtp,
108
+ isLoading,
109
+ error,
110
+ clearError,
111
+ };
112
+ }
@@ -41,6 +41,11 @@ export declare function useUserQuery(): {
41
41
  fid?: string | undefined;
42
42
  };
43
43
  }[] | undefined;
44
+ turnkeySubOrgs?: {
45
+ hasDelegatedUser?: boolean | undefined;
46
+ subOrgId: string;
47
+ accounts: Record<string, any>[];
48
+ }[] | undefined;
44
49
  _id: string | {};
45
50
  userId: string;
46
51
  smartAccountAddress: string;
@@ -49,6 +54,8 @@ export declare function useUserQuery(): {
49
54
  partnerIds: {
50
55
  privyId?: string | undefined;
51
56
  thirdwebId?: string | undefined;
57
+ turnkeyId?: string | undefined;
58
+ turnkeyOtpId?: string | undefined;
52
59
  };
53
60
  } | undefined;
54
61
  setUser: (newUser?: Users) => void;
@@ -27,6 +27,8 @@ interface AuthState {
27
27
  setIsAuthenticating: (isAuthenticating: boolean) => void;
28
28
  hasStartedConnecting: boolean;
29
29
  setHasStartedConnecting: (hasStartedConnecting: boolean) => void;
30
+ justCompletedLogin: boolean;
31
+ setJustCompletedLogin: (justCompletedLogin: boolean) => void;
30
32
  }
31
33
  export declare const useAuthStore: import("zustand").UseBoundStore<import("zustand").StoreApi<AuthState>>;
32
34
  export {};
@@ -42,4 +42,6 @@ exports.useAuthStore = (0, zustand_1.create)(set => ({
42
42
  setIsAuthenticating: isAuthenticating => set({ isAuthenticating: isAuthenticating }),
43
43
  hasStartedConnecting: false,
44
44
  setHasStartedConnecting: hasStartedConnecting => set({ hasStartedConnecting }),
45
+ justCompletedLogin: false,
46
+ setJustCompletedLogin: justCompletedLogin => set({ justCompletedLogin }),
45
47
  }));
@@ -10,6 +10,8 @@ import { Account } from "thirdweb/wallets";
10
10
  interface BaseModalProps {
11
11
  /** Whether to show a back button in the modal header */
12
12
  showBackButton?: boolean;
13
+ /** Whether the modal can be closed by clicking outside or pressing escape. Defaults to true */
14
+ closable?: boolean;
13
15
  }
14
16
  /**
15
17
  * Props for the Sign In With B3 modal
@@ -39,6 +41,24 @@ export interface SignInWithB3ModalProps extends BaseModalProps {
39
41
  /** Whether to show the signers enabled modal */
40
42
  signersEnabled?: boolean;
41
43
  }
44
+ /**
45
+ * Props for the Turnkey Authentication modal
46
+ * Handles Turnkey email/OTP authentication flow
47
+ */
48
+ export interface TurnkeyAuthModalProps extends BaseModalProps {
49
+ /** Modal type identifier */
50
+ type: "turnkeyAuth";
51
+ /** Callback function called when authentication is successful */
52
+ onSuccess: (_user: any) => void;
53
+ /** Callback function called when modal is closed */
54
+ onClose: () => void;
55
+ /** Initial email to pre-fill */
56
+ initialEmail?: string;
57
+ /** Whether to skip directly to OTP step */
58
+ skipToOtp?: boolean;
59
+ /** Whether the modal can be closed - defaults to false for Turnkey */
60
+ closable?: boolean;
61
+ }
42
62
  /**
43
63
  * Props for the Request Permissions modal
44
64
  * Used to request permission for session keys to interact with contracts
@@ -451,7 +471,7 @@ export interface AnySpendCollectorClubPurchaseProps extends BaseModalProps {
451
471
  /**
452
472
  * Union type of all possible modal content types
453
473
  */
454
- export type ModalContentType = SignInWithB3ModalProps | RequestPermissionsModalProps | ManageAccountModalProps | AnySpendModalProps | AnyspendOrderDetailsProps | AnySpendNftProps | AnySpendJoinTournamentProps | AnySpendFundTournamentProps | AnySpendOrderHistoryProps | AnySpendStakeB3Props | AnySpendStakeB3ExactInProps | AnySpendStakeUpsideProps | AnySpendStakeUpsideExactInProps | AnySpendDepositUpsideProps | AnySpendBuySpinProps | AnySpendSignatureMintProps | AnySpendBondKitProps | LinkAccountModalProps | LinkNewAccountModalProps | AnySpendDepositHypeProps | AvatarEditorModalProps | DepositModalProps | SendModalProps | NotificationsModalProps | AnySpendCollectorClubPurchaseProps;
474
+ export type ModalContentType = SignInWithB3ModalProps | TurnkeyAuthModalProps | RequestPermissionsModalProps | ManageAccountModalProps | AnySpendModalProps | AnyspendOrderDetailsProps | AnySpendNftProps | AnySpendJoinTournamentProps | AnySpendFundTournamentProps | AnySpendOrderHistoryProps | AnySpendStakeB3Props | AnySpendStakeB3ExactInProps | AnySpendStakeUpsideProps | AnySpendStakeUpsideExactInProps | AnySpendDepositUpsideProps | AnySpendBuySpinProps | AnySpendSignatureMintProps | AnySpendBondKitProps | LinkAccountModalProps | LinkNewAccountModalProps | AnySpendDepositHypeProps | AvatarEditorModalProps | DepositModalProps | SendModalProps | NotificationsModalProps | AnySpendCollectorClubPurchaseProps;
455
475
  /**
456
476
  * State interface for the modal store
457
477
  */
@@ -20,6 +20,7 @@ import NotificationsContent from "./ManageAccount/NotificationsContent.js";
20
20
  import { RequestPermissions } from "./RequestPermissions/RequestPermissions.js";
21
21
  import { Send } from "./Send/Send.js";
22
22
  import { SignInWithB3Flow } from "./SignInWithB3/SignInWithB3Flow.js";
23
+ import { TurnkeyAuthModal } from "./TurnkeyAuthModal.js";
23
24
  import { ToastContainer, useToastContext } from "./Toast/index.js";
24
25
  import { Dialog, DialogContent, DialogDescription, DialogTitle } from "./ui/dialog.js";
25
26
  import { Drawer, DrawerContent, DrawerDescription, DrawerTitle } from "./ui/drawer.js";
@@ -57,6 +58,7 @@ export function B3DynamicModal() {
57
58
  "anySpendBuySpin",
58
59
  "anySpendOrderHistory",
59
60
  "signInWithB3",
61
+ "turnkeyAuth",
60
62
  "anySpendSignatureMint",
61
63
  "anySpendBondKit",
62
64
  "linkAccount",
@@ -80,10 +82,12 @@ export function B3DynamicModal() {
80
82
  ];
81
83
  // Check if current content type is in freestyle types
82
84
  const isFreestyleType = freestyleTypes.includes(contentType?.type);
85
+ // Determine if modal should be closable - defaults to true unless explicitly set to false
86
+ const isClosable = contentType?.closable !== false;
83
87
  const hideCloseButton = true;
84
88
  // Build content class using cn utility
85
89
  // eslint-disable-next-line tailwindcss/no-custom-classname
86
- const contentClass = cn("b3-modal", theme === "dark" && "dark", fullWidthTypes.includes(contentType?.type) && "w-full", isFreestyleType && "b3-modal-freestyle", contentType?.type === "signInWithB3" && "p-0", contentType?.type === "anySpend" && "md:p-0", contentType?.type === "send" && "p-0", contentType?.type === "manageAccount" && " md:p-0 md:pt-2", contentType?.type === "linkAccount" && "md:p-0");
90
+ const contentClass = cn("b3-modal", theme === "dark" && "dark", fullWidthTypes.includes(contentType?.type) && "w-full", isFreestyleType && "b3-modal-freestyle", contentType?.type === "signInWithB3" && "p-0", contentType?.type === "turnkeyAuth" && "p-0", contentType?.type === "anySpend" && "md:p-0", contentType?.type === "send" && "p-0", contentType?.type === "manageAccount" && " md:p-0 md:pt-2", contentType?.type === "linkAccount" && "md:p-0");
87
91
  debug("contentType", contentType);
88
92
  const renderContent = () => {
89
93
  if (!contentType)
@@ -91,6 +95,8 @@ export function B3DynamicModal() {
91
95
  switch (contentType.type) {
92
96
  case "signInWithB3":
93
97
  return _jsx(SignInWithB3Flow, { ...contentType });
98
+ case "turnkeyAuth":
99
+ return _jsx(TurnkeyAuthModal, { ...contentType });
94
100
  case "requestPermissions":
95
101
  return _jsx(RequestPermissions, { ...contentType });
96
102
  case "manageAccount":
@@ -148,12 +154,20 @@ export function B3DynamicModal() {
148
154
  const ModalContent = isMobile ? DrawerContent : DialogContent;
149
155
  const ModalTitle = isMobile ? DrawerTitle : DialogTitle;
150
156
  const ModalDescription = isMobile ? DrawerDescription : DialogDescription;
151
- return (_jsxs(ModalComponent, { open: isOpen, onOpenChange: setB3ModalOpen, children: [_jsxs(ModalContent, { className: cn(contentClass, "rounded-2xl bg-white shadow-xl dark:bg-gray-900", "border border-gray-200 dark:border-gray-800", (contentType?.type === "manageAccount" ||
157
+ // Create a wrapper for onOpenChange that respects closable property
158
+ const handleOpenChange = (open) => {
159
+ // Only allow closing if the modal is closable
160
+ if (!open && !isClosable) {
161
+ return;
162
+ }
163
+ setB3ModalOpen(open);
164
+ };
165
+ return (_jsxs(ModalComponent, { open: isOpen, onOpenChange: handleOpenChange, children: [_jsxs(ModalContent, { className: cn(contentClass, "rounded-2xl bg-white shadow-xl dark:bg-gray-900", "border border-gray-200 dark:border-gray-800", (contentType?.type === "manageAccount" ||
152
166
  contentType?.type === "deposit" ||
153
167
  contentType?.type === "send" ||
154
168
  contentType?.type === "avatarEditor" ||
155
169
  contentType?.type === "notifications") &&
156
- "p-0", "mx-auto w-full max-w-md sm:max-w-lg"), hideCloseButton: hideCloseButton, children: [_jsx(ModalTitle, { className: "sr-only hidden", children: contentType?.type || "Modal" }), _jsx(ModalDescription, { className: "sr-only hidden", children: contentType?.type || "Modal Body" }), _jsxs("div", { className: cn("b3-modal-content no-scrollbar dark:bg-b3-background flex max-h-[90dvh] flex-col overflow-auto sm:max-h-[80dvh]"), children: [!hideCloseButton && (_jsxs("button", { onClick: navigateBack, className: "flex items-center gap-2 px-6 py-4 text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-white", children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [_jsx("path", { d: "M15.8337 10H4.16699", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M10.0003 15.8334L4.16699 10L10.0003 4.16669", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })] }), _jsx("span", { className: "font-inter text-sm font-semibold", children: "Back" })] })), _jsx("div", { className: "flex-1", children: renderContent() }), _jsx(AnimatePresence, { children: toasts.length > 0 && (_jsx(motion.div, { initial: { height: 0 }, animate: { height: "auto" }, exit: { height: 0 }, transition: { duration: 0.3, ease: "easeInOut" }, className: "toast-section relative z-10 overflow-hidden bg-white dark:border-gray-800 dark:bg-gray-900", children: _jsx(motion.div, { initial: { opacity: 0, y: -10 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -10 }, transition: { duration: 0.2, delay: 0.1 }, className: "p-4 pt-0", children: _jsx(ToastContainer, { toasts: toasts, onDismiss: removeToast, theme: theme }) }) })) })] })] }), isOpen && (_jsx("style", { children: `
170
+ "p-0", "mx-auto w-full max-w-md sm:max-w-lg"), hideCloseButton: hideCloseButton, onEscapeKeyDown: !isClosable ? e => e.preventDefault() : undefined, onPointerDownOutside: !isClosable ? e => e.preventDefault() : undefined, onInteractOutside: !isClosable ? e => e.preventDefault() : undefined, children: [_jsx(ModalTitle, { className: "sr-only hidden", children: contentType?.type || "Modal" }), _jsx(ModalDescription, { className: "sr-only hidden", children: contentType?.type || "Modal Body" }), _jsxs("div", { className: cn("b3-modal-content no-scrollbar dark:bg-b3-background flex max-h-[90dvh] flex-col overflow-auto sm:max-h-[80dvh]"), children: [!hideCloseButton && (_jsxs("button", { onClick: navigateBack, className: "flex items-center gap-2 px-6 py-4 text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-white", children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [_jsx("path", { d: "M15.8337 10H4.16699", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M10.0003 15.8334L4.16699 10L10.0003 4.16669", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })] }), _jsx("span", { className: "font-inter text-sm font-semibold", children: "Back" })] })), _jsx("div", { className: "flex-1", children: renderContent() }), _jsx(AnimatePresence, { children: toasts.length > 0 && (_jsx(motion.div, { initial: { height: 0 }, animate: { height: "auto" }, exit: { height: 0 }, transition: { duration: 0.3, ease: "easeInOut" }, className: "toast-section relative z-10 overflow-hidden bg-white dark:border-gray-800 dark:bg-gray-900", children: _jsx(motion.div, { initial: { opacity: 0, y: -10 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -10 }, transition: { duration: 0.2, delay: 0.1 }, className: "p-4 pt-0", children: _jsx(ToastContainer, { toasts: toasts, onDismiss: removeToast, theme: theme }) }) })) })] })] }), isOpen && (_jsx("style", { children: `
157
171
  .modal-inner-content {
158
172
  transition: margin-bottom 0.3s ease-in-out;
159
173
  margin-bottom: ${toasts.length > 0 ? "0px" : "23px"} !important;
@@ -9,7 +9,7 @@ import { B3ContextType } from "./types";
9
9
  /**
10
10
  * Main B3Provider component
11
11
  */
12
- export declare function B3Provider({ theme, children, accountOverride, environment, automaticallySetFirstEoa, simDuneApiKey, toaster: _toaster, clientType, rpcUrls, partnerId, onConnect, connectors, overrideDefaultConnectors, createClientReferenceId, }: {
12
+ export declare function B3Provider({ theme, children, accountOverride, environment, automaticallySetFirstEoa, simDuneApiKey, toaster: _toaster, clientType, rpcUrls, partnerId, onConnect, connectors, overrideDefaultConnectors, createClientReferenceId, enableTurnkey, }: {
13
13
  theme: "light" | "dark";
14
14
  children: React.ReactNode;
15
15
  accountOverride?: Account;
@@ -27,11 +27,12 @@ export declare function B3Provider({ theme, children, accountOverride, environme
27
27
  connectors?: CreateConnectorFn[];
28
28
  overrideDefaultConnectors?: boolean;
29
29
  createClientReferenceId?: (params: CreateOrderParams | CreateOnrampOrderParams) => Promise<string>;
30
+ enableTurnkey?: boolean;
30
31
  }): import("react/jsx-runtime").JSX.Element;
31
32
  /**
32
33
  * Inner provider component that provides the actual B3Context
33
34
  */
34
- export declare function InnerProvider({ children, accountOverride, environment, defaultPermissions, automaticallySetFirstEoa, theme, clientType, partnerId, createClientReferenceId, }: {
35
+ export declare function InnerProvider({ children, accountOverride, environment, defaultPermissions, automaticallySetFirstEoa, theme, clientType, partnerId, createClientReferenceId, enableTurnkey, }: {
35
36
  children: React.ReactNode;
36
37
  accountOverride?: Account;
37
38
  environment: B3ContextType["environment"];
@@ -41,4 +42,5 @@ export declare function InnerProvider({ children, accountOverride, environment,
41
42
  clientType?: ClientType;
42
43
  partnerId: string;
43
44
  createClientReferenceId?: (params: CreateOrderParams | CreateOnrampOrderParams) => Promise<string>;
45
+ enableTurnkey?: boolean;
44
46
  }): import("react/jsx-runtime").JSX.Element;
@@ -30,7 +30,7 @@ const queryClient = new QueryClient();
30
30
  */
31
31
  export function B3Provider({ theme = "light", children, accountOverride, environment, automaticallySetFirstEoa, simDuneApiKey,
32
32
  // deprecated since v0.0.87
33
- toaster: _toaster, clientType = "rest", rpcUrls, partnerId, onConnect, connectors, overrideDefaultConnectors = false, createClientReferenceId, }) {
33
+ toaster: _toaster, clientType = "rest", rpcUrls, partnerId, onConnect, connectors, overrideDefaultConnectors = false, createClientReferenceId, enableTurnkey = false, }) {
34
34
  // Initialize Google Analytics on mount
35
35
  useEffect(() => {
36
36
  loadGA4Script();
@@ -40,22 +40,24 @@ toaster: _toaster, clientType = "rest", rpcUrls, partnerId, onConnect, connector
40
40
  setClientType(clientType);
41
41
  }, [clientType]);
42
42
  const wagmiConfig = createWagmiConfig({ partnerId, rpcUrls, connectors, overrideDefaultConnectors });
43
- return (_jsx(ThirdwebProvider, { children: _jsx(WagmiProvider, { config: wagmiConfig, reconnectOnMount: false, children: _jsx(QueryClientProvider, { client: queryClient, children: _jsx(TooltipProvider, { children: _jsx(ToastProvider, { children: _jsx(LocalSDKProvider, { onConnectCallback: onConnect, children: _jsxs(InnerProvider, { accountOverride: accountOverride, environment: environment, theme: theme, automaticallySetFirstEoa: !!automaticallySetFirstEoa, clientType: clientType, partnerId: partnerId, createClientReferenceId: createClientReferenceId, children: [_jsx(ToastContextConnector, {}), _jsxs(RelayKitProviderWrapper, { simDuneApiKey: simDuneApiKey, children: [children, _jsx(StyleRoot, { id: "b3-root" })] })] }) }) }) }) }) }) }));
43
+ return (_jsx(ThirdwebProvider, { children: _jsx(WagmiProvider, { config: wagmiConfig, reconnectOnMount: false, children: _jsx(QueryClientProvider, { client: queryClient, children: _jsx(TooltipProvider, { children: _jsx(ToastProvider, { children: _jsx(LocalSDKProvider, { onConnectCallback: onConnect, children: _jsxs(InnerProvider, { accountOverride: accountOverride, environment: environment, theme: theme, automaticallySetFirstEoa: !!automaticallySetFirstEoa, clientType: clientType, partnerId: partnerId, createClientReferenceId: createClientReferenceId, enableTurnkey: enableTurnkey, children: [_jsx(ToastContextConnector, {}), _jsxs(RelayKitProviderWrapper, { simDuneApiKey: simDuneApiKey, children: [children, _jsx(StyleRoot, { id: "b3-root" })] })] }) }) }) }) }) }) }));
44
44
  }
45
45
  /**
46
46
  * Inner provider component that provides the actual B3Context
47
47
  */
48
- export function InnerProvider({ children, accountOverride, environment, defaultPermissions = DEFAULT_PERMISSIONS, automaticallySetFirstEoa, theme = "light", clientType = "socket", partnerId, createClientReferenceId, }) {
48
+ export function InnerProvider({ children, accountOverride, environment, defaultPermissions = DEFAULT_PERMISSIONS, automaticallySetFirstEoa, theme = "light", clientType = "socket", partnerId, createClientReferenceId, enableTurnkey, }) {
49
49
  const activeAccount = useActiveAccount();
50
50
  const [manuallySelectedWallet, setManuallySelectedWallet] = useState(undefined);
51
51
  const wallets = useConnectedWallets();
52
52
  const isAuthenticated = useAuthStore(state => state.isAuthenticated);
53
53
  const isConnected = useAuthStore(state => state.isConnected);
54
+ const justCompletedLogin = useAuthStore(state => state.justCompletedLogin);
54
55
  const setActiveWallet = useSetActiveWallet();
55
56
  const { user, setUser, refetchUser } = useAuthentication(partnerId);
56
57
  debug("@@B3Provider:isConnected", isConnected);
57
58
  debug("@@wallets", wallets);
58
59
  debug("@@B3Provider:user", user);
60
+ debug("@@B3Provider:justCompletedLogin", justCompletedLogin);
59
61
  // Use given accountOverride or activeAccount from thirdweb
60
62
  const effectiveAccount = isAuthenticated ? accountOverride || activeAccount : undefined;
61
63
  const setWallet = useCallback((wallet) => {
@@ -100,6 +102,7 @@ export function InnerProvider({ children, accountOverride, environment, defaultP
100
102
  clientType,
101
103
  partnerId: partnerId,
102
104
  createClientReferenceId,
105
+ enableTurnkey,
103
106
  }, children: _jsx(InnerProvider2, { children: children }) }));
104
107
  }
105
108
  const InnerProvider2 = ({ children }) => {
@@ -23,6 +23,7 @@ export interface B3ContextType {
23
23
  clientType: ClientType;
24
24
  partnerId: string;
25
25
  createClientReferenceId?: (params: CreateOrderParams | CreateOnrampOrderParams) => Promise<string>;
26
+ enableTurnkey?: boolean;
26
27
  }
27
28
  /**
28
29
  * Context for B3 provider
@@ -17,4 +17,5 @@ export const B3Context = createContext({
17
17
  clientType: "rest",
18
18
  partnerId: "",
19
19
  createClientReferenceId: undefined,
20
+ enableTurnkey: false,
20
21
  });
@@ -13,16 +13,18 @@ const MAX_REFETCH_ATTEMPTS = 20;
13
13
  * Handles different login providers, authentication steps, and session key management
14
14
  */
15
15
  export function SignInWithB3Flow({ strategies, onLoginSuccess, onSessionKeySuccess, onError, chain, sessionKeyAddress, partnerId, closeAfterLogin = false, source = "signInWithB3Button", signersEnabled = false, }) {
16
- const { automaticallySetFirstEoa } = useB3();
16
+ const { automaticallySetFirstEoa, user, refetchUser, enableTurnkey } = useB3();
17
17
  const [step, setStep] = useState(source === "requestPermissions" ? null : "login");
18
18
  const [sessionKeyAdded, setSessionKeyAdded] = useState(source === "requestPermissions" ? true : false);
19
- const { setB3ModalContentType, setB3ModalOpen, isOpen } = useModalStore();
19
+ const { setB3ModalContentType, setB3ModalOpen, isOpen, contentType } = useModalStore();
20
20
  const account = useActiveAccount();
21
21
  const isAuthenticating = useAuthStore(state => state.isAuthenticating);
22
22
  const isAuthenticated = useAuthStore(state => state.isAuthenticated);
23
23
  const isConnected = useAuthStore(state => state.isConnected);
24
+ const setJustCompletedLogin = useAuthStore(state => state.setJustCompletedLogin);
24
25
  const [refetchCount, setRefetchCount] = useState(0);
25
26
  const [refetchError, setRefetchError] = useState(null);
27
+ const [turnkeyAuthCompleted, setTurnkeyAuthCompleted] = useState(false);
26
28
  const { data: signers, refetch: refetchSigners, isFetching: isFetchingSigners, } = useGetAllTWSigners({
27
29
  chain,
28
30
  accountAddress: account?.address,
@@ -52,6 +54,104 @@ export function SignInWithB3Flow({ strategies, onLoginSuccess, onSessionKeySucce
52
54
  setRefetchQueued(false);
53
55
  }, backoffDelay);
54
56
  }, [refetchCount, refetchSigners, onError, setRefetchQueued, refetchQueued]);
57
+ // Extract the completion flow logic to be reused
58
+ const handlePostTurnkeyFlow = useCallback(() => {
59
+ debug("Running post-Turnkey flow logic");
60
+ // Check if we already have a signer for this partner
61
+ const hasExistingSigner = signers?.some(signer => signer.partner.id === partnerId);
62
+ if (hasExistingSigner) {
63
+ // Path 1: User already has a signer for this partner
64
+ setSessionKeyAdded(true);
65
+ onSessionKeySuccess?.();
66
+ if (closeAfterLogin) {
67
+ setB3ModalOpen(false);
68
+ }
69
+ else {
70
+ setB3ModalContentType({
71
+ type: "manageAccount",
72
+ chain,
73
+ partnerId,
74
+ });
75
+ }
76
+ }
77
+ else if (signersEnabled) {
78
+ // Path 2: No existing signer, but signers are enabled
79
+ if (source !== "requestPermissions") {
80
+ // Navigate to permissions step to request new signer
81
+ setStep("permissions");
82
+ }
83
+ else {
84
+ // Already in request permissions flow, retry fetching signers
85
+ handleRefetchSigners();
86
+ }
87
+ }
88
+ else {
89
+ // Path 3: No existing signer and signers are not enabled
90
+ // Default handling for when no signer exists and signers are not enabled
91
+ if (closeAfterLogin) {
92
+ setB3ModalOpen(false);
93
+ }
94
+ else {
95
+ // if not closed, default to manage account
96
+ setB3ModalContentType({
97
+ type: "manageAccount",
98
+ chain,
99
+ partnerId,
100
+ });
101
+ }
102
+ }
103
+ }, [
104
+ signers,
105
+ partnerId,
106
+ onSessionKeySuccess,
107
+ closeAfterLogin,
108
+ setB3ModalOpen,
109
+ setB3ModalContentType,
110
+ chain,
111
+ source,
112
+ signersEnabled,
113
+ handleRefetchSigners,
114
+ setSessionKeyAdded,
115
+ ]);
116
+ // Define handleTurnkeySuccess before the useEffect that uses it
117
+ const handleTurnkeySuccess = useCallback(async (user) => {
118
+ debug("Turnkey authentication successful - setting completed flag", { user });
119
+ // Set completed flag FIRST before any async operations
120
+ setTurnkeyAuthCompleted(true);
121
+ // Refetch user to update the user state with Turnkey ID
122
+ debug("Refetching user after Turnkey success...");
123
+ await refetchUser();
124
+ debug("User refetched successfully");
125
+ // After user data is refreshed, close Turnkey modal and go back to sign-in flow
126
+ debug("Switching back to signInWithB3 modal");
127
+ setB3ModalContentType({
128
+ type: "signInWithB3",
129
+ strategies,
130
+ onLoginSuccess,
131
+ onSessionKeySuccess,
132
+ onError,
133
+ chain,
134
+ sessionKeyAddress,
135
+ partnerId,
136
+ closeAfterLogin,
137
+ source,
138
+ signersEnabled,
139
+ });
140
+ // The useEffect will re-run with updated user data to complete the sign-in process
141
+ }, [
142
+ refetchUser,
143
+ setB3ModalContentType,
144
+ strategies,
145
+ onLoginSuccess,
146
+ onSessionKeySuccess,
147
+ onError,
148
+ chain,
149
+ sessionKeyAddress,
150
+ partnerId,
151
+ closeAfterLogin,
152
+ source,
153
+ signersEnabled,
154
+ ]);
55
155
  // Handle post-login flow after signers are loaded
56
156
  useEffect(() => {
57
157
  debug("@@SignInWithB3Flow:useEffect", {
@@ -62,41 +162,46 @@ export function SignInWithB3Flow({ strategies, onLoginSuccess, onSessionKeySucce
62
162
  isOpen,
63
163
  source,
64
164
  });
65
- if (isConnected && isAuthenticated) {
66
- // Check if we already have a signer for this partner
67
- const hasExistingSigner = signers?.some(signer => signer.partner.id === partnerId);
68
- if (hasExistingSigner) {
69
- setSessionKeyAdded(true);
70
- onSessionKeySuccess?.();
71
- if (closeAfterLogin) {
72
- setB3ModalOpen(false);
73
- }
74
- else {
75
- setB3ModalContentType({
76
- type: "manageAccount",
77
- chain,
78
- partnerId,
79
- });
80
- }
81
- }
82
- else if (source !== "requestPermissions") {
83
- if (signersEnabled)
84
- setStep("permissions");
85
- }
86
- else {
87
- if (signersEnabled)
88
- handleRefetchSigners();
89
- }
90
- // Default handling
165
+ if (isConnected && isAuthenticated && user) {
166
+ // Mark that login just completed BEFORE opening manage account or closing modal
167
+ // This allows Turnkey modal to show (if enableTurnkey is true)
91
168
  if (closeAfterLogin) {
92
- setB3ModalOpen(false);
169
+ setJustCompletedLogin(true);
93
170
  }
94
- // if not closed, always default to manage account
95
- setB3ModalContentType({
96
- type: "manageAccount",
97
- chain,
98
- partnerId,
99
- });
171
+ // Check if we should show Turnkey login form
172
+ // Show if enableTurnkey is true AND user just logged in AND hasn't completed Turnkey auth in this session
173
+ // For new users (!turnkeyId): Show email form
174
+ // For returning users (turnkeyId && turnkeyEmail): Auto-skip to OTP
175
+ // Also check that we're not already showing the Turnkey modal
176
+ const hasTurnkeyId = user?.partnerIds?.turnkeyId;
177
+ const hasTurnkeyEmail = !!user?.email;
178
+ const isTurnkeyModalCurrentlyOpen = contentType?.type === "turnkeyAuth";
179
+ const shouldShowTurnkeyModal = enableTurnkey &&
180
+ user &&
181
+ !turnkeyAuthCompleted &&
182
+ !isTurnkeyModalCurrentlyOpen &&
183
+ (!hasTurnkeyId || (hasTurnkeyId && hasTurnkeyEmail));
184
+ if (shouldShowTurnkeyModal) {
185
+ // Extract email from user object - check partnerIds.turnkeyEmail first, then twProfiles, then user.email
186
+ const email = user?.email || user?.twProfiles?.find((profile) => profile.details?.email)?.details?.email;
187
+ // Open Turnkey modal through the modal store
188
+ setB3ModalContentType({
189
+ type: "turnkeyAuth",
190
+ onSuccess: handleTurnkeySuccess,
191
+ onClose: () => {
192
+ // After closing Turnkey modal, continue with the rest of the flow
193
+ setTurnkeyAuthCompleted(true);
194
+ debug("Turnkey modal closed, running post-Turnkey flow");
195
+ handlePostTurnkeyFlow();
196
+ },
197
+ initialEmail: email,
198
+ skipToOtp: !!(hasTurnkeyId && hasTurnkeyEmail),
199
+ closable: false, // Turnkey modal cannot be closed until auth is complete
200
+ });
201
+ return;
202
+ }
203
+ // Normal flow continues after Turnkey auth is complete (or if not needed)
204
+ handlePostTurnkeyFlow();
100
205
  }
101
206
  }, [
102
207
  signers,
@@ -114,6 +219,13 @@ export function SignInWithB3Flow({ strategies, onLoginSuccess, onSessionKeySucce
114
219
  isAuthenticating,
115
220
  isAuthenticated,
116
221
  isOpen,
222
+ setJustCompletedLogin,
223
+ user,
224
+ enableTurnkey,
225
+ turnkeyAuthCompleted,
226
+ handleTurnkeySuccess,
227
+ contentType,
228
+ handlePostTurnkeyFlow,
117
229
  ]);
118
230
  debug("render", {
119
231
  step,
@@ -168,24 +280,28 @@ export function SignInWithB3Flow({ strategies, onLoginSuccess, onSessionKeySucce
168
280
  });
169
281
  }
170
282
  }, [chain, onError, onSessionKeySuccessEnhanced, sessionKeyAddress, setB3ModalContentType, step]);
283
+ // Render content based on current step/state
284
+ let content = null;
171
285
  // Display error if refetch limit exceeded
172
286
  if (refetchError) {
173
- return (_jsx(LoginStepContainer, { partnerId: partnerId, children: _jsx("div", { className: "p-4 text-center text-red-500", children: refetchError }) }));
287
+ content = (_jsx(LoginStepContainer, { partnerId: partnerId, children: _jsx("div", { className: "p-4 text-center text-red-500", children: refetchError }) }));
174
288
  }
175
- if (isAuthenticating || (isFetchingSigners && step === "login") || source === "requestPermissions") {
176
- return (_jsx(LoginStepContainer, { partnerId: partnerId, children: _jsx("div", { className: "my-8 flex min-h-[350px] items-center justify-center", children: _jsx(Loading, { variant: "white", size: "lg" }) }) }));
289
+ else if (isAuthenticating || (isFetchingSigners && step === "login") || source === "requestPermissions") {
290
+ content = (_jsx(LoginStepContainer, { partnerId: partnerId, children: _jsx("div", { className: "my-8 flex min-h-[350px] items-center justify-center", children: _jsx(Loading, { variant: "white", size: "lg" }) }) }));
177
291
  }
178
- if (step === "login") {
292
+ else if (step === "login") {
179
293
  // Custom strategy
180
294
  if (strategies?.[0] === "privy") {
181
- return _jsx(SignInWithB3Privy, { onSuccess: handleLoginSuccess, chain: chain });
295
+ content = _jsx(SignInWithB3Privy, { onSuccess: handleLoginSuccess, chain: chain });
296
+ }
297
+ else if (strategies) {
298
+ // Strategies are explicitly provided
299
+ content = (_jsx(LoginStepCustom, { strategies: strategies, chain: chain, onSuccess: handleLoginSuccess, onError: onError, automaticallySetFirstEoa: !!automaticallySetFirstEoa }));
182
300
  }
183
- // Strategies are explicitly provided
184
- if (strategies) {
185
- return (_jsx(LoginStepCustom, { strategies: strategies, chain: chain, onSuccess: handleLoginSuccess, onError: onError, automaticallySetFirstEoa: !!automaticallySetFirstEoa }));
301
+ else {
302
+ // Default to handle all strategies we support
303
+ content = _jsx(LoginStep, { chain: chain, onSuccess: handleLoginSuccess, onError: onError });
186
304
  }
187
- // Default to handle all strategies we support
188
- return _jsx(LoginStep, { chain: chain, onSuccess: handleLoginSuccess, onError: onError });
189
305
  }
190
- return null;
306
+ return content;
191
307
  }
@@ -0,0 +1,8 @@
1
+ interface TurnkeyAuthModalProps {
2
+ onClose: () => void;
3
+ onSuccess: (_user: any) => void;
4
+ initialEmail?: string;
5
+ skipToOtp?: boolean;
6
+ }
7
+ export declare function TurnkeyAuthModal({ onClose, onSuccess, initialEmail, skipToOtp }: TurnkeyAuthModalProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};