@b3dotfun/sdk 0.0.88-alpha.2 → 0.0.88-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.
Files changed (31) hide show
  1. package/dist/cjs/global-account/react/components/B3Provider/RelayKitProviderWrapper.js +3 -1
  2. package/dist/cjs/global-account/react/components/SignInWithB3/SignInWithB3Flow.js +76 -20
  3. package/dist/cjs/global-account/react/components/TurnkeyAuthModal.js +3 -1
  4. package/dist/cjs/global-account/react/hooks/index.d.ts +1 -0
  5. package/dist/cjs/global-account/react/hooks/index.js +3 -1
  6. package/dist/cjs/global-account/react/hooks/useAuth.d.ts +76 -0
  7. package/dist/cjs/global-account/react/hooks/useAuth.js +338 -0
  8. package/dist/cjs/global-account/react/hooks/useTWAuth.d.ts +3 -0
  9. package/dist/cjs/global-account/react/hooks/useTWAuth.js +8 -0
  10. package/dist/cjs/global-account/react/hooks/useTurnkeyAuth.js +50 -22
  11. package/dist/esm/global-account/react/components/B3Provider/RelayKitProviderWrapper.js +3 -1
  12. package/dist/esm/global-account/react/components/SignInWithB3/SignInWithB3Flow.js +76 -20
  13. package/dist/esm/global-account/react/components/TurnkeyAuthModal.js +5 -3
  14. package/dist/esm/global-account/react/hooks/index.d.ts +1 -0
  15. package/dist/esm/global-account/react/hooks/index.js +1 -0
  16. package/dist/esm/global-account/react/hooks/useAuth.d.ts +76 -0
  17. package/dist/esm/global-account/react/hooks/useAuth.js +332 -0
  18. package/dist/esm/global-account/react/hooks/useTWAuth.d.ts +3 -0
  19. package/dist/esm/global-account/react/hooks/useTWAuth.js +8 -0
  20. package/dist/esm/global-account/react/hooks/useTurnkeyAuth.js +50 -22
  21. package/dist/types/global-account/react/hooks/index.d.ts +1 -0
  22. package/dist/types/global-account/react/hooks/useAuth.d.ts +76 -0
  23. package/dist/types/global-account/react/hooks/useTWAuth.d.ts +3 -0
  24. package/package.json +1 -1
  25. package/src/global-account/react/components/B3Provider/RelayKitProviderWrapper.tsx +4 -1
  26. package/src/global-account/react/components/SignInWithB3/SignInWithB3Flow.tsx +168 -100
  27. package/src/global-account/react/components/TurnkeyAuthModal.tsx +7 -4
  28. package/src/global-account/react/hooks/index.ts +1 -0
  29. package/src/global-account/react/hooks/useAuth.ts +380 -0
  30. package/src/global-account/react/hooks/useTWAuth.tsx +10 -0
  31. package/src/global-account/react/hooks/useTurnkeyAuth.ts +54 -23
@@ -1,8 +1,16 @@
1
+ /**
2
+ * @deprecated This hook is deprecated. Use useAuth() with Turnkey authentication instead.
3
+ * This file is kept for backward compatibility but should not be used in new code.
4
+ */
1
5
  import app from "../../../global-account/app.js";
2
6
  import debug from "../../../shared/utils/debug.js";
3
7
  import { useCallback } from "react";
4
8
  import { useSearchParam } from "./useSearchParamsSSR.js";
