@crossmint/client-sdk-react-ui 1.3.13 → 1.3.15

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.
@@ -0,0 +1,198 @@
1
+ import { fireEvent, render } from "@testing-library/react";
2
+ import { ReactNode } from "react";
3
+ import { beforeEach, describe, expect, vi } from "vitest";
4
+ import { mock } from "vitest-mock-extended";
5
+
6
+ import { EVMSmartWallet, SmartWalletSDK } from "@crossmint/client-sdk-smart-wallet";
7
+ import { createCrossmint } from "@crossmint/common-sdk-base";
8
+
9
+ import { useAuth, useWallet } from "../hooks";
10
+ import { CrossmintProvider, useCrossmint } from "../hooks/useCrossmint";
11
+ import { MOCK_API_KEY, waitForSettledState } from "../testUtils";
12
+ import { CrossmintAuthProvider, CrossmintAuthWalletConfig } from "./CrossmintAuthProvider";
13
+
14
+ vi.mock("@crossmint/client-sdk-smart-wallet", async () => {
15
+ const actual = await vi.importActual("@crossmint/client-sdk-smart-wallet");
16
+ return {
17
+ ...actual,
18
+ SmartWalletSDK: {
19
+ init: vi.fn(),
20
+ },
21
+ };
22
+ });
23
+
24
+ vi.mock("@crossmint/common-sdk-base", async () => {
25
+ const actual = await vi.importActual("@crossmint/common-sdk-base");
26
+ return {
27
+ ...actual,
28
+ createCrossmint: vi.fn(),
29
+ validateApiKeyAndGetCrossmintBaseUrl: vi.fn(),
30
+ };
31
+ });
32
+
33
+ function renderAuthProvider({
34
+ children,
35
+ embeddedWallets,
36
+ }: {
37
+ children: ReactNode;
38
+ embeddedWallets: CrossmintAuthWalletConfig;
39
+ }) {
40
+ return render(
41
+ <CrossmintProvider apiKey={MOCK_API_KEY}>
42
+ <CrossmintAuthProvider embeddedWallets={embeddedWallets}>{children}</CrossmintAuthProvider>
43
+ </CrossmintProvider>
44
+ );
45
+ }
46
+
47
+ function TestComponent() {
48
+ const { setJwt } = useCrossmint();
49
+ const { wallet, status: walletStatus, error } = useWallet();
50
+ const { status: authStatus } = useAuth();
51
+
52
+ return (
53
+ <div>
54
+ <div data-testid="error">{error?.message ?? "No Error"}</div>
55
+ <div data-testid="wallet-status">{walletStatus}</div>
56
+ <div data-testid="auth-status">{authStatus}</div>
57
+ <div data-testid="wallet">{wallet ? "Wallet Loaded" : "No Wallet"}</div>
58
+ <button data-testid="jwt-input" onClick={() => setJwt("mock-jwt")}>
59
+ Set JWT
60
+ </button>
61
+
62
+ <button data-testid="clear-jwt-button" onClick={() => setJwt(undefined)}>
63
+ Clear JWT
64
+ </button>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ describe("CrossmintAuthProvider", () => {
70
+ let mockSDK: SmartWalletSDK;
71
+ let mockWallet: EVMSmartWallet;
72
+ let embeddedWallets: CrossmintAuthWalletConfig;
73
+
74
+ beforeEach(() => {
75
+ vi.resetAllMocks();
76
+ vi.mocked(createCrossmint).mockImplementation(() => ({
77
+ apiKey: MOCK_API_KEY,
78
+ jwt: "mock-jwt",
79
+ }));
80
+
81
+ mockSDK = mock<SmartWalletSDK>();
82
+ mockWallet = mock<EVMSmartWallet>();
83
+ vi.mocked(SmartWalletSDK.init).mockReturnValue(mockSDK);
84
+ vi.mocked(mockSDK.getOrCreateWallet).mockResolvedValue(mockWallet);
85
+
86
+ embeddedWallets = {
87
+ defaultChain: "polygon",
88
+ createOnLogin: "all-users",
89
+ type: "evm-smart-wallet",
90
+ };
91
+ });
92
+
93
+ test("Happy path", async () => {
94
+ const { getByTestId } = renderAuthProvider({
95
+ children: <TestComponent />,
96
+ embeddedWallets,
97
+ });
98
+
99
+ expect(getByTestId("wallet-status").textContent).toBe("in-progress");
100
+ expect(getByTestId("auth-status").textContent).toBe("logged-in");
101
+ expect(getByTestId("wallet").textContent).toBe("No Wallet");
102
+ expect(getByTestId("error").textContent).toBe("No Error");
103
+
104
+ await waitForSettledState(() => {
105
+ expect(getByTestId("wallet-status").textContent).toBe("loaded");
106
+ expect(getByTestId("auth-status").textContent).toBe("logged-in");
107
+ expect(getByTestId("wallet").textContent).toBe("Wallet Loaded");
108
+ expect(getByTestId("error").textContent).toBe("No Error");
109
+ });
110
+
111
+ expect(vi.mocked(mockSDK.getOrCreateWallet)).toHaveBeenCalledOnce();
112
+ });
113
+
114
+ test(`When "createOnLogin" is "false", wallet is not loaded`, async () => {
115
+ const { getByTestId } = renderAuthProvider({
116
+ children: <TestComponent />,
117
+ embeddedWallets: {
118
+ defaultChain: "polygon",
119
+ createOnLogin: "off",
120
+ type: "evm-smart-wallet",
121
+ },
122
+ });
123
+
124
+ await waitForSettledState(() => {
125
+ expect(getByTestId("wallet").textContent).toBe("No Wallet");
126
+ expect(getByTestId("wallet-status").textContent).toBe("not-loaded");
127
+ });
128
+
129
+ expect(vi.mocked(mockSDK.getOrCreateWallet)).not.toHaveBeenCalled();
130
+ });
131
+
132
+ test(`When the jwt from crossmint provider is not defined, wallet is not loaded`, async () => {
133
+ vi.mocked(createCrossmint).mockImplementation(() => ({
134
+ apiKey: MOCK_API_KEY,
135
+ jwt: undefined,
136
+ }));
137
+
138
+ const { getByTestId } = renderAuthProvider({
139
+ children: <TestComponent />,
140
+ embeddedWallets,
141
+ });
142
+
143
+ await waitForSettledState(() => {
144
+ expect(getByTestId("wallet-status").textContent).toBe("not-loaded");
145
+ expect(getByTestId("auth-status").textContent).toBe("logged-out");
146
+ expect(getByTestId("wallet").textContent).toBe("No Wallet");
147
+ });
148
+
149
+ expect(vi.mocked(mockSDK.getOrCreateWallet)).not.toHaveBeenCalled();
150
+ });
151
+
152
+ test("When the jwt is cleared, so is the wallet", async () => {
153
+ const { getByTestId } = renderAuthProvider({
154
+ children: <TestComponent />,
155
+ embeddedWallets,
156
+ });
157
+
158
+ await waitForSettledState(() => {
159
+ expect(getByTestId("wallet-status").textContent).toBe("loaded");
160
+ expect(getByTestId("auth-status").textContent).toBe("logged-in");
161
+ expect(getByTestId("wallet").textContent).toBe("Wallet Loaded");
162
+ });
163
+
164
+ fireEvent.click(getByTestId("clear-jwt-button"));
165
+
166
+ await waitForSettledState(() => {
167
+ expect(getByTestId("wallet-status").textContent).toBe("not-loaded");
168
+ expect(getByTestId("auth-status").textContent).toBe("logged-out");
169
+ expect(getByTestId("wallet").textContent).toBe("No Wallet");
170
+ });
171
+
172
+ expect(vi.mocked(mockSDK.getOrCreateWallet)).toHaveBeenCalledOnce();
173
+ });
174
+
175
+ test(`Logging in and asserting the auth status`, async () => {
176
+ vi.mocked(createCrossmint).mockImplementation(() => ({
177
+ apiKey: MOCK_API_KEY,
178
+ jwt: undefined,
179
+ }));
180
+
181
+ const { getByTestId } = renderAuthProvider({
182
+ children: <TestComponent />,
183
+ embeddedWallets,
184
+ });
185
+
186
+ await waitForSettledState(() => {
187
+ expect(getByTestId("auth-status").textContent).toBe("logged-out");
188
+ });
189
+
190
+ fireEvent.click(getByTestId("jwt-input"));
191
+
192
+ // We can't assert the status: "in-progress" because the jwt state is set instantly
193
+
194
+ await waitForSettledState(() => {
195
+ expect(getByTestId("auth-status").textContent).toBe("logged-in");
196
+ });
197
+ });
198
+ });
@@ -1,23 +1,138 @@
1
- import { useCrossmint } from "@/hooks";
2
- import { ReactNode } from "react";
1
+ import { type ReactNode, createContext, useEffect, useState } from "react";
2
+ import { createPortal } from "react-dom";
3
3
 
4
- import { AuthProvider as AuthCoreProvider } from "@crossmint/client-sdk-auth-core/client";
5
- import { UIConfig } from "@crossmint/common-sdk-base";
4
+ import type { EVMSmartWalletChain } from "@crossmint/client-sdk-smart-wallet";
5
+ import { type UIConfig, validateApiKeyAndGetCrossmintBaseUrl } from "@crossmint/common-sdk-base";
6
6
 
7
- import { CrossmintWalletConfig, CrossmintWalletProvider } from "./CrossmintWalletProvider";
7
+ import AuthModal from "../components/auth/AuthModal";
8
+ import { useCrossmint, useWallet } from "../hooks";
9
+ import { SESSION_PREFIX } from "../utils";
10
+ import { CrossmintWalletProvider } from "./CrossmintWalletProvider";
8
11
 
9
- type CrossmintAuthProviderProps = {
10
- embeddedWallets: CrossmintWalletConfig;
11
- children: ReactNode;
12
+ export type CrossmintAuthWalletConfig = {
13
+ defaultChain: EVMSmartWalletChain;
14
+ createOnLogin: "all-users" | "off";
15
+ type: "evm-smart-wallet";
16
+ };
17
+
18
+ export type CrossmintAuthProviderProps = {
19
+ embeddedWallets: CrossmintAuthWalletConfig;
12
20
  appearance?: UIConfig;
21
+ children: ReactNode;
13
22
  };
14
23
 
24
+ type AuthStatus = "logged-in" | "logged-out" | "in-progress";
25
+
26
+ type AuthContextType = {
27
+ login: () => void;
28
+ logout: () => void;
29
+ jwt?: string;
30
+ status: AuthStatus;
31
+ };
32
+
33
+ export const AuthContext = createContext<AuthContextType>({
34
+ login: () => {},
35
+ logout: () => {},
36
+ status: "logged-out",
37
+ });
38
+
15
39
  export function CrossmintAuthProvider({ embeddedWallets, children, appearance }: CrossmintAuthProviderProps) {
16
40
  const { crossmint, setJwt } = useCrossmint("CrossmintAuthProvider must be used within CrossmintProvider");
41
+ const crossmintBaseUrl = validateApiKeyAndGetCrossmintBaseUrl(crossmint.apiKey);
42
+ const [modalOpen, setModalOpen] = useState(false);
43
+
44
+ const login = () => {
45
+ if (crossmint.jwt != null) {
46
+ console.log("User already logged in");
47
+ return;
48
+ }
49
+
50
+ setModalOpen(true);
51
+ };
52
+
53
+ const logout = () => {
54
+ document.cookie = `${SESSION_PREFIX}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
55
+ setJwt(undefined);
56
+ };
57
+
58
+ useEffect(() => {
59
+ if (crossmint.jwt == null) {
60
+ return;
61
+ }
62
+
63
+ setModalOpen(false);
64
+ }, [crossmint.jwt]);
65
+
66
+ useEffect(() => {
67
+ if (crossmint.jwt) {
68
+ document.cookie = `${SESSION_PREFIX}=${crossmint.jwt}; path=/;SameSite=Lax;`;
69
+ }
70
+ }, [crossmint.jwt]);
71
+
72
+ const getAuthStatus = (): AuthStatus => {
73
+ if (crossmint.jwt != null) {
74
+ return "logged-in";
75
+ }
76
+ if (modalOpen) {
77
+ return "in-progress";
78
+ }
79
+ return "logged-out";
80
+ };
17
81
 
18
82
  return (
19
- <AuthCoreProvider setJwtToken={setJwt} crossmint={crossmint} appearance={appearance}>
20
- <CrossmintWalletProvider config={embeddedWallets}>{children}</CrossmintWalletProvider>
21
- </AuthCoreProvider>
83
+ <AuthContext.Provider
84
+ value={{
85
+ login,
86
+ logout,
87
+ jwt: crossmint.jwt,
88
+ status: getAuthStatus(),
89
+ }}
90
+ >
91
+ <CrossmintWalletProvider defaultChain={embeddedWallets.defaultChain}>
92
+ <WalletManager embeddedWallets={embeddedWallets} accessToken={crossmint.jwt}>
93
+ {children}
94
+ </WalletManager>
95
+ {modalOpen
96
+ ? createPortal(
97
+ <AuthModal
98
+ baseUrl={crossmintBaseUrl}
99
+ setModalOpen={setModalOpen}
100
+ setJwtToken={setJwt}
101
+ apiKey={crossmint.apiKey}
102
+ appearance={appearance}
103
+ />,
104
+
105
+ document.body
106
+ )
107
+ : null}
108
+ </CrossmintWalletProvider>
109
+ </AuthContext.Provider>
22
110
  );
23
111
  }
112
+
113
+ function WalletManager({
114
+ embeddedWallets,
115
+ children,
116
+ accessToken,
117
+ }: {
118
+ embeddedWallets: CrossmintAuthWalletConfig;
119
+ children: ReactNode;
120
+ accessToken: string | undefined;
121
+ }) {
122
+ const { getOrCreateWallet, clearWallet, status } = useWallet();
123
+
124
+ useEffect(() => {
125
+ if (embeddedWallets.createOnLogin === "all-users" && status === "not-loaded" && accessToken != null) {
126
+ getOrCreateWallet({
127
+ type: embeddedWallets.type,
128
+ signer: { type: "PASSKEY" },
129
+ });
130
+ }
131
+
132
+ if (status === "loaded" && accessToken == null) {
133
+ clearWallet();
134
+ }
135
+ }, [accessToken, status]);
136
+
137
+ return <>{children}</>;
138
+ }
@@ -0,0 +1,206 @@
1
+ import { fireEvent, render, waitFor } from "@testing-library/react";
2
+ import { ReactNode } from "react";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { mock } from "vitest-mock-extended";
5
+
6
+ import { EVMSmartWallet, SmartWalletError, SmartWalletSDK } from "@crossmint/client-sdk-smart-wallet";
7
+ import { createCrossmint } from "@crossmint/common-sdk-base";
8
+
9
+ import { CrossmintProvider, useCrossmint } from "../hooks/useCrossmint";
10
+ import { useWallet } from "../hooks/useWallet";
11
+ import { MOCK_API_KEY, waitForSettledState } from "../testUtils";
12
+ import { CrossmintWalletProvider } from "./CrossmintWalletProvider";
13
+
14
+ vi.mock("@crossmint/client-sdk-smart-wallet", async () => {
15
+ const actual = await vi.importActual("@crossmint/client-sdk-smart-wallet");
16
+ return {
17
+ ...actual,
18
+ SmartWalletSDK: {
19
+ init: vi.fn(),
20
+ },
21
+ };
22
+ });
23
+
24
+ vi.mock("@crossmint/common-sdk-base", async () => {
25
+ const actual = await vi.importActual("@crossmint/common-sdk-base");
26
+ return {
27
+ ...actual,
28
+ createCrossmint: vi.fn(),
29
+ };
30
+ });
31
+
32
+ function renderWalletProvider({ children }: { children: ReactNode }) {
33
+ return render(
34
+ <CrossmintProvider apiKey={MOCK_API_KEY}>
35
+ <CrossmintWalletProvider defaultChain="polygon-amoy">{children}</CrossmintWalletProvider>
36
+ </CrossmintProvider>
37
+ );
38
+ }
39
+
40
+ function TestComponent() {
41
+ const { status, wallet, error, getOrCreateWallet, clearWallet } = useWallet();
42
+
43
+ return (
44
+ <div>
45
+ <div data-testid="error">{error?.message ?? "No Error"}</div>
46
+ <div data-testid="status">{status}</div>
47
+ <div data-testid="wallet">{wallet ? "Wallet Loaded" : "No Wallet"}</div>
48
+ <button data-testid="create-wallet-button" onClick={() => getOrCreateWallet()}>
49
+ Create Wallet
50
+ </button>
51
+ <button data-testid="clear-wallet-button" onClick={() => clearWallet()}>
52
+ Clear Wallet
53
+ </button>
54
+ </div>
55
+ );
56
+ }
57
+
58
+ describe("CrossmintWalletProvider", () => {
59
+ let mockSDK: SmartWalletSDK;
60
+ let mockWallet: EVMSmartWallet;
61
+
62
+ beforeEach(() => {
63
+ vi.resetAllMocks();
64
+
65
+ vi.mocked(createCrossmint).mockImplementation(() => ({
66
+ apiKey: MOCK_API_KEY,
67
+ jwt: "mock-jwt",
68
+ }));
69
+
70
+ mockSDK = mock<SmartWalletSDK>();
71
+ mockWallet = mock<EVMSmartWallet>();
72
+ vi.mocked(SmartWalletSDK.init).mockReturnValue(mockSDK);
73
+ vi.mocked(mockSDK.getOrCreateWallet).mockResolvedValue(mockWallet);
74
+ });
75
+
76
+ describe("getOrCreateWallet", () => {
77
+ test("happy path ", async () => {
78
+ const { getByTestId } = renderWalletProvider({
79
+ children: <TestComponent />,
80
+ });
81
+
82
+ expect(getByTestId("status").textContent).toBe("not-loaded");
83
+ expect(getByTestId("wallet").textContent).toBe("No Wallet");
84
+ expect(getByTestId("error").textContent).toBe("No Error");
85
+
86
+ fireEvent.click(getByTestId("create-wallet-button"));
87
+
88
+ await waitFor(() => {
89
+ expect(getByTestId("status").textContent).toBe("in-progress");
90
+ expect(getByTestId("wallet").textContent).toBe("No Wallet");
91
+ expect(getByTestId("error").textContent).toBe("No Error");
92
+ });
93
+
94
+ await waitForSettledState(() => {
95
+ expect(getByTestId("status").textContent).toBe("loaded");
96
+ expect(getByTestId("wallet").textContent).toBe("Wallet Loaded");
97
+ expect(getByTestId("error").textContent).toBe("No Error");
98
+ });
99
+
100
+ expect(vi.mocked(mockSDK.getOrCreateWallet)).toHaveBeenCalledOnce();
101
+ });
102
+
103
+ describe(`When jwt is not set in "CrossmintProvider"`, () => {
104
+ beforeEach(() => {
105
+ vi.mocked(createCrossmint).mockImplementation(() => ({
106
+ apiKey: MOCK_API_KEY,
107
+ jwt: undefined,
108
+ }));
109
+ });
110
+
111
+ it("does not create a wallet", async () => {
112
+ const { getByTestId } = renderWalletProvider({
113
+ children: <TestComponent />,
114
+ });
115
+
116
+ fireEvent.click(getByTestId("create-wallet-button"));
117
+
118
+ await waitForSettledState(() => {
119
+ expect(getByTestId("status").textContent).toBe("not-loaded");
120
+ expect(getByTestId("wallet").textContent).toBe("No Wallet");
121
+ expect(getByTestId("error").textContent).toBe("No Error");
122
+ });
123
+
124
+ expect(vi.mocked(mockSDK.getOrCreateWallet)).not.toHaveBeenCalled();
125
+ });
126
+ });
127
+
128
+ describe("When getOrCreateWallet throws a known error", () => {
129
+ beforeEach(() => {
130
+ vi.mocked(mockSDK.getOrCreateWallet).mockRejectedValue(new SmartWalletError("Wallet creation failed"));
131
+ });
132
+
133
+ it("should set error directly with the thrown error", async () => {
134
+ const { getByTestId } = renderWalletProvider({
135
+ children: <TestComponent />,
136
+ });
137
+
138
+ fireEvent.click(getByTestId("create-wallet-button"));
139
+
140
+ await waitFor(() => {
141
+ expect(getByTestId("status").textContent).toBe("in-progress");
142
+ expect(getByTestId("wallet").textContent).toBe("No Wallet");
143
+ expect(getByTestId("error").textContent).toBe("No Error");
144
+ });
145
+
146
+ await waitForSettledState(() => {
147
+ expect(getByTestId("status").textContent).toBe("loading-error");
148
+ expect(getByTestId("wallet").textContent).toBe("No Wallet");
149
+ expect(getByTestId("error").textContent).toBe("Wallet creation failed");
150
+ });
151
+
152
+ expect(vi.mocked(mockSDK.getOrCreateWallet)).toHaveBeenCalledOnce();
153
+ });
154
+ });
155
+
156
+ describe("When getOrCreateWallet throws an unknown error", () => {
157
+ beforeEach(() => {
158
+ vi.mocked(mockSDK.getOrCreateWallet).mockRejectedValue(new Error("Wallet creation failed"));
159
+ });
160
+
161
+ it("should set the error with the thrown error wrapped with a SmartWalletError", async () => {
162
+ const { getByTestId } = renderWalletProvider({
163
+ children: <TestComponent />,
164
+ });
165
+
166
+ fireEvent.click(getByTestId("create-wallet-button"));
167
+
168
+ await waitFor(() => {
169
+ expect(getByTestId("status").textContent).toBe("in-progress");
170
+ expect(getByTestId("wallet").textContent).toBe("No Wallet");
171
+ expect(getByTestId("error").textContent).toBe("No Error");
172
+ });
173
+
174
+ await waitForSettledState(() => {
175
+ expect(getByTestId("status").textContent).toBe("loading-error");
176
+ expect(getByTestId("wallet").textContent).toBe("No Wallet");
177
+ expect(getByTestId("error").textContent).toBe("Unknown Wallet Error: Wallet creation failed");
178
+ });
179
+
180
+ expect(vi.mocked(mockSDK.getOrCreateWallet)).toHaveBeenCalledOnce();
181
+ });
182
+ });
183
+ });
184
+
185
+ test("clearWallet happy path", async () => {
186
+ const { getByTestId } = renderWalletProvider({
187
+ children: <TestComponent />,
188
+ });
189
+
190
+ fireEvent.click(getByTestId("create-wallet-button"));
191
+
192
+ await waitForSettledState(() => {
193
+ expect(getByTestId("status").textContent).toBe("loaded");
194
+ expect(getByTestId("wallet").textContent).toBe("Wallet Loaded");
195
+ expect(getByTestId("error").textContent).toBe("No Error");
196
+ });
197
+
198
+ fireEvent.click(getByTestId("clear-wallet-button"));
199
+
200
+ await waitForSettledState(() => {
201
+ expect(getByTestId("status").textContent).toBe("not-loaded");
202
+ expect(getByTestId("wallet").textContent).toBe("No Wallet");
203
+ expect(getByTestId("error").textContent).toBe("No Error");
204
+ });
205
+ });
206
+ });