@b3dotfun/sdk 0.0.77-alpha.0 → 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 (63) hide show
  1. package/dist/cjs/global-account/react/components/B3DynamicModal.js +18 -4
  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/ManageAccount/BottomNavigation.js +2 -2
  7. package/dist/cjs/global-account/react/components/SignInWithB3/SignInWithB3Flow.js +162 -46
  8. package/dist/cjs/global-account/react/components/TurnkeyAuthModal.d.ts +8 -0
  9. package/dist/cjs/global-account/react/components/TurnkeyAuthModal.js +84 -0
  10. package/dist/cjs/global-account/react/components/index.d.ts +1 -0
  11. package/dist/cjs/global-account/react/components/index.js +6 -3
  12. package/dist/cjs/global-account/react/hooks/index.d.ts +1 -0
  13. package/dist/cjs/global-account/react/hooks/index.js +3 -1
  14. package/dist/cjs/global-account/react/hooks/useAuthentication.d.ts +7 -0
  15. package/dist/cjs/global-account/react/hooks/useTurnkeyAuth.d.ts +20 -0
  16. package/dist/cjs/global-account/react/hooks/useTurnkeyAuth.js +112 -0
  17. package/dist/cjs/global-account/react/hooks/useUserQuery.d.ts +7 -0
  18. package/dist/cjs/global-account/react/stores/useAuthStore.d.ts +2 -0
  19. package/dist/cjs/global-account/react/stores/useAuthStore.js +2 -0
  20. package/dist/cjs/global-account/react/stores/useModalStore.d.ts +21 -1
  21. package/dist/esm/global-account/react/components/B3DynamicModal.js +18 -4
  22. package/dist/esm/global-account/react/components/B3Provider/B3Provider.d.ts +4 -2
  23. package/dist/esm/global-account/react/components/B3Provider/B3Provider.js +6 -3
  24. package/dist/esm/global-account/react/components/B3Provider/types.d.ts +1 -0
  25. package/dist/esm/global-account/react/components/B3Provider/types.js +1 -0
  26. package/dist/esm/global-account/react/components/ManageAccount/BottomNavigation.js +2 -2
  27. package/dist/esm/global-account/react/components/SignInWithB3/SignInWithB3Flow.js +162 -46
  28. package/dist/esm/global-account/react/components/TurnkeyAuthModal.d.ts +8 -0
  29. package/dist/esm/global-account/react/components/TurnkeyAuthModal.js +81 -0
  30. package/dist/esm/global-account/react/components/index.d.ts +1 -0
  31. package/dist/esm/global-account/react/components/index.js +2 -0
  32. package/dist/esm/global-account/react/hooks/index.d.ts +1 -0
  33. package/dist/esm/global-account/react/hooks/index.js +1 -0
  34. package/dist/esm/global-account/react/hooks/useAuthentication.d.ts +7 -0
  35. package/dist/esm/global-account/react/hooks/useTurnkeyAuth.d.ts +20 -0
  36. package/dist/esm/global-account/react/hooks/useTurnkeyAuth.js +106 -0
  37. package/dist/esm/global-account/react/hooks/useUserQuery.d.ts +7 -0
  38. package/dist/esm/global-account/react/stores/useAuthStore.d.ts +2 -0
  39. package/dist/esm/global-account/react/stores/useAuthStore.js +2 -0
  40. package/dist/esm/global-account/react/stores/useModalStore.d.ts +21 -1
  41. package/dist/styles/index.css +1 -1
  42. package/dist/types/global-account/react/components/B3Provider/B3Provider.d.ts +4 -2
  43. package/dist/types/global-account/react/components/B3Provider/types.d.ts +1 -0
  44. package/dist/types/global-account/react/components/TurnkeyAuthModal.d.ts +8 -0
  45. package/dist/types/global-account/react/components/index.d.ts +1 -0
  46. package/dist/types/global-account/react/hooks/index.d.ts +1 -0
  47. package/dist/types/global-account/react/hooks/useAuthentication.d.ts +7 -0
  48. package/dist/types/global-account/react/hooks/useTurnkeyAuth.d.ts +20 -0
  49. package/dist/types/global-account/react/hooks/useUserQuery.d.ts +7 -0
  50. package/dist/types/global-account/react/stores/useAuthStore.d.ts +2 -0
  51. package/dist/types/global-account/react/stores/useModalStore.d.ts +21 -1
  52. package/package.json +2 -2
  53. package/src/global-account/react/components/B3DynamicModal.tsx +26 -3
  54. package/src/global-account/react/components/B3Provider/B3Provider.tsx +9 -0
  55. package/src/global-account/react/components/B3Provider/types.ts +2 -0
  56. package/src/global-account/react/components/ManageAccount/BottomNavigation.tsx +3 -3
  57. package/src/global-account/react/components/SignInWithB3/SignInWithB3Flow.tsx +170 -48
  58. package/src/global-account/react/components/TurnkeyAuthModal.tsx +240 -0
  59. package/src/global-account/react/components/index.ts +3 -0
  60. package/src/global-account/react/hooks/index.ts +1 -0
  61. package/src/global-account/react/hooks/useTurnkeyAuth.ts +138 -0
  62. package/src/global-account/react/stores/useAuthStore.ts +4 -0
  63. package/src/global-account/react/stores/useModalStore.ts +22 -0
