@crossmint/client-sdk-react-ui 1.3.23 → 1.4.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.
Files changed (33) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +20 -2
  4. package/dist/index.d.ts +20 -2
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/package.json +7 -10
  8. package/src/components/CrossmintNFTCollectionView.tsx +1 -1
  9. package/src/components/CrossmintNFTDetail.test.tsx +1 -1
  10. package/src/components/CrossmintNFTDetail.tsx +1 -1
  11. package/src/components/auth/AuthModal.tsx +15 -11
  12. package/src/components/embed/EmbeddedCheckoutIFrame.tsx +2 -2
  13. package/src/components/embed/crypto/CryptoEmbeddedCheckout.tsx +1 -1
  14. package/src/components/embed/crypto/CryptoEmbeddedCheckoutIFrame.tsx +5 -5
  15. package/src/components/embed/fiat/FiatEmbeddedCheckout.tsx +1 -1
  16. package/src/components/embed/fiat/FiatEmbeddedCheckoutIFrame.tsx +1 -1
  17. package/src/components/embed/index.tsx +1 -1
  18. package/src/components/hosted/CrossmintPayButton.tsx +3 -3
  19. package/src/components/hosted/styles.ts +4 -5
  20. package/src/consts/version.ts +3 -1
  21. package/src/hooks/index.ts +1 -0
  22. package/src/hooks/useCrossmint.test.tsx +44 -8
  23. package/src/hooks/useCrossmint.tsx +14 -3
  24. package/src/hooks/useDeepEffect.ts +1 -1
  25. package/src/hooks/useRefreshToken.test.ts +142 -0
  26. package/src/hooks/useRefreshToken.ts +62 -0
  27. package/src/index.ts +0 -1
  28. package/src/providers/CrossmintAuthProvider.test.tsx +105 -15
  29. package/src/providers/CrossmintAuthProvider.tsx +31 -26
  30. package/src/providers/CrossmintWalletProvider.test.tsx +3 -3
  31. package/src/providers/CrossmintWalletProvider.tsx +4 -4
  32. package/src/utils/authCookies.test.ts +41 -0
  33. package/src/utils/authCookies.ts +16 -0
@@ -1,17 +1,17 @@
1
+ import { deleteCookie, REFRESH_TOKEN_PREFIX, SESSION_PREFIX } from "@/utils/authCookies";
1
2
  import { fireEvent, render } from "@testing-library/react";
2
- import { ReactNode } from "react";
3
+ import { type ReactNode, act } from "react";
3
4
  import { beforeEach, describe, expect, vi } from "vitest";
4
5
  import { mock } from "vitest-mock-extended";
5
6
 
6
- import { EVMSmartWallet, SmartWalletSDK } from "@crossmint/client-sdk-smart-wallet";
7
+ import { CrossmintAuthService, getJWTExpiration } from "@crossmint/client-sdk-auth-core/client";
8
+ import { type EVMSmartWallet, SmartWalletSDK } from "@crossmint/client-sdk-smart-wallet";
7
9
  import { createCrossmint } from "@crossmint/common-sdk-base";
8
10
 
9
11
  import { useAuth, useWallet } from "../hooks";
10
12
  import { CrossmintProvider, useCrossmint } from "../hooks/useCrossmint";
11
13
  import { MOCK_API_KEY, waitForSettledState } from "../testUtils";
12
- import { CrossmintAuthProvider, CrossmintAuthWalletConfig } from "./CrossmintAuthProvider";
13
-
14
- const SESSION_PREFIX = "crossmint-session";
14
+ import { CrossmintAuthProvider, type CrossmintAuthWalletConfig } from "./CrossmintAuthProvider";
15
15
 
