@crossmint/client-sdk-react-ui 1.4.1 → 1.6.0

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.
@@ -44,6 +44,10 @@ vi.mock("@crossmint/client-sdk-auth-core/client", async () => {
44
44
  secret: "new-mock-refresh-token",
45
45
  expiresAt: new Date(Date.now() + 1000 * 60 * 60).toISOString(),
46
46
  },
47
+ user: {
48
+ id: "123",
49
+ email: "test@test.com",
50
+ },
47
51
  }),
48
52
  })),
49
53
  };
@@ -2,7 +2,7 @@ import { REFRESH_TOKEN_PREFIX, SESSION_PREFIX, deleteCookie, getCookie, setCooki
2
2
  import { type ReactNode, createContext, useEffect, useState } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
 
5
- import { CrossmintAuthService } from "@crossmint/client-sdk-auth-core/client";
5
+ import { CrossmintAuthService, type SDKExternalUser } from "@crossmint/client-sdk-auth-core/client";
6
6
  import type { EVMSmartWalletChain } from "@crossmint/client-sdk-smart-wallet";
7
7
  import { type UIConfig, validateApiKeyAndGetCrossmintBaseUrl } from "@crossmint/common-sdk-base";
8
8
 
@@ -14,6 +14,7 @@ export type CrossmintAuthWalletConfig = {
14
14
  defaultChain: EVMSmartWalletChain;
15
15
  createOnLogin: "all-users" | "off";
16
16
  type: "evm-smart-wallet";
17
+ showWalletModals?: boolean;
17
18
  };
18
19
 
19
20
  export type CrossmintAuthProviderProps = {
@@ -29,16 +30,20 @@ type AuthContextType = {
29
30
  logout: () => void;
30
31
  jwt?: string;
31
32
  refreshToken?: string;
33
+ user?: SDKExternalUser;
32
34
  status: AuthStatus;
35
+ getUser: () => void;
33
36
  };
34
37
 
35
38
  export const AuthContext = createContext<AuthContextType>({
36
39
  login: () => {},
37
40
  logout: () => {},
38
41
  status: "logged-out",
42
+ getUser: () => {},
39
43
  });
40
44
 
41
45
  export function CrossmintAuthProvider({ embeddedWallets, children, appearance }: CrossmintAuthProviderProps) {
46
+ const [user, setUser] = useState<SDKExternalUser | undefined>(undefined);
42
47
  const { crossmint, setJwt, setRefreshToken } = useCrossmint(
43
48
  "CrossmintAuthProvider must be used within CrossmintProvider"
44
49
  );
@@ -51,6 +56,7 @@ export function CrossmintAuthProvider({ embeddedWallets, children, appearance }:
51
56
  setCookie(REFRESH_TOKEN_PREFIX, authMaterial.refreshToken.secret, authMaterial.refreshToken.expiresAt);
52
57
  setJwt(authMaterial.jwtToken);
53
58
  setRefreshToken(authMaterial.refreshToken.secret);
59
+ setUser(authMaterial.user);
54
60
  };
55
61
 
56
62
  const logout = () => {
@@ -58,19 +64,11 @@ export function CrossmintAuthProvider({ embeddedWallets, children, appearance }:
58
64
  deleteCookie(REFRESH_TOKEN_PREFIX);
59
65
  setJwt(undefined);
60
66
  setRefreshToken(undefined);
67
+ setUser(undefined);
61
68
  };
62
69
 
63
70
  useRefreshToken({ crossmintAuthService, setAuthMaterial, logout });
64
71
 
65
- const login = () => {
66
- if (crossmint.jwt != null) {
67
- console.log("User already logged in");
68
- return;
69
- }
70
-
71
- setModalOpen(true);
72
- };
73
-
74
72
  useEffect(() => {
75
73
  if (crossmint.jwt == null) {
76
74
  const jwt = getCookie(SESSION_PREFIX);
@@ -86,6 +84,15 @@ export function CrossmintAuthProvider({ embeddedWallets, children, appearance }:
86
84
  setModalOpen(false);
87
85
  }, [crossmint.jwt]);
88
86
 
87
+ const login = () => {
88
+ if (crossmint.jwt != null) {
89
+ console.log("User already logged in");
90
+ return;
91
+ }
92
+
93
+ setModalOpen(true);
94
+ };
95
+
89
96
  const getAuthStatus = (): AuthStatus => {
90
97
  if (crossmint.jwt != null) {
91
98
  return "logged-in";
@@ -96,6 +103,22 @@ export function CrossmintAuthProvider({ embeddedWallets, children, appearance }:
96
103
  return "logged-out";
97
104
  };
98
105
 
106
+ const fetchAuthMaterial = async (refreshToken: string): Promise<AuthMaterial> => {
107
+ const authMaterial = await crossmintAuthService.refreshAuthMaterial(refreshToken);
108
+ setAuthMaterial(authMaterial);
109
+ return authMaterial;
110
+ };
111
+
112
+ const getUser = async () => {
113
+ if (crossmint.jwt == null) {
114
+ console.log("User not logged in");
115
+ return;
116
+ }
117
+
118
+ const user = await crossmintAuthService.getUserFromClient(crossmint.jwt);
119
+ setUser(user);
120
+ };
121
+
99
122
  return (
100
123
  <AuthContext.Provider
101
124
  value={{
@@ -103,10 +126,16 @@ export function CrossmintAuthProvider({ embeddedWallets, children, appearance }:
103
126
  logout,
104
127
  jwt: crossmint.jwt,
105
128
  refreshToken: crossmint.refreshToken,
129
+ user,
106
130
  status: getAuthStatus(),
131
+ getUser,
107
132
  }}
108
133
  >
109
- <CrossmintWalletProvider defaultChain={embeddedWallets.defaultChain}>
134
+ <CrossmintWalletProvider
135
+ defaultChain={embeddedWallets.defaultChain}
136
+ showWalletModals={embeddedWallets.showWalletModals}
137
+ appearance={appearance}
138
+ >
110
139
  <WalletManager embeddedWallets={embeddedWallets} accessToken={crossmint.jwt}>
111
140
  {children}
112
141
  </WalletManager>
@@ -115,7 +144,7 @@ export function CrossmintAuthProvider({ embeddedWallets, children, appearance }:
115
144
  <AuthModal
116
145
  baseUrl={crossmintBaseUrl}
117
146
  setModalOpen={setModalOpen}
118
- setAuthMaterial={setAuthMaterial}
147
+ fetchAuthMaterial={fetchAuthMaterial}
119
148
  apiKey={crossmint.apiKey}
120
149
  appearance={appearance}
121
150
  />,
@@ -1,4 +1,5 @@
1
1
  import { type ReactNode, createContext, useMemo, useState } from "react";
2
+ import { createPortal } from "react-dom";
2
3
 
3
4
  import {
4
5
  type EVMSmartWallet,
@@ -6,11 +7,30 @@ import {
6
7
  SmartWalletError,
7
8
  SmartWalletSDK,
8
9
  type WalletParams,
10
+ type PasskeySigner,
9
11
  } from "@crossmint/client-sdk-smart-wallet";
10
12
 
11
13
  import { useCrossmint } from "../hooks";
14
+ import type { UIConfig } from "@crossmint/common-sdk-base";
15
+ import { PasskeyPrompt } from "@/components/auth/PasskeyPrompt";
12
16
 
13
17
  type WalletStatus = "not-loaded" | "in-progress" | "loaded" | "loading-error";
18
+
19
+ type ValidPasskeyPromptType =
20
+ | "create-wallet"
21
+ | "transaction"
22
+ | "not-supported"
23
+ | "create-wallet-error"
24
+ | "transaction-error";
25
+ type PasskeyPromptState =
26
+ | {
27
+ type: ValidPasskeyPromptType;
28
+ open: true;
29
+ primaryActionOnClick: () => void;
30
+ secondaryActionOnClick?: () => void;
31
+ }
32
+ | { open: false };
33
+
14
34
  type ValidWalletState =
15
35
  | { status: "not-loaded" | "in-progress" }
16
36
  | { status: "loaded"; wallet: EVMSmartWallet }
@@ -20,13 +40,15 @@ type WalletContext = {
20
40
  status: WalletStatus;
21
41
  wallet?: EVMSmartWallet;
22
42
  error?: SmartWalletError;
23
- getOrCreateWallet: (config?: WalletConfig) => { startedCreation: boolean; reason?: string };
43
+ getOrCreateWallet: (
44
+ config?: Pick<WalletConfig, "signer" | "type">
45
+ ) => Promise<{ startedCreation: boolean; reason?: string }>;
24
46
  clearWallet: () => void;
25
47
  };
26
48
 
27
49
  export const WalletContext = createContext<WalletContext>({
28
50
  status: "not-loaded",
29
- getOrCreateWallet: () => ({ startedCreation: false }),
51
+ getOrCreateWallet: () => Promise.resolve({ startedCreation: false }),
30
52
  clearWallet: () => {},
31
53
  });
32
54
 
@@ -35,17 +57,24 @@ export type WalletConfig = WalletParams & { type: "evm-smart-wallet" };
35
57
  export function CrossmintWalletProvider({
36
58
  children,
37
59
  defaultChain,
60
+ showWalletModals = true, // enabled by default
61
+ appearance,
38
62
  }: {
39
63
  children: ReactNode;
40
64
  defaultChain: EVMSmartWalletChain;
65
+ showWalletModals?: boolean;
66
+ appearance?: UIConfig;
41
67
  }) {
42
68
  const { crossmint } = useCrossmint("CrossmintWalletProvider must be used within CrossmintProvider");
43
69
  const smartWalletSDK = useMemo(() => SmartWalletSDK.init({ clientApiKey: crossmint.apiKey }), [crossmint.apiKey]);
44
70
 
45
- const [state, setState] = useState<ValidWalletState>({ status: "not-loaded" });
71
+ const [walletState, setWalletState] = useState<ValidWalletState>({ status: "not-loaded" });
72
+ const [passkeyPromptState, setPasskeyPromptState] = useState<PasskeyPromptState>({ open: false });
46
73
 
47
- const getOrCreateWallet = (config: WalletConfig = { type: "evm-smart-wallet", signer: { type: "PASSKEY" } }) => {
48
- if (state.status == "in-progress") {
74
+ const getOrCreateWallet = async (
75
+ config: WalletConfig = { type: "evm-smart-wallet", signer: { type: "PASSKEY" } }
76
+ ) => {
77
+ if (walletState.status == "in-progress") {
49
78
  console.log("Wallet already loading");
50
79
  return { startedCreation: false, reason: "Wallet is already loading." };
51
80
  }
@@ -54,31 +83,64 @@ export function CrossmintWalletProvider({
54
83
  return { startedCreation: false, reason: `Jwt not set in "CrossmintProvider".` };
55
84
  }
56
85
 
57
- const internalCall = async () => {
58
- try {
59
- setState({ status: "in-progress" });
60
- const wallet = await smartWalletSDK.getOrCreateWallet(
61
- { jwt: crossmint.jwt as string },
62
- defaultChain,
63
- config
64
- );
65
- setState({ status: "loaded", wallet });
66
- } catch (error: unknown) {
67
- console.error("There was an error creating a wallet ", error);
68
- setState(deriveErrorState(error));
69
- }
70
- };
71
-
72
- internalCall();
86
+ try {
87
+ setWalletState({ status: "in-progress" });
88
+ const wallet = await smartWalletSDK.getOrCreateWallet(
89
+ { jwt: crossmint.jwt as string },
90
+ defaultChain,
91
+ enhanceConfigWithPasskeyPrompts(config)
92
+ );
93
+ setWalletState({ status: "loaded", wallet });
94
+ } catch (error: unknown) {
95
+ console.error("There was an error creating a wallet ", error);
96
+ setWalletState(deriveErrorState(error));
97
+ }
73
98
  return { startedCreation: true };
74
99
  };
75
100
 
101
+ const enhanceConfigWithPasskeyPrompts = (config: WalletConfig) => {
102
+ if (showWalletModals && (config.signer as PasskeySigner).type === "PASSKEY") {
103
+ return {
104
+ ...config,
105
+ signer: {
106
+ ...config.signer,
107
+ onPrePasskeyRegistration: createPasskeyPrompt("create-wallet"),
108
+ onPasskeyRegistrationError: createPasskeyPrompt("create-wallet-error"),
109
+ onFirstTimePasskeySigning: createPasskeyPrompt("transaction"),
110
+ onFirstTimePasskeySigningError: createPasskeyPrompt("transaction-error"),
111
+ },
112
+ };
113
+ }
114
+ return config;
115
+ };
116
+
117
+ const createPasskeyPrompt = (type: ValidPasskeyPromptType) => () =>
118
+ new Promise<void>((resolve) => {
119
+ setPasskeyPromptState({
120
+ type,
121
+ open: true,
122
+ primaryActionOnClick: () => {
123
+ setPasskeyPromptState({ open: false });
124
+ resolve();
125
+ },
126
+ secondaryActionOnClick: () => {
127
+ setPasskeyPromptState({ open: false });
128
+ resolve();
129
+ },
130
+ });
131
+ });
132
+
76
133
  const clearWallet = () => {
77
- setState({ status: "not-loaded" });
134
+ setWalletState({ status: "not-loaded" });
78
135
  };
79
136
 
80
137
  return (
81
- <WalletContext.Provider value={{ ...state, getOrCreateWallet, clearWallet }}>{children}</WalletContext.Provider>
138
+ <WalletContext.Provider value={{ ...walletState, getOrCreateWallet, clearWallet }}>
139
+ {children}
140
+ {passkeyPromptState.open
141
+ ? createPortal(<PasskeyPrompt state={passkeyPromptState} appearance={appearance} />, document.body)
142
+ : null}
143
+ </WalletContext.Provider>
82
144
  );
83
145
  }
84
146