@@ -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 {};
@@ -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
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b3dotfun/sdk",
3
- "version": "0.0.77-alpha.0",
3
+ "version": "0.0.77-alpha.2",
4
4
  "source": "src/index.ts",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "react-native": "./dist/cjs/index.native.js",
@@ -283,7 +283,7 @@
283
283
  ],
284
284
  "dependencies": {
285
285
  "@adraffy/ens-normalize": "1.11.1",
286
- "@b3dotfun/b3-api": "0.0.87",
286
+ "@b3dotfun/b3-api": "0.0.89",
287
287
  "@b3dotfun/basement-api": "0.0.11",
288
288
  "@feathersjs/authentication-client": "5.0.33",
289
289
  "@feathersjs/feathers": "5.0.33",
@@ -11,6 +11,7 @@ import {
11
11
  OrderHistory,
12
12
  } from "@b3dotfun/sdk/anyspend/react";
13
13
  import { AnySpendDepositHype } from "@b3dotfun/sdk/anyspend/react/components/AnyspendDepositHype";
14
+ import { AnySpendDepositUpside } from "@b3dotfun/sdk/anyspend/react/components/AnySpendDepositUpside";
14
15
  import { AnySpendStakeUpside } from "@b3dotfun/sdk/anyspend/react/components/AnySpendStakeUpside";
15
16
  import { AnySpendStakeUpsideExactIn } from "@b3dotfun/sdk/anyspend/react/components/AnySpendStakeUpsideExactIn";
16
17
  import { useGlobalAccount, useIsMobile, useModalStore } from "@b3dotfun/sdk/global-account/react";
@@ -29,10 +30,10 @@ import NotificationsContent from "./ManageAccount/NotificationsContent";
29
30
  import { RequestPermissions } from "./RequestPermissions/RequestPermissions";
30
31
  import { Send } from "./Send/Send";
31
32
  import { SignInWithB3Flow } from "./SignInWithB3/SignInWithB3Flow";
33
+ import { TurnkeyAuthModal } from "./TurnkeyAuthModal";
32
34
  import { ToastContainer, useToastContext } from "./Toast/index";
33
35
  import { Dialog, DialogContent, DialogDescription, DialogTitle } from "./ui/dialog";
34
36
  import { Drawer, DrawerContent, DrawerDescription, DrawerTitle } from "./ui/drawer";
35
- import { AnySpendDepositUpside } from "@b3dotfun/sdk/anyspend/react/components/AnySpendDepositUpside";
36
37
 
37
38
  const debug = debugB3React("B3DynamicModal");
38
39
 
@@ -72,6 +73,7 @@ export function B3DynamicModal() {
72
73
  "anySpendBuySpin",
73
74
  "anySpendOrderHistory",
74
75
  "signInWithB3",
76
+ "turnkeyAuth",
75
77
  "anySpendSignatureMint",
76
78
  "anySpendBondKit",
77
79
  "linkAccount",
@@ -97,6 +99,8 @@ export function B3DynamicModal() {
97
99
 
98
100
  // Check if current content type is in freestyle types
99
101
  const isFreestyleType = freestyleTypes.includes(contentType?.type as string);
102
+ // Determine if modal should be closable - defaults to true unless explicitly set to false
103
+ const isClosable = contentType?.closable !== false;
100
104
  const hideCloseButton = true;
101
105
 
102
106
  // Build content class using cn utility
@@ -107,6 +111,7 @@ export function B3DynamicModal() {
107
111
  fullWidthTypes.includes(contentType?.type as string) && "w-full",
108
112
  isFreestyleType && "b3-modal-freestyle",
109
113
  contentType?.type === "signInWithB3" && "p-0",
114
+ contentType?.type === "turnkeyAuth" && "p-0",
110
115
  contentType?.type === "anySpend" && "md:p-0",
111
116
  contentType?.type === "send" && "p-0",
112
117
  contentType?.type === "manageAccount" && " md:p-0 md:pt-2",
@@ -123,6 +128,8 @@ export function B3DynamicModal() {
123
128
  switch (contentType.type) {
124
129
  case "signInWithB3":
125
130
  return <SignInWithB3Flow {...contentType} />;
131
+ case "turnkeyAuth":
132
+ return <TurnkeyAuthModal {...contentType} />;
126
133
  case "requestPermissions":
127
134
  return <RequestPermissions {...contentType} />;
128
135
  case "manageAccount":
@@ -182,8 +189,17 @@ export function B3DynamicModal() {
182
189
  const ModalTitle = isMobile ? DrawerTitle : DialogTitle;
183
190
  const ModalDescription = isMobile ? DrawerDescription : DialogDescription;
184
191
 
192
+ // Create a wrapper for onOpenChange that respects closable property
193
+ const handleOpenChange = (open: boolean) => {
194
+ // Only allow closing if the modal is closable
195
+ if (!open && !isClosable) {
196
+ return;
197
+ }
198
+ setB3ModalOpen(open);
199
+ };
200
+
185
201
  return (
186
- <ModalComponent open={isOpen} onOpenChange={setB3ModalOpen}>
202
+ <ModalComponent open={isOpen} onOpenChange={handleOpenChange}>
187
203
  <ModalContent
188
204
  className={cn(
189
205
  contentClass,
@@ -198,11 +214,18 @@ export function B3DynamicModal() {
198
214
  "mx-auto w-full max-w-md sm:max-w-lg",
199
215
  )}
200
216
  hideCloseButton={hideCloseButton}
217
+ onEscapeKeyDown={!isClosable ? e => e.preventDefault() : undefined}
218
+ onPointerDownOutside={!isClosable ? e => e.preventDefault() : undefined}
219
+ onInteractOutside={!isClosable ? e => e.preventDefault() : undefined}
201
220
  >
202
221
  <ModalTitle className="sr-only hidden">{contentType?.type || "Modal"}</ModalTitle>
203
222
  <ModalDescription className="sr-only hidden">{contentType?.type || "Modal Body"}</ModalDescription>
204
223
 
205
- <div className={cn("no-scrollbar flex max-h-[90dvh] flex-col overflow-auto sm:max-h-[80dvh]")}>
224
+ <div
225
+ className={cn(
226
+ "b3-modal-content no-scrollbar dark:bg-b3-background flex max-h-[90dvh] flex-col overflow-auto sm:max-h-[80dvh]",
227
+ )}
228
+ >
206
229
  {!hideCloseButton && (
207
230
  <button
208
231
  onClick={navigateBack}
@@ -62,6 +62,7 @@ export function B3Provider({
62
62
  connectors,
63
63
  overrideDefaultConnectors = false,
64
64
  createClientReferenceId,
65
+ enableTurnkey = false,
65
66
  }: {
66
67
  theme: "light" | "dark";
67
68
  children: React.ReactNode;
@@ -80,6 +81,7 @@ export function B3Provider({
80
81
  connectors?: CreateConnectorFn[];
81
82
  overrideDefaultConnectors?: boolean;
82
83
  createClientReferenceId?: (params: CreateOrderParams | CreateOnrampOrderParams) => Promise<string>;
84
+ enableTurnkey?: boolean;
83
85
  }) {
84
86
  // Initialize Google Analytics on mount
85
87
  useEffect(() => {
@@ -107,6 +109,7 @@ export function B3Provider({
107
109
  clientType={clientType}
108
110
  partnerId={partnerId}
109
111
  createClientReferenceId={createClientReferenceId}
112
+ enableTurnkey={enableTurnkey}
110
113
  >
111
114
  <ToastContextConnector />
112
115
  <RelayKitProviderWrapper simDuneApiKey={simDuneApiKey}>
@@ -137,6 +140,7 @@ export function InnerProvider({
137
140
  clientType = "socket",
138
141
  partnerId,
139
142
  createClientReferenceId,
143
+ enableTurnkey,
140
144
  }: {
141
145
  children: React.ReactNode;
142
146
  accountOverride?: Account;
@@ -147,17 +151,21 @@ export function InnerProvider({
147
151
  clientType?: ClientType;
148
152
  partnerId: string;
149
153
  createClientReferenceId?: (params: CreateOrderParams | CreateOnrampOrderParams) => Promise<string>;
154
+ enableTurnkey?: boolean;
150
155
  }) {
151
156
  const activeAccount = useActiveAccount();
152
157
  const [manuallySelectedWallet, setManuallySelectedWallet] = useState<Wallet | undefined>(undefined);
153
158
  const wallets = useConnectedWallets();
154
159
  const isAuthenticated = useAuthStore(state => state.isAuthenticated);
155
160
  const isConnected = useAuthStore(state => state.isConnected);
161
+ const justCompletedLogin = useAuthStore(state => state.justCompletedLogin);
156
162
  const setActiveWallet = useSetActiveWallet();
157
163
  const { user, setUser, refetchUser } = useAuthentication(partnerId);
164
+
158
165
  debug("@@B3Provider:isConnected", isConnected);
159
166
  debug("@@wallets", wallets);
160
167
  debug("@@B3Provider:user", user);
168
+ debug("@@B3Provider:justCompletedLogin", justCompletedLogin);
161
169
 
162
170
  // Use given accountOverride or activeAccount from thirdweb
163
171
  const effectiveAccount = isAuthenticated ? accountOverride || activeAccount : undefined;
@@ -215,6 +223,7 @@ export function InnerProvider({
215
223
  clientType,
216
224
  partnerId: partnerId,
217
225
  createClientReferenceId,
226
+ enableTurnkey,
218
227
  }}
219
228
  >
220
229
  <InnerProvider2>{children}</InnerProvider2>
@@ -25,6 +25,7 @@ export interface B3ContextType {
25
25
  clientType: ClientType;
26
26
  partnerId: string;
27
27
  createClientReferenceId?: (params: CreateOrderParams | CreateOnrampOrderParams) => Promise<string>;
28
+ enableTurnkey?: boolean;
28
29
  }
29
30
 
30
31
  /**
@@ -45,4 +46,5 @@ export const B3Context = createContext<B3ContextType>({
45
46
  clientType: "rest",
46
47
  partnerId: "",
47
48
  createClientReferenceId: undefined,
49
+ enableTurnkey: false,
48
50
  });
@@ -48,7 +48,7 @@ const BottomNavigation = () => {
48
48
  <TabsListPrimitive className="flex h-[68px] w-full items-center justify-center gap-4 border-none bg-transparent">
49
49
  <TabTriggerPrimitive
50
50
  value="home"
51
- className="data-[state=active]:border-b3-primary-blue group flex flex-initial flex-col items-center gap-1 border-r-0 border-t-0 px-6 pb-2 pt-2.5 text-[#a0a0ab] data-[state=active]:border-t-4 data-[state=active]:text-[#18181B]"
51
+ className="data-[state=active]:border-b3-primary-blue group flex flex-initial flex-col items-center gap-1 border-r-0 border-t-0 px-6 pb-2 pt-2.5 text-[#a0a0ab] data-[state=active]:border-t-4 data-[state=active]:text-[#18181B] dark:data-[state=active]:text-white"
52
52
  >
53
53
  <HomeIcon />
54
54
  <span className="text-b3-grey font-neue-montreal-semibold text-xs">Home</span>
@@ -56,7 +56,7 @@ const BottomNavigation = () => {
56
56
 
57
57
  <TabTriggerPrimitive
58
58
  value="swap"
59
- className="data-[state=active]:border-b3-primary-blue group flex flex-initial flex-col items-center gap-1 border-r-0 border-t-0 px-6 pb-2 pt-2.5 text-[#a0a0ab] data-[state=active]:border-t-4 data-[state=active]:text-[#18181B]"
59
+ className="data-[state=active]:border-b3-primary-blue group flex flex-initial flex-col items-center gap-1 border-r-0 border-t-0 px-6 pb-2 pt-2.5 text-[#a0a0ab] data-[state=active]:border-t-4 data-[state=active]:text-[#18181B] dark:data-[state=active]:text-white"
60
60
  onClick={() => {
61
61
  setB3ModalContentType({
62
62
  type: "anySpend",
@@ -70,7 +70,7 @@ const BottomNavigation = () => {
70
70
 
71
71
  <TabTriggerPrimitive
72
72
  value="settings"
73
- className="data-[state=active]:border-b3-primary-blue group flex flex-initial flex-col items-center gap-1 border-r-0 border-t-0 px-6 pb-2 pt-2.5 text-[#a0a0ab] data-[state=active]:border-t-4 data-[state=active]:text-[#18181B]"
73
+ className="data-[state=active]:border-b3-primary-blue group flex flex-initial flex-col items-center gap-1 border-r-0 border-t-0 px-6 pb-2 pt-2.5 text-[#a0a0ab] data-[state=active]:border-t-4 data-[state=active]:text-[#18181B] dark:data-[state=active]:text-white"
74
74
  >
75
75
  <SettingsIcon />
76
76
  <span className="text-b3-grey font-neue-montreal-semibold text-xs">Settings</span>
@@ -33,16 +33,18 @@ export function SignInWithB3Flow({
33
33
  source = "signInWithB3Button",
34
34
  signersEnabled = false,
35
35
  }: SignInWithB3ModalProps) {
36
- const { automaticallySetFirstEoa } = useB3();
36
+ const { automaticallySetFirstEoa, user, refetchUser, enableTurnkey } = useB3();
37
37
  const [step, setStep] = useState<"login" | "permissions" | null>(source === "requestPermissions" ? null : "login");
38
38
  const [sessionKeyAdded, setSessionKeyAdded] = useState(source === "requestPermissions" ? true : false);
39
- const { setB3ModalContentType, setB3ModalOpen, isOpen } = useModalStore();
39
+ const { setB3ModalContentType, setB3ModalOpen, isOpen, contentType } = useModalStore();
40
40
  const account = useActiveAccount();
41
41
  const isAuthenticating = useAuthStore(state => state.isAuthenticating);
42
42
  const isAuthenticated = useAuthStore(state => state.isAuthenticated);
43
43
  const isConnected = useAuthStore(state => state.isConnected);
44
+ const setJustCompletedLogin = useAuthStore(state => state.setJustCompletedLogin);
44
45
  const [refetchCount, setRefetchCount] = useState(0);
45
46
  const [refetchError, setRefetchError] = useState<string | null>(null);
47
+ const [turnkeyAuthCompleted, setTurnkeyAuthCompleted] = useState(false);
46
48
  const {
47
49
  data: signers,
48
50
  refetch: refetchSigners,
@@ -82,6 +84,109 @@ export function SignInWithB3Flow({
82
84
  }, backoffDelay);
83
85
  }, [refetchCount, refetchSigners, onError, setRefetchQueued, refetchQueued]);
84
86
 
87
+ // Extract the completion flow logic to be reused
88
+ const handlePostTurnkeyFlow = useCallback(() => {
89
+ debug("Running post-Turnkey flow logic");
90
+
91
+ // Check if we already have a signer for this partner
92
+ const hasExistingSigner = signers?.some(signer => signer.partner.id === partnerId);
93
+
94
+ if (hasExistingSigner) {
95
+ // Path 1: User already has a signer for this partner
96
+ setSessionKeyAdded(true);
97
+ onSessionKeySuccess?.();
98
+ if (closeAfterLogin) {
99
+ setB3ModalOpen(false);
100
+ } else {
101
+ setB3ModalContentType({
102
+ type: "manageAccount",
103
+ chain,
104
+ partnerId,
105
+ });
106
+ }
107
+ } else if (signersEnabled) {
108
+ // Path 2: No existing signer, but signers are enabled
109
+ if (source !== "requestPermissions") {
110
+ // Navigate to permissions step to request new signer
111
+ setStep("permissions");
112
+ } else {
113
+ // Already in request permissions flow, retry fetching signers
114
+ handleRefetchSigners();
115
+ }
116
+ } else {
117
+ // Path 3: No existing signer and signers are not enabled
118
+ // Default handling for when no signer exists and signers are not enabled
119
+ if (closeAfterLogin) {
120
+ setB3ModalOpen(false);
121
+ } else {
122
+ // if not closed, default to manage account
123
+ setB3ModalContentType({
124
+ type: "manageAccount",
125
+ chain,
126
+ partnerId,
127
+ });
128
+ }
129
+ }
130
+ }, [
131
+ signers,
132
+ partnerId,
133
+ onSessionKeySuccess,
134
+ closeAfterLogin,
135
+ setB3ModalOpen,
136
+ setB3ModalContentType,
137
+ chain,
138
+ source,
139
+ signersEnabled,
140
+ handleRefetchSigners,
141
+ setSessionKeyAdded,
142
+ ]);
143
+
144
+ // Define handleTurnkeySuccess before the useEffect that uses it
145
+ const handleTurnkeySuccess = useCallback(
146
+ async (user: any) => {
147
+ debug("Turnkey authentication successful - setting completed flag", { user });
148
+
149
+ // Set completed flag FIRST before any async operations
150
+ setTurnkeyAuthCompleted(true);
151
+
152
+ // Refetch user to update the user state with Turnkey ID
153
+ debug("Refetching user after Turnkey success...");
154
+ await refetchUser();
155
+ debug("User refetched successfully");
156
+
157
+ // After user data is refreshed, close Turnkey modal and go back to sign-in flow
158
+ debug("Switching back to signInWithB3 modal");
159
+ setB3ModalContentType({
160
+ type: "signInWithB3",
161
+ strategies,
162
+ onLoginSuccess,
163
+ onSessionKeySuccess,
164
+ onError,
165
+ chain,
166
+ sessionKeyAddress,
167
+ partnerId,
168
+ closeAfterLogin,
169
+ source,
170
+ signersEnabled,
171
+ });
172
+ // The useEffect will re-run with updated user data to complete the sign-in process
173
+ },
174
+ [
175
+ refetchUser,
176
+ setB3ModalContentType,
177
+ strategies,
178
+ onLoginSuccess,
179
+ onSessionKeySuccess,
180
+ onError,
181
+ chain,
182
+ sessionKeyAddress,
183
+ partnerId,
184
+ closeAfterLogin,
185
+ source,
186
+ signersEnabled,
187
+ ],
188
+ );
189
+
85
190
  // Handle post-login flow after signers are loaded
86
191
  useEffect(() => {
87
192
  debug("@@SignInWithB3Flow:useEffect", {
@@ -93,38 +198,51 @@ export function SignInWithB3Flow({
93
198
  source,
94
199
  });
95
200
 
96
- if (isConnected && isAuthenticated) {
97
- // Check if we already have a signer for this partner
98
- const hasExistingSigner = signers?.some(signer => signer.partner.id === partnerId);
99
- if (hasExistingSigner) {
100
- setSessionKeyAdded(true);
101
- onSessionKeySuccess?.();
102
- if (closeAfterLogin) {
103
- setB3ModalOpen(false);
104
- } else {
105
- setB3ModalContentType({
106
- type: "manageAccount",
107
- chain,
108
- partnerId,
109
- });
110
- }
111
- } else if (source !== "requestPermissions") {
112
- if (signersEnabled) setStep("permissions");
113
- } else {
114
- if (signersEnabled) handleRefetchSigners();
201
+ if (isConnected && isAuthenticated && user) {
202
+ // Mark that login just completed BEFORE opening manage account or closing modal
203
+ // This allows Turnkey modal to show (if enableTurnkey is true)
204
+ if (closeAfterLogin) {
205
+ setJustCompletedLogin(true);
115
206
  }
116
207
 
117
- // Default handling
118
- if (closeAfterLogin) {
119
- setB3ModalOpen(false);
208
+ // Check if we should show Turnkey login form
209
+ // Show if enableTurnkey is true AND user just logged in AND hasn't completed Turnkey auth in this session
210
+ // For new users (!turnkeyId): Show email form
211
+ // For returning users (turnkeyId && turnkeyEmail): Auto-skip to OTP
212
+ // Also check that we're not already showing the Turnkey modal
213
+ const hasTurnkeyId = user?.partnerIds?.turnkeyId;
214
+ const hasTurnkeyEmail = !!user?.email;
215
+ const isTurnkeyModalCurrentlyOpen = contentType?.type === "turnkeyAuth";
216
+ const shouldShowTurnkeyModal =
217
+ enableTurnkey &&
218
+ user &&
219
+ !turnkeyAuthCompleted &&
220
+ !isTurnkeyModalCurrentlyOpen &&
221
+ (!hasTurnkeyId || (hasTurnkeyId && hasTurnkeyEmail));
222
+
223
+ if (shouldShowTurnkeyModal) {
224
+ // Extract email from user object - check partnerIds.turnkeyEmail first, then twProfiles, then user.email
225
+ const email = user?.email || user?.twProfiles?.find((profile: any) => profile.details?.email)?.details?.email;
226
+
227
+ // Open Turnkey modal through the modal store
228
+ setB3ModalContentType({
229
+ type: "turnkeyAuth",
230
+ onSuccess: handleTurnkeySuccess,
231
+ onClose: () => {
232
+ // After closing Turnkey modal, continue with the rest of the flow
233
+ setTurnkeyAuthCompleted(true);
234
+ debug("Turnkey modal closed, running post-Turnkey flow");
235
+ handlePostTurnkeyFlow();
236
+ },
237
+ initialEmail: email,
238
+ skipToOtp: !!(hasTurnkeyId && hasTurnkeyEmail),
239
+ closable: false, // Turnkey modal cannot be closed until auth is complete
240
+ });
241
+ return;
120
242
  }
121
243
 
122
- // if not closed, always default to manage account
123
- setB3ModalContentType({
124
- type: "manageAccount",
125
- chain,
126
- partnerId,
127
- });
244
+ // Normal flow continues after Turnkey auth is complete (or if not needed)
245
+ handlePostTurnkeyFlow();
128
246
  }
129
247
  }, [
130
248
  signers,
@@ -142,6 +260,13 @@ export function SignInWithB3Flow({
142
260
  isAuthenticating,
143
261
  isAuthenticated,
144
262
  isOpen,
263
+ setJustCompletedLogin,
264
+ user,
265
+ enableTurnkey,
266
+ turnkeyAuthCompleted,
267
+ handleTurnkeySuccess,
268
+ contentType,
269
+ handlePostTurnkeyFlow,
145
270
  ]);
146
271
 
147
272
  debug("render", {
@@ -205,34 +330,31 @@ export function SignInWithB3Flow({
205
330
  }
206
331
  }, [chain, onError, onSessionKeySuccessEnhanced, sessionKeyAddress, setB3ModalContentType, step]);
207
332
 
333
+ // Render content based on current step/state
334
+ let content = null;
335
+
208
336
  // Display error if refetch limit exceeded
209
337
  if (refetchError) {
210
- return (
338
+ content = (
211
339
  <LoginStepContainer partnerId={partnerId}>
212
340
  <div className="p-4 text-center text-red-500">{refetchError}</div>
213
341
  </LoginStepContainer>
214
342
  );
215
- }
216
-
217
- if (isAuthenticating || (isFetchingSigners && step === "login") || source === "requestPermissions") {
218
- return (
343
+ } else if (isAuthenticating || (isFetchingSigners && step === "login") || source === "requestPermissions") {
344
+ content = (
219
345
  <LoginStepContainer partnerId={partnerId}>
220
346
  <div className="my-8 flex min-h-[350px] items-center justify-center">
221
347
  <Loading variant="white" size="lg" />
222
348
  </div>
223
349
  </LoginStepContainer>
224
350
  );
225
- }
226
-
227
- if (step === "login") {
351
+ } else if (step === "login") {
228
352
  // Custom strategy
229
353
  if (strategies?.[0] === "privy") {
230
- return <SignInWithB3Privy onSuccess={handleLoginSuccess} chain={chain} />;
231
- }
232
-
233
- // Strategies are explicitly provided
234
- if (strategies) {
235
- return (
354
+ content = <SignInWithB3Privy onSuccess={handleLoginSuccess} chain={chain} />;
355
+ } else if (strategies) {
356
+ // Strategies are explicitly provided
357
+ content = (
236
358
  <LoginStepCustom
237
359
  strategies={strategies}
238
360
  chain={chain}
@@ -241,11 +363,11 @@ export function SignInWithB3Flow({
241
363
  automaticallySetFirstEoa={!!automaticallySetFirstEoa}
242
364
  />
243
365
  );
366
+ } else {
367
+ // Default to handle all strategies we support
368
+ content = <LoginStep chain={chain} onSuccess={handleLoginSuccess} onError={onError} />;
244
369
  }
245
-
246
- // Default to handle all strategies we support
247
- return <LoginStep chain={chain} onSuccess={handleLoginSuccess} onError={onError} />;
248
370
  }
249
371
 
250
- return null;
372
+ return content;
251
373
  }