16
16
  vi.mock("@crossmint/client-sdk-smart-wallet", async () => {
17
17
  const actual = await vi.importActual("@crossmint/client-sdk-smart-wallet");
@@ -32,6 +32,23 @@ vi.mock("@crossmint/common-sdk-base", async () => {
32
32
  };
33
33
  });
34
34
 
35
+ vi.mock("@crossmint/client-sdk-auth-core/client", async () => {
36
+ const actual = await vi.importActual("@crossmint/client-sdk-auth-core/client");
37
+ return {
38
+ ...actual,
39
+ getJWTExpiration: vi.fn(),
40
+ CrossmintAuthService: vi.fn().mockImplementation(() => ({
41
+ refreshAuthMaterial: vi.fn().mockResolvedValue({
42
+ jwtToken: "new-mock-jwt",
43
+ refreshToken: {
44
+ secret: "new-mock-refresh-token",
45
+ expiresAt: new Date(Date.now() + 1000 * 60 * 60).toISOString(),
46
+ },
47
+ }),
48
+ })),
49
+ };
50
+ });
51
+
35
52
  function renderAuthProvider({
36
53
  children,
37
54
  embeddedWallets,
@@ -47,23 +64,30 @@ function renderAuthProvider({
47
64
  }
48
65
 
49
66
  function TestComponent() {
50
- const { setJwt } = useCrossmint();
67
+ const { setJwt, setRefreshToken } = useCrossmint();
51
68
  const { wallet, status: walletStatus, error } = useWallet();
52
- const { status: authStatus } = useAuth();
53
-
69
+ const { status: authStatus, refreshToken } = useAuth();
54
70
  return (
55
71
  <div>
56
72
  <div data-testid="error">{error?.message ?? "No Error"}</div>
57
73
  <div data-testid="wallet-status">{walletStatus}</div>
58
74
  <div data-testid="auth-status">{authStatus}</div>
59
75
  <div data-testid="wallet">{wallet ? "Wallet Loaded" : "No Wallet"}</div>
76
+
60
77
  <button data-testid="jwt-input" onClick={() => setJwt("mock-jwt")}>
61
78
  Set JWT
62
79
  </button>
63
-
64
80
  <button data-testid="clear-jwt-button" onClick={() => setJwt(undefined)}>
65
81
  Clear JWT
66
82
  </button>
83
+
84
+ <div data-testid="refresh-token">{refreshToken ?? "No Refresh Token"}</div>
85
+ <button data-testid="set-refresh-token" onClick={() => setRefreshToken("mock-refresh-token")}>
86
+ Set Refresh Token
87
+ </button>
88
+ <button data-testid="clear-refresh-token-button" onClick={() => setRefreshToken(undefined)}>
89
+ Clear Refresh Token
90
+ </button>
67
91
  </div>
68
92
  );
69
93
  }
@@ -72,15 +96,17 @@ describe("CrossmintAuthProvider", () => {
72
96
  let mockSDK: SmartWalletSDK;
73
97
  let mockWallet: EVMSmartWallet;
74
98
  let embeddedWallets: CrossmintAuthWalletConfig;
99
+ let mockCrossmintAuthService: { refreshAuthMaterial: ReturnType<typeof vi.fn> };
75
100
 
76
101
  beforeEach(() => {
77
102
  vi.resetAllMocks();
78
- vi.mocked(createCrossmint).mockImplementation(() => ({} as any));
103
+ vi.mocked(createCrossmint).mockImplementation(() => ({}) as any);
79
104
 
80
105
  mockSDK = mock<SmartWalletSDK>();
81
106
  mockWallet = mock<EVMSmartWallet>();
82
107
  vi.mocked(SmartWalletSDK.init).mockReturnValue(mockSDK);
83
108
  vi.mocked(mockSDK.getOrCreateWallet).mockResolvedValue(mockWallet);
109
+ vi.mocked(getJWTExpiration).mockReturnValue(1000);
84
110
 
85
111
  embeddedWallets = {
86
112
  defaultChain: "polygon",
@@ -88,14 +114,27 @@ describe("CrossmintAuthProvider", () => {
88
114
  type: "evm-smart-wallet",
89
115
  };
90
116
 
91
- Object.defineProperty(document, "cookie", {
92
- writable: true,
93
- value: "",
94
- });
117
+ deleteCookie(REFRESH_TOKEN_PREFIX);
118
+ deleteCookie(SESSION_PREFIX);
119
+
120
+ mockCrossmintAuthService = {
121
+ refreshAuthMaterial: vi.fn().mockResolvedValue({
122
+ jwtToken: "new-mock-jwt",
123
+ refreshToken: {
124
+ secret: "new-mock-refresh-token",
125
+ expiresAt: new Date(Date.now() + 1000 * 60 * 60).toISOString(),
126
+ },
127
+ }),
128
+ };
129
+ vi.mocked(CrossmintAuthService).mockImplementation(() => mockCrossmintAuthService as any);
95
130
  });
96
131
 
97
132
  test("Happy path", async () => {
98
- document.cookie = `${SESSION_PREFIX}=mock-jwt; path=/;SameSite=Lax;`;
133
+ await act(() => {
134
+ document.cookie = `${REFRESH_TOKEN_PREFIX}=mock-refresh-token; path=/; SameSite=Lax;`;
135
+ document.cookie = `${SESSION_PREFIX}=mock-jwt; path=/; SameSite=Lax;`;
136
+ });
137
+
99
138
  const { getByTestId } = renderAuthProvider({
100
139
  children: <TestComponent />,
101
140
  embeddedWallets,
@@ -103,16 +142,19 @@ describe("CrossmintAuthProvider", () => {
103
142
 
104
143
  expect(getByTestId("wallet-status").textContent).toBe("in-progress");
105
144
  expect(getByTestId("auth-status").textContent).toBe("logged-in");
145
+ expect(getByTestId("refresh-token").textContent).toBe("No Refresh Token");
106
146
  expect(getByTestId("wallet").textContent).toBe("No Wallet");
107
147
  expect(getByTestId("error").textContent).toBe("No Error");
108
148
 
109
149
  await waitForSettledState(() => {
110
150
  expect(getByTestId("wallet-status").textContent).toBe("loaded");
111
151
  expect(getByTestId("auth-status").textContent).toBe("logged-in");
152
+ expect(getByTestId("refresh-token").textContent).toBe("new-mock-refresh-token");
112
153
  expect(getByTestId("wallet").textContent).toBe("Wallet Loaded");
113
154
  expect(getByTestId("error").textContent).toBe("No Error");
114
155
  });
115
156
 
157
+ expect(mockCrossmintAuthService.refreshAuthMaterial).toHaveBeenCalledOnce();
116
158
  expect(vi.mocked(mockSDK.getOrCreateWallet)).toHaveBeenCalledOnce();
117
159
  });
118
160
 
@@ -190,4 +232,52 @@ describe("CrossmintAuthProvider", () => {
190
232
  expect(getByTestId("auth-status").textContent).toBe("logged-in");
191
233
  });
192
234
  });
235
+
236
+ test("Setting and clearing refresh token", async () => {
237
+ const { getByTestId } = renderAuthProvider({
238
+ children: <TestComponent />,
239
+ embeddedWallets,
240
+ });
241
+
242
+ expect(getByTestId("refresh-token").textContent).toBe("No Refresh Token");
243
+
244
+ fireEvent.click(getByTestId("set-refresh-token"));
245
+
246
+ await waitForSettledState(() => {
247
+ expect(getByTestId("refresh-token").textContent).toBe("mock-refresh-token");
248
+ });
249
+
250
+ fireEvent.click(getByTestId("clear-refresh-token-button"));
251
+
252
+ await waitForSettledState(() => {
253
+ expect(getByTestId("refresh-token").textContent).toBe("No Refresh Token");
254
+ });
255
+ });
256
+
257
+ test("Logout clears both JWT and refresh token", async () => {
258
+ await act(() => {
259
+ document.cookie = `${REFRESH_TOKEN_PREFIX}=mock-refresh-token; path=/; SameSite=Lax;`;
260
+ document.cookie = `${SESSION_PREFIX}=mock-jwt; path=/; SameSite=Lax;`;
261
+ });
262
+
263
+ const { getByTestId } = renderAuthProvider({
264
+ children: <TestComponent />,
265
+ embeddedWallets,
266
+ });
267
+
268
+ await waitForSettledState(() => {
269
+ expect(getByTestId("auth-status").textContent).toBe("logged-in");
270
+ expect(getByTestId("refresh-token").textContent).toBe("new-mock-refresh-token");
271
+ });
272
+
273
+ await act(() => {
274
+ fireEvent.click(getByTestId("clear-jwt-button"));
275
+ fireEvent.click(getByTestId("clear-refresh-token-button"));
276
+ });
277
+
278
+ await waitForSettledState(() => {
279
+ expect(getByTestId("auth-status").textContent).toBe("logged-out");
280
+ expect(getByTestId("refresh-token").textContent).toBe("No Refresh Token");
281
+ });
282
+ });
193
283
  });
@@ -1,15 +1,15 @@
1
+ import { REFRESH_TOKEN_PREFIX, SESSION_PREFIX, deleteCookie, getCookie, setCookie } from "@/utils/authCookies";
1
2
  import { type ReactNode, createContext, useEffect, useState } from "react";
2
3
  import { createPortal } from "react-dom";
3
4
 
5
+ import { CrossmintAuthService } from "@crossmint/client-sdk-auth-core/client";
4
6
  import type { EVMSmartWalletChain } from "@crossmint/client-sdk-smart-wallet";
5
7
  import { type UIConfig, validateApiKeyAndGetCrossmintBaseUrl } from "@crossmint/common-sdk-base";
6
8
 
7
9
  import AuthModal from "../components/auth/AuthModal";
8
- import { useCrossmint, useWallet } from "../hooks";
10
+ import { AuthMaterial, useCrossmint, useRefreshToken, useWallet } from "../hooks";
9
11
  import { CrossmintWalletProvider } from "./CrossmintWalletProvider";
10
12
 
11
- const SESSION_PREFIX = "crossmint-session";
12
-
13
13
  export type CrossmintAuthWalletConfig = {
14
14
  defaultChain: EVMSmartWalletChain;
15
15
  createOnLogin: "all-users" | "off";
@@ -28,6 +28,7 @@ type AuthContextType = {
28
28
  login: () => void;
29
29
  logout: () => void;
30
30
  jwt?: string;
31
+ refreshToken?: string;
31
32
  status: AuthStatus;
32
33
  };
33
34
 
@@ -38,16 +39,28 @@ export const AuthContext = createContext<AuthContextType>({
38
39
  });
39
40
 
40
41
  export function CrossmintAuthProvider({ embeddedWallets, children, appearance }: CrossmintAuthProviderProps) {
41
- const { crossmint, setJwt } = useCrossmint("CrossmintAuthProvider must be used within CrossmintProvider");
42
+ const { crossmint, setJwt, setRefreshToken } = useCrossmint(
43
+ "CrossmintAuthProvider must be used within CrossmintProvider"
44
+ );
45
+ const crossmintAuthService = new CrossmintAuthService(crossmint.apiKey);
42
46
  const crossmintBaseUrl = validateApiKeyAndGetCrossmintBaseUrl(crossmint.apiKey);
43
47
  const [modalOpen, setModalOpen] = useState(false);
44
48
 
45
- useEffect(() => {
46
- const session = sessionFromClient();
47
- if (session != null) {
48
- setJwt(session);
49
- }
50
- }, []);
49
+ const setAuthMaterial = (authMaterial: AuthMaterial) => {
50
+ setCookie(SESSION_PREFIX, authMaterial.jwtToken);
51
+ setCookie(REFRESH_TOKEN_PREFIX, authMaterial.refreshToken.secret, authMaterial.refreshToken.expiresAt);
52
+ setJwt(authMaterial.jwtToken);
53
+ setRefreshToken(authMaterial.refreshToken.secret);
54
+ };
55
+
56
+ const logout = () => {
57
+ deleteCookie(SESSION_PREFIX);
58
+ deleteCookie(REFRESH_TOKEN_PREFIX);
59
+ setJwt(undefined);
60
+ setRefreshToken(undefined);
61
+ };
62
+
63
+ useRefreshToken({ crossmintAuthService, setAuthMaterial, logout });
51
64
 
52
65
  const login = () => {
53
66
  if (crossmint.jwt != null) {
@@ -58,10 +71,12 @@ export function CrossmintAuthProvider({ embeddedWallets, children, appearance }:
58
71
  setModalOpen(true);
59
72
  };
60
73
 
61
- const logout = () => {
62
- document.cookie = `${SESSION_PREFIX}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
63
- setJwt(undefined);
64
- };
74
+ useEffect(() => {
75
+ if (crossmint.jwt == null) {
76
+ const jwt = getCookie(SESSION_PREFIX);
77
+ setJwt(jwt);
78
+ }
79
+ }, []);
65
80
 
66
81
  useEffect(() => {
67
82
  if (crossmint.jwt == null) {
@@ -71,12 +86,6 @@ export function CrossmintAuthProvider({ embeddedWallets, children, appearance }:
71
86
  setModalOpen(false);
72
87
  }, [crossmint.jwt]);
73
88
 
74
- useEffect(() => {
75
- if (crossmint.jwt) {
76
- document.cookie = `${SESSION_PREFIX}=${crossmint.jwt}; path=/;SameSite=Lax;`;
77
- }
78
- }, [crossmint.jwt]);
79
-
80
89
  const getAuthStatus = (): AuthStatus => {
81
90
  if (crossmint.jwt != null) {
82
91
  return "logged-in";
@@ -93,6 +102,7 @@ export function CrossmintAuthProvider({ embeddedWallets, children, appearance }:
93
102
  login,
94
103
  logout,
95
104
  jwt: crossmint.jwt,
105
+ refreshToken: crossmint.refreshToken,
96
106
  status: getAuthStatus(),
97
107
  }}
98
108
  >
@@ -105,7 +115,7 @@ export function CrossmintAuthProvider({ embeddedWallets, children, appearance }:
105
115
  <AuthModal
106
116
  baseUrl={crossmintBaseUrl}
107
117
  setModalOpen={setModalOpen}
108
- setJwtToken={setJwt}
118
+ setAuthMaterial={setAuthMaterial}
109
119
  apiKey={crossmint.apiKey}
110
120
  appearance={appearance}
111
121
  />,
@@ -144,8 +154,3 @@ function WalletManager({
144
154
 
145
155
  return <>{children}</>;
146
156
  }
147
-
148
- function sessionFromClient(): string | undefined {
149
- const crossmintSession = document.cookie.split("; ").find((row) => row.startsWith(SESSION_PREFIX));
150
- return crossmintSession ? crossmintSession.split("=")[1] : undefined;
151
- }
@@ -1,9 +1,9 @@
1
1
  import { fireEvent, render, waitFor } from "@testing-library/react";
2
- import { ReactNode } from "react";
2
+ import type { ReactNode } from "react";
3
3
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import { mock } from "vitest-mock-extended";
5
5
 
6
- import { EVMSmartWallet, SmartWalletError, SmartWalletSDK } from "@crossmint/client-sdk-smart-wallet";
6
+ import { type EVMSmartWallet, SmartWalletError, SmartWalletSDK } from "@crossmint/client-sdk-smart-wallet";
7
7
  import { createCrossmint } from "@crossmint/common-sdk-base";
8
8
 
9
9
  import { CrossmintProvider, useCrossmint } from "../hooks/useCrossmint";
@@ -69,7 +69,7 @@ describe("CrossmintWalletProvider", () => {
69
69
 
70
70
  beforeEach(() => {
71
71
  vi.resetAllMocks();
72
- vi.mocked(createCrossmint).mockImplementation(() => ({} as any));
72
+ vi.mocked(createCrossmint).mockImplementation(() => ({}) as any);
73
73
  vi.mocked(useCrossmint).mockReturnValue({
74
74
  crossmint: {
75
75
  apiKey: MOCK_API_KEY,
@@ -1,11 +1,11 @@
1
- import { ReactNode, createContext, useMemo, useState } from "react";
1
+ import { type ReactNode, createContext, useMemo, useState } from "react";
2
2
 
3
3
  import {
4
- EVMSmartWallet,
5
- EVMSmartWalletChain,
4
+ type EVMSmartWallet,
5
+ type EVMSmartWalletChain,
6
6
  SmartWalletError,
7
7
  SmartWalletSDK,
8
- WalletParams,
8
+ type WalletParams,
9
9
  } from "@crossmint/client-sdk-smart-wallet";
10
10
 
11
11
  import { useCrossmint } from "../hooks";
@@ -0,0 +1,41 @@
1
+ import { beforeEach, describe, expect, test } from "vitest";
2
+
3
+ import { waitForSettledState } from "../testUtils";
4
+ import { deleteCookie, getCookie, setCookie } from "./authCookies";
5
+
6
+ describe("authCookies", () => {
7
+ beforeEach(() => {
8
+ // Clear all cookies before each test
9
+ document.cookie.split(";").forEach((cookie) => {
10
+ document.cookie = cookie
11
+ .replace(/^ +/, "")
12
+ .replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/");
13
+ });
14
+ });
15
+
16
+ test("should return undefined for non-existent cookie", () => {
17
+ expect(getCookie("non-existent")).toBeUndefined();
18
+ });
19
+
20
+ test("should return the correct value for an existing cookie", async () => {
21
+ document.cookie = "test-cookie=test-value";
22
+ await waitForSettledState(() => {
23
+ expect(getCookie("test-cookie")).toBe("test-value");
24
+ });
25
+ });
26
+
27
+ test("should set a cookie without expiration", async () => {
28
+ setCookie("test-cookie", "test-value");
29
+ await waitForSettledState(() => {
30
+ expect(document.cookie).toContain("test-cookie=test-value");
31
+ });
32
+ });
33
+
34
+ test("should delete an existing cookie", async () => {
35
+ document.cookie = "test-cookie=test-value";
36
+ deleteCookie("test-cookie");
37
+ await waitForSettledState(() => {
38
+ expect(document.cookie).not.toContain("test-cookie");
39
+ });
40
+ });
41
+ });
@@ -0,0 +1,16 @@
1
+ export const SESSION_PREFIX = "crossmint-session";
2
+ export const REFRESH_TOKEN_PREFIX = "crossmint-refresh-token";
3
+
4
+ export function getCookie(name: string): string | undefined {
5
+ const crossmintRefreshToken = document.cookie.split("; ").find((row) => row.startsWith(name));
6
+ return crossmintRefreshToken ? crossmintRefreshToken.split("=")[1] : undefined;
7
+ }
8
+
9
+ export function setCookie(name: string, value: string, expiresAt?: string) {
10
+ const expiresInUtc = expiresAt ? new Date(expiresAt).toUTCString() : "";
11
+ document.cookie = `${name}=${value}; ${expiresAt ? `expires=${expiresInUtc};` : ""} path=/; SameSite=Lax;`;
12
+ }
13
+
14
+ export function deleteCookie(name: string) {
15
+ document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
16
+ }