9
+ /**
10
+ * @deprecated Use useAuth() with Turnkey authentication instead
11
+ */
5
12
  export function useTWAuth() {
13
+ console.warn("useTWAuth is deprecated. Please migrate to useAuth() with Turnkey authentication. See useTurnkeyAuth.ts for the new implementation.");
6
14
  const referralCode = useSearchParam("referralCode");
7
15
  const authenticate = useCallback(async (wallet, partnerId) => {
8
16
  if (!wallet || !wallet?.getAuthToken?.())
@@ -3,7 +3,7 @@ import { useCallback, useState } from "react";
3
3
  import app from "../../app.js";
4
4
  import { useB3Config } from "../components/index.js";
5
5
  import { useAuthStore } from "../stores/index.js";
6
- import { useAuthentication } from "./useAuthentication.js";
6
+ import { useAuth } from "./useAuth.js";
7
7
  const debug = debugB3React("useTurnkeyAuth");
8
8
  /**
9
9
  * Hook for Turnkey email-based OTP authentication
@@ -19,29 +19,54 @@ export function useTurnkeyAuth() {
19
19
  const setIsAuthenticating = useAuthStore(state => state.setIsAuthenticating);
20
20
  const setIsAuthenticated = useAuthStore(state => state.setIsAuthenticated);
21
21
  const { partnerId } = useB3Config();
22
- const { user } = useAuthentication(partnerId);
22
+ const { authenticate } = useAuth();
23
23
  /**
24
24
  * Step 1: Initiate login with email
25
- * - Calls backend to create sub-org (if needed) and send OTP
25
+ * - Calls backend turnkey-jwt strategy (init action) to create sub-org (if needed) and send OTP
26
26
  * - Returns otpId to use in verification step
27
+ *
28
+ * Note: Uses the turnkey-jwt authentication strategy, not the service directly.
29
+ * The turnkey-jwt strategy handles both OTP flow (init/verify) and final authentication.
27
30
  */
28
31
  const initiateLogin = useCallback(async (email) => {
29
32
  setIsLoading(true);
30
33
  setError(null);
31
34
  setIsAuthenticating(true);
32
35
  try {
33
- if (!user?.userId) {
34
- throw new Error("User ID is required to initiate Turnkey login.");
35
- }
36
36
  debug(`Initiating login for: ${email}`);
37
- // Call FeathersJS service to initialize OTP
38
- const data = await app.service("turnkey-auth").init({ email, userId: user.userId });
37
+ // Use authentication service with turnkey-jwt strategy (init action)
38
+ // userId is resolved from authentication context on the backend (params.user.userId)
39
+ // Backend will get userId from _params.user?.userId if authenticated, or handle unauthenticated case
40
+ // So we only need to send email
41
+ debug(`Calling app.authenticate with turnkey-jwt strategy (init action)`, { email });
42
+ const response = await app.authenticate({
43
+ strategy: "turnkey-jwt",
44
+ action: "init",
45
+ email,
46
+ });
47
+ // The strategy returns the TurnkeyAuthInitResponse directly
48
+ const data = response;
39
49
  debug(`OTP initialized successfully. OtpId: ${data.otpId}`);
40
50
  return data;
41
51
  }
42
52
  catch (err) {
43
53
  debug("Error initiating login:", err);
44
- const errorMessage = err.message || "Failed to send OTP email. Please try again.";
54
+ // Provide more detailed error information
55
+ let errorMessage = "Failed to send OTP email. Please try again.";
56
+ if (err.message) {
57
+ errorMessage = err.message;
58
+ }
59
+ else if (err.name === "TypeError" && err.message?.includes("fetch")) {
60
+ errorMessage = "Network error: Unable to reach the server. Please check your connection and try again.";
61
+ }
62
+ else if (err.code === "ECONNREFUSED" || err.code === "ENOTFOUND") {
63
+ errorMessage = "Connection error: Unable to reach the server. Please check your internet connection.";
64
+ }
65
+ else if (err.response) {
66
+ // FeathersJS error response
67
+ errorMessage = err.response.message || err.message || errorMessage;
68
+ debug("FeathersJS error response:", err.response);
69
+ }
45
70
  setError(errorMessage);
46
71
  throw err;
47
72
  }
@@ -49,11 +74,11 @@ export function useTurnkeyAuth() {
49
74
  setIsLoading(false);
50
75
  setIsAuthenticating(false);
51
76
  }
52
- }, [user, setIsAuthenticating]);
77
+ }, [setIsAuthenticating]);
53
78
  /**
54
79
  * Step 2: Verify OTP and authenticate
55
- * - Verifies OTP with backend
56
- * - Gets Turnkey session JWT
80
+ * - Verifies OTP with backend via turnkey-jwt strategy (verify action)
81
+ * - Gets Turnkey session JWT from the verify response
57
82
  * - Authenticates with b3-api using "turnkey-jwt" strategy
58
83
  * - JWT automatically stored in cookies by SDK
59
84
  */
@@ -62,19 +87,22 @@ export function useTurnkeyAuth() {
62
87
  setError(null);
63
88
  setIsAuthenticating(true);
64
89
  try {
65
- debug(`Verifying OTP...`, { userId: user?.userId });
66
- // Step 1: Verify OTP and get Turnkey session JWT
67
- const { turnkeySessionJwt } = await app.service("turnkey-auth").verify({
90
+ debug(`Verifying OTP...`, { otpId });
91
+ // Step 1: Verify OTP with backend using turnkey-jwt strategy (verify action)
92
+ // This returns the Turnkey session JWT
93
+ const response = await app.authenticate({
94
+ strategy: "turnkey-jwt",
95
+ action: "verify",
68
96
  otpId,
69
97
  otpCode,
70
98
  });
71
- debug(`OTP verified! Authenticating with b3-api...`);
99
+ // The strategy returns the TurnkeyAuthVerifyResponse directly
100
+ const verifyResult = response;
101
+ const { turnkeySessionJwt } = verifyResult;
102
+ debug(`OTP verified! Got Turnkey session JWT. Authenticating with b3-api...`);
72
103
  // Step 2: Authenticate with b3-api using Turnkey JWT
73
- // The SDK will automatically store the b3-api JWT in cookies
74
- const authResult = await app.authenticate({
75
- strategy: "turnkey-jwt",
76
- accessToken: turnkeySessionJwt,
77
- });
104
+ // Use the unified useAuth hook for authentication with "turnkey-jwt" strategy
105
+ const authResult = await authenticate(turnkeySessionJwt, partnerId || "");
78
106
  debug(`Successfully authenticated with b3-api!`, authResult);
79
107
  // Update auth store to reflect authenticated state
80
108
  setIsAuthenticated(true);
@@ -94,7 +122,7 @@ export function useTurnkeyAuth() {
94
122
  setIsLoading(false);
95
123
  setIsAuthenticating(false);
96
124
  }
97
- }, [user, setIsAuthenticating, setIsAuthenticated]);
125
+ }, [partnerId, setIsAuthenticating, setIsAuthenticated, authenticate]);
98
126
  const clearError = useCallback(() => {
99
127
  setError(null);
100
128
  }, []);
@@ -3,6 +3,7 @@ export { useAccountAssets } from "./useAccountAssets";
3
3
  export { useAccountWallet } from "./useAccountWallet";
4
4
  export { useAddTWSessionKey } from "./useAddTWSessionKey";
5
5
  export { useAnalytics } from "./useAnalytics";
6
+ export { useAuth } from "./useAuth";
6
7
  export { useAuthentication } from "./useAuthentication";
7
8
  export { useB3BalanceFromAddresses } from "./useB3BalanceFromAddresses";
8
9
  export { useB3EnsName } from "./useB3EnsName";
@@ -0,0 +1,76 @@
1
+ import { Wallet } from "thirdweb/wallets";
2
+ import { preAuthenticate } from "thirdweb/wallets/in-app";
3
+ /**
4
+ * Unified authentication hook that uses Turnkey for authentication
5
+ * This replaces the previous Thirdweb-based authentication
6
+ *
7
+ * This hook provides 1:1 feature parity with useAuthentication.ts
8
+ */
9
+ export declare function useAuth(): {
10
+ authenticate: (turnkeySessionJwt: string, partnerId: string) => Promise<import("@feathersjs/authentication").AuthenticationResult>;
11
+ reAuthenticate: () => Promise<import("@feathersjs/authentication").AuthenticationResult>;
12
+ logout: (callback?: () => void) => Promise<void>;
13
+ isAuthenticated: boolean;
14
+ isReady: boolean;
15
+ isConnecting: boolean;
16
+ isConnected: boolean;
17
+ wallet: import("thirdweb/dist/types/wallets/in-app/core/wallet/types").EcosystemWallet;
18
+ preAuthenticate: typeof preAuthenticate;
19
+ connect: (_walleAutoConnectedWith: Wallet, allConnectedWallets: Wallet[]) => Promise<void>;
20
+ isAuthenticating: boolean;
21
+ onConnect: (_walleAutoConnectedWith: Wallet, allConnectedWallets: Wallet[]) => Promise<void>;
22
+ user: {
23
+ email?: string | undefined;
24
+ username?: string | undefined;
25
+ telNumber?: string | undefined;
26
+ ens?: string | undefined;
27
+ avatar?: string | undefined;
28
+ preferences?: {} | undefined;
29
+ referredBy?: string | {} | undefined;
30
+ sourceApp?: string | undefined;
31
+ referralCode?: string | undefined;
32
+ userGroups?: number[] | undefined;
33
+ isMigratedFromBSMNT?: boolean | undefined;
34
+ privyLinkedAccounts?: {
35
+ name?: string | undefined;
36
+ address?: string | undefined;
37
+ email?: string | undefined;
38
+ chain_type?: string | undefined;
39
+ lv?: number | undefined;
40
+ wallet_client_type?: string | undefined;
41
+ smart_wallet_type?: string | undefined;
42
+ subject?: string | undefined;
43
+ type: string;
44
+ }[] | undefined;
45
+ twProfiles?: {
46
+ type: string;
47
+ details: {
48
+ id?: string | undefined;
49
+ name?: string | undefined;
50
+ address?: string | undefined;
51
+ email?: string | undefined;
52
+ username?: string | undefined;
53
+ phone?: string | undefined;
54
+ fid?: string | undefined;
55
+ };
56
+ }[] | undefined;
57
+ turnkeySubOrgs?: {
58
+ hasDelegatedUser?: boolean | undefined;
59
+ subOrgId: string;
60
+ accounts: Record<string, any>[];
61
+ }[] | undefined;
62
+ _id: string | {};
63
+ userId: string;
64
+ smartAccountAddress: string;
65
+ createdAt: number;
66
+ updatedAt: number;
67
+ partnerIds: {
68
+ privyId?: string | undefined;
69
+ thirdwebId?: string | undefined;
70
+ turnkeyId?: string | undefined;
71
+ turnkeyOtpId?: string | undefined;
72
+ };
73
+ } | undefined;
74
+ refetchUser: () => Promise<import("@feathersjs/authentication").AuthenticationResult>;
75
+ setUser: (newUser?: import("@b3dotfun/b3-api").Users) => void;
76
+ };
@@ -1,4 +1,7 @@
1
1
  import { Wallet } from "thirdweb/wallets";
2
+ /**
3
+ * @deprecated Use useAuth() with Turnkey authentication instead
4
+ */
2
5
  export declare function useTWAuth(): {
3
6
  authenticate: (wallet: Wallet, partnerId: string) => Promise<import("@feathersjs/authentication").AuthenticationResult>;
4
7
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b3dotfun/sdk",
3
- "version": "0.0.88-alpha.2",
3
+ "version": "0.0.88-alpha.4",
4
4
  "source": "src/index.ts",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "react-native": "./dist/cjs/index.native.js",
@@ -19,6 +19,9 @@ export function RelayKitProviderWrapper({
19
19
  fetchChains();
20
20
  }, []);
21
21
 
22
+ const isTurnkeyPrimary = process.env.NEXT_PUBLIC_TURNKEY_PRIMARY === "true";
23
+ const appName = isTurnkeyPrimary ? "Smart Wallet" : "AnySpend";
24
+
22
25
  return (
23
26
  <RelayKitProvider
24
27
  options={{
@@ -30,7 +33,7 @@ export function RelayKitProviderWrapper({
30
33
  },
31
34
  chains: relayChains,
32
35
  privateChainIds: undefined,
33
- appName: "AnySpend",
36
+ appName,
34
37
  useGasFeeEstimations: true,
35
38
  }}
36
39
  >
@@ -11,6 +11,7 @@ import { debugB3React } from "@b3dotfun/sdk/shared/utils/debug";
11
11
  import { useCallback, useEffect, useState } from "react";
12
12
  import { useActiveAccount } from "thirdweb/react";
13
13
  import { Account } from "thirdweb/wallets";
14
+ import { TurnkeyAuthModal } from "../TurnkeyAuthModal";
14
15
  import { SignInWithB3Privy } from "./SignInWithB3Privy";
15
16
  import { LoginStep, LoginStepContainer } from "./steps/LoginStep";
16
17
  import { LoginStepCustom } from "./steps/LoginStepCustom";
@@ -43,7 +44,9 @@ export function SignInWithB3Flow({
43
44
  const account = useActiveAccount();
44
45
  const isAuthenticating = useAuthStore(state => state.isAuthenticating);
45
46
  const isAuthenticated = useAuthStore(state => state.isAuthenticated);
47
+ const setIsAuthenticated = useAuthStore(state => state.setIsAuthenticated);
46
48
  const isConnected = useAuthStore(state => state.isConnected);
49
+ const setIsConnected = useAuthStore(state => state.setIsConnected);
47
50
  const setJustCompletedLogin = useAuthStore(state => state.setJustCompletedLogin);
48
51
  const [refetchCount, setRefetchCount] = useState(0);
49
52
  const [refetchError, setRefetchError] = useState<string | null>(null);
@@ -85,7 +88,9 @@ export function SignInWithB3Flow({
85
88
  refetchSigners();
86
89
  setRefetchQueued(false);
87
90
  }, backoffDelay);
88
- }, [refetchCount, refetchSigners, onError, setRefetchQueued, refetchQueued]);
91
+ // State setters are stable and don't need to be in dependencies
92
+ // eslint-disable-next-line react-hooks/exhaustive-deps
93
+ }, [refetchCount, refetchSigners, onError, refetchQueued]);
89
94
 
90
95
  // Extract the completion flow logic to be reused
91
96
  const handlePostTurnkeyFlow = useCallback(() => {
@@ -157,6 +162,11 @@ export function SignInWithB3Flow({
157
162
  await refetchUser();
158
163
  debug("User refetched successfully");
159
164
 
165
+ // Set authentication and connection state so UI updates properly
166
+ setIsAuthenticated(true);
167
+ setIsConnected(true);
168
+ setJustCompletedLogin(true);
169
+
160
170
  // After user data is refreshed, close Turnkey modal and go back to sign-in flow
161
171
  debug("Switching back to signInWithB3 modal");
162
172
  setB3ModalContentType({
@@ -176,7 +186,6 @@ export function SignInWithB3Flow({
176
186
  },
177
187
  [
178
188
  refetchUser,
179
- setB3ModalContentType,
180
189
  strategies,
181
190
  onLoginSuccess,
182
191
  onSessionKeySuccess,
@@ -187,90 +196,102 @@ export function SignInWithB3Flow({
187
196
  closeAfterLogin,
188
197
  source,
189
198
  signersEnabled,
199
+ setB3ModalContentType,
200
+ setIsAuthenticated,
201
+ setIsConnected,
202
+ setJustCompletedLogin,
190
203
  ],
191
204
  );
192
205
 
193
206
  // Handle post-login flow after signers are loaded
194
- useEffect(() => {
195
- debug("@@SignInWithB3Flow:useEffect", {
196
- isConnected,
197
- isAuthenticating,
198
- isFetchingSigners,
199
- closeAfterLogin,
200
- isOpen,
201
- source,
202
- });
207
+ useEffect(
208
+ () => {
209
+ debug("@@SignInWithB3Flow:useEffect", {
210
+ isConnected,
211
+ isAuthenticating,
212
+ isFetchingSigners,
213
+ closeAfterLogin,
214
+ isOpen,
215
+ source,
216
+ });
203
217
 
204
- if (isConnected && isAuthenticated && user) {
205
- // Mark that login just completed BEFORE opening manage account or closing modal
206
- // This allows Turnkey modal to show (if enableTurnkey is true)
207
- if (closeAfterLogin) {
208
- setJustCompletedLogin(true);
209
- }
218
+ if (isConnected && isAuthenticated && user) {
219
+ // Mark that login just completed BEFORE opening manage account or closing modal
220
+ // This allows Turnkey modal to show (if enableTurnkey is true)
221
+ if (closeAfterLogin) {
222
+ setJustCompletedLogin(true);
223
+ }
210
224
 
211
- // Check if we should show Turnkey login form
212
- // Show if enableTurnkey is true AND user just logged in AND hasn't completed Turnkey auth in this session
213
- // For new users (!turnkeyId): Show email form
214
- // For returning users (turnkeyId && turnkeyEmail): Auto-skip to OTP
215
- // Also check that we're not already showing the Turnkey modal
216
- const hasTurnkeyId = user?.partnerIds?.turnkeyId;
217
- const hasTurnkeyEmail = !!user?.email;
218
- const isTurnkeyModalCurrentlyOpen = contentType?.type === "turnkeyAuth";
219
- const shouldShowTurnkeyModal =
220
- enableTurnkey &&
221
- user &&
222
- !turnkeyAuthCompleted &&
223
- !isTurnkeyModalCurrentlyOpen &&
224
- (!hasTurnkeyId || (hasTurnkeyId && hasTurnkeyEmail));
225
+ // Check if we should show Turnkey login form as SECONDARY option (after wallet connection)
226
+ // This only applies when:
227
+ // - enableTurnkey={true} is set on B3Provider
228
+ // - NEXT_PUBLIC_TURNKEY_PRIMARY is NOT set to true (otherwise Turnkey shows as primary)
229
+ // - User just logged in AND hasn't completed Turnkey auth in this session
230
+ // For new users (!turnkeyId): Show email form
231
+ // For returning users (turnkeyId && turnkeyEmail): Auto-skip to OTP
232
+ // Also check that we're not already showing the Turnkey modal
233
+ const hasTurnkeyId = user?.partnerIds?.turnkeyId;
234
+ const hasTurnkeyEmail = !!user?.email;
235
+ const isTurnkeyModalCurrentlyOpen = contentType?.type === "turnkeyAuth";
236
+ const isTurnkeyPrimary = process.env.NEXT_PUBLIC_TURNKEY_PRIMARY === "true";
237
+ const shouldShowTurnkeyModal =
238
+ enableTurnkey &&
239
+ !isTurnkeyPrimary &&
240
+ user &&
241
+ !turnkeyAuthCompleted &&
242
+ !isTurnkeyModalCurrentlyOpen &&
243
+ (!hasTurnkeyId || (hasTurnkeyId && hasTurnkeyEmail));
225
244
 
226
- if (shouldShowTurnkeyModal) {
227
- // Extract email from user object - check partnerIds.turnkeyEmail first, then twProfiles, then user.email
228
- const email = user?.email || user?.twProfiles?.find((profile: any) => profile.details?.email)?.details?.email;
245
+ if (shouldShowTurnkeyModal) {
246
+ // Extract email from user object - check partnerIds.turnkeyEmail first, then twProfiles, then user.email
247
+ const email = user?.email || user?.twProfiles?.find((profile: any) => profile.details?.email)?.details?.email;
229
248
 
230
- // Open Turnkey modal through the modal store
231
- setB3ModalContentType({
232
- type: "turnkeyAuth",
233
- onSuccess: handleTurnkeySuccess,
234
- onClose: () => {
235
- // After closing Turnkey modal, continue with the rest of the flow
236
- setTurnkeyAuthCompleted(true);
237
- debug("Turnkey modal closed, running post-Turnkey flow");
238
- handlePostTurnkeyFlow();
239
- },
240
- initialEmail: email,
241
- skipToOtp: !!(hasTurnkeyId && hasTurnkeyEmail),
242
- closable: false, // Turnkey modal cannot be closed until auth is complete
243
- });
244
- return;
245
- }
249
+ // Open Turnkey modal through the modal store
250
+ setB3ModalContentType({
251
+ type: "turnkeyAuth",
252
+ onSuccess: handleTurnkeySuccess,
253
+ onClose: () => {
254
+ // After closing Turnkey modal, continue with the rest of the flow
255
+ setTurnkeyAuthCompleted(true);
256
+ debug("Turnkey modal closed, running post-Turnkey flow");
257
+ handlePostTurnkeyFlow();
258
+ },
259
+ initialEmail: email,
260
+ skipToOtp: !!(hasTurnkeyId && hasTurnkeyEmail),
261
+ closable: false, // Turnkey modal cannot be closed until auth is complete
262
+ });
263
+ return;
264
+ }
246
265
 
247
- // Normal flow continues after Turnkey auth is complete (or if not needed)
248
- handlePostTurnkeyFlow();
249
- }
250
- }, [
251
- signers,
252
- isFetchingSigners,
253
- partnerId,
254
- handleRefetchSigners,
255
- source,
256
- closeAfterLogin,
257
- setB3ModalContentType,
258
- chain,
259
- onSessionKeySuccess,
260
- setB3ModalOpen,
261
- signersEnabled,
262
- isConnected,
263
- isAuthenticating,
264
- isAuthenticated,
265
- isOpen,
266
- setJustCompletedLogin,
267
- user,
268
- enableTurnkey,
269
- turnkeyAuthCompleted,
270
- handleTurnkeySuccess,
271
- contentType,
272
- handlePostTurnkeyFlow,
273
- ]);
266
+ // Normal flow continues after Turnkey auth is complete (or if not needed)
267
+ handlePostTurnkeyFlow();
268
+ }
269
+ },
270
+ // eslint-disable-next-line react-hooks/exhaustive-deps
271
+ [
272
+ signers,
273
+ isFetchingSigners,
274
+ partnerId,
275
+ handleRefetchSigners,
276
+ source,
277
+ closeAfterLogin,
278
+ setB3ModalContentType,
279
+ chain,
280
+ onSessionKeySuccess,
281
+ setB3ModalOpen,
282
+ signersEnabled,
283
+ isConnected,
284
+ isAuthenticating,
285
+ isAuthenticated,
286
+ isOpen,
287
+ user,
288
+ enableTurnkey,
289
+ turnkeyAuthCompleted,
290
+ // handleTurnkeySuccess, // This is causing infinite loops
291
+ contentType,
292
+ handlePostTurnkeyFlow,
293
+ ],
294
+ );
274
295
 
275
296
  debug("render", {
276
297
  step,
@@ -286,7 +307,9 @@ export function SignInWithB3Flow({
286
307
  if (closeAfterLogin && sessionKeyAdded) {
287
308
  setB3ModalOpen(false);
288
309
  }
289
- }, [closeAfterLogin, sessionKeyAdded, setB3ModalOpen]);
310
+ // setB3ModalOpen is stable
311
+ // eslint-disable-next-line react-hooks/exhaustive-deps
312
+ }, [closeAfterLogin, sessionKeyAdded]);
290
313
 
291
314
  const onSessionKeySuccessEnhanced = useCallback(() => {
292
315
  onSessionKeySuccess?.();
@@ -343,32 +366,77 @@ export function SignInWithB3Flow({
343
366
  <div className="p-4 text-center text-red-500">{refetchError}</div>
344
367
  </LoginStepContainer>
345
368
  );
346
- } else if (isAuthenticating || (isFetchingSigners && step === "login") || source === "requestPermissions") {
347
- content = (
348
- <LoginStepContainer partnerId={partnerId}>
349
- <div className="my-8 flex min-h-[350px] items-center justify-center">
350
- <Loading variant="white" size="lg" />
351
- </div>
352
- </LoginStepContainer>
353
- );
354
369
  } else if (step === "login") {
355
- // Custom strategy
356
- if (strategies?.[0] === "privy") {
357
- content = <SignInWithB3Privy onSuccess={handleLoginSuccess} chain={chain} />;
358
- } else if (strategies) {
359
- // Strategies are explicitly provided
370
+ // PRIORITY: If NEXT_PUBLIC_TURNKEY_PRIMARY is true, show Turnkey modal FIRST as the primary authentication option
371
+ // Setting NEXT_PUBLIC_TURNKEY_PRIMARY="true" implicitly enables Turnkey
372
+ const isTurnkeyPrimary = process.env.NEXT_PUBLIC_TURNKEY_PRIMARY === "true";
373
+ const shouldShowTurnkeyFirst = isTurnkeyPrimary && !turnkeyAuthCompleted;
374
+
375
+ if (shouldShowTurnkeyFirst) {
376
+ // Don't show loading spinner for Turnkey - let the modal handle its own loading state
377
+ // This prevents the infinite loop where isAuthenticating causes the modal to be replaced
378
+ debug("Showing Turnkey as primary authentication option", {
379
+ isTurnkeyPrimary,
380
+ turnkeyAuthCompleted,
381
+ isAuthenticated,
382
+ });
383
+ // Show Turnkey authentication as primary option
360
384
  content = (
361
- <LoginStepCustom
362
- strategies={strategies}
363
- chain={chain}
364
- onSuccess={handleLoginSuccess}
365
- onError={onError}
366
- automaticallySetFirstEoa={!!automaticallySetFirstEoa}
367
- />
385
+ <LoginStepContainer partnerId={partnerId}>
386
+ <TurnkeyAuthModal
387
+ onSuccess={async (authenticatedUser: any) => {
388
+ debug("Turnkey authentication successful in primary flow", { authenticatedUser });
389
+ setTurnkeyAuthCompleted(true);
390
+ // After Turnkey auth, refetch user to get the full user object
391
+ await refetchUser();
392
+ // User is now authenticated via Turnkey
393
+ // Set both isAuthenticated and isConnected to true so UI updates properly
394
+ // Wallet connection is optional and can happen later for signing transactions
395
+ setIsAuthenticated(true);
396
+ setIsConnected(true);
397
+ setJustCompletedLogin(true);
398
+ // Call the login success callback
399
+ onLoginSuccess?.({} as Account);
400
+ }}
401
+ onClose={() => {
402
+ // If user closes Turnkey modal, they can still use wallet connection as fallback
403
+ setTurnkeyAuthCompleted(true);
404
+ }}
405
+ initialEmail=""
406
+ skipToOtp={false}
407
+ />
408
+ </LoginStepContainer>
368
409
  );
369
410
  } else {
370
- // Default to handle all strategies we support
371
- content = <LoginStep chain={chain} onSuccess={handleLoginSuccess} onError={onError} />;
411
+ // Show loading spinner only if not in Turnkey flow
412
+ if (isAuthenticating || (isFetchingSigners && step === "login") || source === "requestPermissions") {
413
+ content = (
414
+ <LoginStepContainer partnerId={partnerId}>
415
+ <div className="my-8 flex min-h-[350px] items-center justify-center">
416
+ <Loading variant="white" size="lg" />
417
+ </div>
418
+ </LoginStepContainer>
419
+ );
420
+ } else {
421
+ // Custom strategy
422
+ if (strategies?.[0] === "privy") {
423
+ content = <SignInWithB3Privy onSuccess={handleLoginSuccess} chain={chain} />;
424
+ } else if (strategies) {
425
+ // Strategies are explicitly provided
426
+ content = (
427
+ <LoginStepCustom
428
+ strategies={strategies}
429
+ chain={chain}
430
+ onSuccess={handleLoginSuccess}
431
+ onError={onError}
432
+ automaticallySetFirstEoa={!!automaticallySetFirstEoa}
433
+ />
434
+ );
435
+ } else {
436
+ // Default to handle all strategies we support
437
+ content = <LoginStep chain={chain} onSuccess={handleLoginSuccess} onError={onError} />;
438
+ }
439
+ }
372
440
  }
373
441
  }
374
442
 
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useRef } from "react";
1
+ import React, { useEffect, useRef, useState } from "react";
2
2
  import { useTurnkeyAuth } from "../hooks/useTurnkeyAuth";
3
3
 
4
4
  type ModalStep = "email" | "otp" | "success";
@@ -94,17 +94,20 @@ export function TurnkeyAuthModal({ onClose, onSuccess, initialEmail = "", skipTo
94
94
  }
95
95
  };
96
96
 
97
+ const isTurnkeyPrimary = process.env.NEXT_PUBLIC_TURNKEY_PRIMARY === "true";
98
+ const walletBrand = isTurnkeyPrimary ? "Smart Wallet" : "AnySpend Wallet";
99
+
97
100
  return (
98
101
  <div className="font-neue-montreal p-8">
99
102
  {/* Email Step */}
100
103
  {step === "email" && (
101
104
  <>
102
105
  <h2 className="mb-6 text-center text-2xl font-bold text-gray-900 dark:text-white">
103
- Setup your AnySpend Wallet
106
+ Setup your {walletBrand}
104
107
  </h2>
105
108
  <div className="mb-6 space-y-3 text-center text-sm text-gray-600 dark:text-gray-400">
106
109
  <p>
107
- AnySpend uses a secure,
110
+ {isTurnkeyPrimary ? "We use a secure," : "AnySpend uses a secure,"}
108
111
  <br />
109
112
  embedded wallet to fund your workflows.
110
113
  </p>
@@ -158,7 +161,7 @@ export function TurnkeyAuthModal({ onClose, onSuccess, initialEmail = "", skipTo
158
161
  <h2 className="mb-4 text-center text-2xl font-bold text-gray-900 dark:text-white">2FA Security</h2>
159
162
  <div className="mb-6 space-y-3 text-center text-sm text-gray-600 dark:text-gray-400">
160
163
  <p>
161
- AnySpend uses a secure,
164
+ {isTurnkeyPrimary ? "We use a secure," : "AnySpend uses a secure,"}
162
165
  <br />
163
166
  embedded wallet to fund your workflows.
164
167
  <br />
@@ -3,6 +3,7 @@ export { useAccountAssets } from "./useAccountAssets";
3
3
  export { useAccountWallet } from "./useAccountWallet";
4
4
  export { useAddTWSessionKey } from "./useAddTWSessionKey";
5
5
  export { useAnalytics } from "./useAnalytics";
6
+ export { useAuth } from "./useAuth";
6
7
  export { useAuthentication } from "./useAuthentication";
7
8
  export { useB3BalanceFromAddresses } from "./useB3BalanceFromAddresses";
8
9
  export { useB3EnsName } from "./useB3EnsName";