@crossmint/client-sdk-react-ui 1.3.24 → 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.
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/components/auth/AuthModal.tsx +13 -9
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useCrossmint.test.tsx +43 -7
- package/src/hooks/useCrossmint.tsx +12 -1
- package/src/hooks/useRefreshToken.test.ts +142 -0
- package/src/hooks/useRefreshToken.ts +62 -0
- package/src/index.ts +0 -1
- package/src/providers/CrossmintAuthProvider.test.tsx +102 -12
- package/src/providers/CrossmintAuthProvider.tsx +31 -26
- package/src/utils/authCookies.test.ts +41 -0
- package/src/utils/authCookies.ts +16 -0
|
@@ -18,6 +18,9 @@ class MockSDK {
|
|
|
18
18
|
somethingThatUpdatesJWT(newJWT: string) {
|
|
19
19
|
this.crossmint.jwt = newJWT;
|
|
20
20
|
}
|
|
21
|
+
somethingThatUpdatesRefreshToken(newRefreshToken: string) {
|
|
22
|
+
this.crossmint.refreshToken = newRefreshToken;
|
|
23
|
+
}
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
function renderCrossmintProvider({ children }: { children: JSX.Element }) {
|
|
@@ -30,16 +33,23 @@ describe("CrossmintProvider", () => {
|
|
|
30
33
|
vi.mocked(createCrossmint).mockImplementation(() => ({
|
|
31
34
|
apiKey: MOCK_API_KEY,
|
|
32
35
|
jwt: "",
|
|
36
|
+
refreshToken: "",
|
|
33
37
|
}));
|
|
34
38
|
});
|
|
35
39
|
|
|
36
|
-
it("provides initial JWT
|
|
40
|
+
it("provides initial JWT and refreshToken values", () => {
|
|
37
41
|
const TestComponent = () => {
|
|
38
42
|
const { crossmint } = useCrossmint();
|
|
39
|
-
return
|
|
43
|
+
return (
|
|
44
|
+
<div>
|
|
45
|
+
<div data-testid="jwt">{crossmint.jwt}</div>
|
|
46
|
+
<div data-testid="refreshToken">{crossmint.refreshToken}</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
40
49
|
};
|
|
41
50
|
const { getByTestId } = renderCrossmintProvider({ children: <TestComponent /> });
|
|
42
51
|
expect(getByTestId("jwt").textContent).toBe("");
|
|
52
|
+
expect(getByTestId("refreshToken").textContent).toBe("");
|
|
43
53
|
});
|
|
44
54
|
|
|
45
55
|
it("updates JWT using setJwt", () => {
|
|
@@ -57,30 +67,54 @@ describe("CrossmintProvider", () => {
|
|
|
57
67
|
expect(getByTestId("jwt").textContent).toBe("new_jwt");
|
|
58
68
|
});
|
|
59
69
|
|
|
60
|
-
it("updates
|
|
70
|
+
it("updates refreshToken using setRefreshToken", () => {
|
|
71
|
+
const TestComponent = () => {
|
|
72
|
+
const { crossmint, setRefreshToken } = useCrossmint();
|
|
73
|
+
return (
|
|
74
|
+
<div>
|
|
75
|
+
<div data-testid="refreshToken">{crossmint.refreshToken}</div>
|
|
76
|
+
<button onClick={() => setRefreshToken("new_refresh_token")}>Update Refresh Token</button>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
const { getByTestId, getByText } = renderCrossmintProvider({ children: <TestComponent /> });
|
|
81
|
+
fireEvent.click(getByText("Update Refresh Token"));
|
|
82
|
+
expect(getByTestId("refreshToken").textContent).toBe("new_refresh_token");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("updates JWT and refreshToken using WalletSDK", () => {
|
|
61
86
|
const TestComponent = () => {
|
|
62
87
|
const { crossmint } = useCrossmint();
|
|
63
88
|
useEffect(() => {
|
|
64
89
|
const wallet = new MockSDK(crossmint);
|
|
65
90
|
wallet.somethingThatUpdatesJWT("sdk_jwt");
|
|
91
|
+
wallet.somethingThatUpdatesRefreshToken("sdk_refresh_token");
|
|
66
92
|
}, []);
|
|
67
|
-
return
|
|
93
|
+
return (
|
|
94
|
+
<div>
|
|
95
|
+
<div data-testid="jwt">{crossmint.jwt}</div>
|
|
96
|
+
<div data-testid="refreshToken">{crossmint.refreshToken}</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
68
99
|
};
|
|
69
100
|
const { getByTestId } = renderCrossmintProvider({ children: <TestComponent /> });
|
|
70
101
|
expect(getByTestId("jwt").textContent).toBe("sdk_jwt");
|
|
102
|
+
expect(getByTestId("refreshToken").textContent).toBe("sdk_refresh_token");
|
|
71
103
|
});
|
|
72
104
|
|
|
73
|
-
it("triggers re-render on JWT change", () => {
|
|
105
|
+
it("triggers re-render on JWT and refreshToken change", () => {
|
|
74
106
|
const renderCount = vi.fn();
|
|
75
107
|
const TestComponent = () => {
|
|
76
|
-
const { crossmint, setJwt } = useCrossmint();
|
|
108
|
+
const { crossmint, setJwt, setRefreshToken } = useCrossmint();
|
|
77
109
|
useEffect(() => {
|
|
78
110
|
renderCount();
|
|
79
111
|
});
|
|
80
112
|
return (
|
|
81
113
|
<div>
|
|
82
114
|
<div data-testid="jwt">{crossmint.jwt}</div>
|
|
115
|
+
<div data-testid="refreshToken">{crossmint.refreshToken}</div>
|
|
83
116
|
<button onClick={() => setJwt("new_jwt")}>Update JWT</button>
|
|
117
|
+
<button onClick={() => setRefreshToken("new_refresh_token")}>Update Refresh Token</button>
|
|
84
118
|
</div>
|
|
85
119
|
);
|
|
86
120
|
};
|
|
@@ -90,7 +124,9 @@ describe("CrossmintProvider", () => {
|
|
|
90
124
|
expect(renderCount).toHaveBeenCalledTimes(1);
|
|
91
125
|
|
|
92
126
|
fireEvent.click(getByText("Update JWT"));
|
|
93
|
-
|
|
94
127
|
expect(renderCount).toHaveBeenCalledTimes(2);
|
|
128
|
+
|
|
129
|
+
fireEvent.click(getByText("Update Refresh Token"));
|
|
130
|
+
expect(renderCount).toHaveBeenCalledTimes(3);
|
|
95
131
|
});
|
|
96
132
|
});
|
|
@@ -5,6 +5,7 @@ import { type Crossmint, createCrossmint } from "@crossmint/common-sdk-base";
|
|
|
5
5
|
export interface CrossmintContext {
|
|
6
6
|
crossmint: Crossmint;
|
|
7
7
|
setJwt: (jwt: string | undefined) => void;
|
|
8
|
+
setRefreshToken: (refreshToken: string | undefined) => void;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
const CrossmintContext = createContext<CrossmintContext | null>(null);
|
|
@@ -24,6 +25,9 @@ export function CrossmintProvider({
|
|
|
24
25
|
if (prop === "jwt" && target.jwt !== value) {
|
|
25
26
|
setVersion((v) => v + 1);
|
|
26
27
|
}
|
|
28
|
+
if (prop === "refreshToken" && target.refreshToken !== value) {
|
|
29
|
+
setVersion((v) => v + 1);
|
|
30
|
+
}
|
|
27
31
|
return Reflect.set(target, prop, value);
|
|
28
32
|
},
|
|
29
33
|
})
|
|
@@ -35,14 +39,21 @@ export function CrossmintProvider({
|
|
|
35
39
|
}
|
|
36
40
|
}, []);
|
|
37
41
|
|
|
42
|
+
const setRefreshToken = useCallback((refreshToken: string | undefined) => {
|
|
43
|
+
if (refreshToken !== crossmintRef.current.refreshToken) {
|
|
44
|
+
crossmintRef.current.refreshToken = refreshToken;
|
|
45
|
+
}
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
38
48
|
const value = useMemo(
|
|
39
49
|
() => ({
|
|
40
50
|
get crossmint() {
|
|
41
51
|
return crossmintRef.current;
|
|
42
52
|
},
|
|
43
53
|
setJwt,
|
|
54
|
+
setRefreshToken,
|
|
44
55
|
}),
|
|
45
|
-
[setJwt, version]
|
|
56
|
+
[setJwt, setRefreshToken, version]
|
|
46
57
|
);
|
|
47
58
|
|
|
48
59
|
return <CrossmintContext.Provider value={value}>{children}</CrossmintContext.Provider>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { act, renderHook } from "@testing-library/react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { type CrossmintAuthService, getJWTExpiration } from "@crossmint/client-sdk-auth-core/client";
|
|
5
|
+
import { queueTask } from "@crossmint/client-sdk-base";
|
|
6
|
+
|
|
7
|
+
import * as authCookies from "../utils/authCookies";
|
|
8
|
+
import { type AuthMaterial, useRefreshToken } from "./useRefreshToken";
|
|
9
|
+
|
|
10
|
+
vi.mock("@crossmint/client-sdk-auth-core", () => ({
|
|
11
|
+
CrossmintAuthService: vi.fn(),
|
|
12
|
+
getJWTExpiration: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("../utils/authCookies", () => ({
|
|
16
|
+
getCookie: vi.fn(),
|
|
17
|
+
REFRESH_TOKEN_PREFIX: "crossmint-refresh-token",
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("@crossmint/client-sdk-base", () => ({
|
|
21
|
+
queueTask: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
describe("useRefreshToken", () => {
|
|
25
|
+
const mockCrossmintAuthService = {
|
|
26
|
+
refreshAuthMaterial: vi.fn(),
|
|
27
|
+
} as unknown as CrossmintAuthService;
|
|
28
|
+
|
|
29
|
+
const mockSetAuthMaterial = vi.fn();
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.useFakeTimers();
|
|
33
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
vi.useRealTimers();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should not refresh token if refresh token is not present", async () => {
|
|
42
|
+
vi.mocked(authCookies.getCookie).mockReturnValue(undefined);
|
|
43
|
+
|
|
44
|
+
renderHook(() =>
|
|
45
|
+
useRefreshToken({
|
|
46
|
+
crossmintAuthService: mockCrossmintAuthService,
|
|
47
|
+
setAuthMaterial: mockSetAuthMaterial,
|
|
48
|
+
logout: vi.fn(),
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
await act(async () => {
|
|
53
|
+
await vi.runAllTimersAsync();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(mockCrossmintAuthService.refreshAuthMaterial).not.toHaveBeenCalled();
|
|
57
|
+
expect(mockSetAuthMaterial).not.toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should refresh token if refresh token is present", async () => {
|
|
61
|
+
const mockRefreshToken = "mock-refresh-token";
|
|
62
|
+
const mockAuthMaterial: AuthMaterial = {
|
|
63
|
+
jwtToken: "mock-jwt-token",
|
|
64
|
+
refreshToken: {
|
|
65
|
+
secret: "mock-secret",
|
|
66
|
+
expiresAt: "2023-04-01T00:00:00Z",
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
vi.mocked(authCookies.getCookie).mockReturnValue(mockRefreshToken);
|
|
71
|
+
vi.mocked(mockCrossmintAuthService.refreshAuthMaterial).mockResolvedValue(mockAuthMaterial);
|
|
72
|
+
vi.mocked(getJWTExpiration).mockReturnValue(Date.now() / 1000 + 3600); // 1 hour from now
|
|
73
|
+
|
|
74
|
+
renderHook(() =>
|
|
75
|
+
useRefreshToken({
|
|
76
|
+
crossmintAuthService: mockCrossmintAuthService,
|
|
77
|
+
setAuthMaterial: mockSetAuthMaterial,
|
|
78
|
+
logout: vi.fn(),
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
await act(async () => {
|
|
83
|
+
await vi.runAllTimersAsync();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(mockCrossmintAuthService.refreshAuthMaterial).toHaveBeenCalledWith(mockRefreshToken);
|
|
87
|
+
expect(mockSetAuthMaterial).toHaveBeenCalledWith(mockAuthMaterial);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should schedule next refresh before token expiration", async () => {
|
|
91
|
+
const mockRefreshToken = "mock-refresh-token";
|
|
92
|
+
const mockAuthMaterial: AuthMaterial = {
|
|
93
|
+
jwtToken: "mock-jwt-token",
|
|
94
|
+
refreshToken: {
|
|
95
|
+
secret: "mock-secret",
|
|
96
|
+
expiresAt: "2023-04-01T00:00:00Z",
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
vi.mocked(authCookies.getCookie).mockReturnValue(mockRefreshToken);
|
|
101
|
+
vi.mocked(mockCrossmintAuthService.refreshAuthMaterial).mockResolvedValue(mockAuthMaterial);
|
|
102
|
+
vi.mocked(getJWTExpiration).mockReturnValue(Date.now() / 1000 + 3600); // 1 hour from now
|
|
103
|
+
|
|
104
|
+
renderHook(() =>
|
|
105
|
+
useRefreshToken({
|
|
106
|
+
crossmintAuthService: mockCrossmintAuthService,
|
|
107
|
+
setAuthMaterial: mockSetAuthMaterial,
|
|
108
|
+
logout: vi.fn(),
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
await act(async () => {
|
|
113
|
+
await vi.runAllTimersAsync();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(vi.mocked(queueTask)).toHaveBeenCalledTimes(1);
|
|
117
|
+
expect(vi.mocked(queueTask)).toHaveBeenCalledWith(expect.any(Function), expect.any(Number));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should handle errors during token refresh", async () => {
|
|
121
|
+
const mockRefreshToken = "mock-refresh-token";
|
|
122
|
+
const mockError = new Error("Refresh failed");
|
|
123
|
+
|
|
124
|
+
vi.mocked(authCookies.getCookie).mockReturnValue(mockRefreshToken);
|
|
125
|
+
vi.mocked(mockCrossmintAuthService.refreshAuthMaterial).mockRejectedValue(mockError);
|
|
126
|
+
|
|
127
|
+
renderHook(() =>
|
|
128
|
+
useRefreshToken({
|
|
129
|
+
crossmintAuthService: mockCrossmintAuthService,
|
|
130
|
+
setAuthMaterial: mockSetAuthMaterial,
|
|
131
|
+
logout: vi.fn(),
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
await act(async () => {
|
|
136
|
+
await vi.runAllTimersAsync();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(console.error).toHaveBeenCalledWith(mockError);
|
|
140
|
+
expect(mockSetAuthMaterial).not.toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
import type { CrossmintAuthService } from "@crossmint/client-sdk-auth-core/client";
|
|
4
|
+
import { getJWTExpiration } from "@crossmint/client-sdk-auth-core/client";
|
|
5
|
+
import { queueTask, type CancellableTask } from "@crossmint/client-sdk-base";
|
|
6
|
+
|
|
7
|
+
import { REFRESH_TOKEN_PREFIX, getCookie } from "../utils/authCookies";
|
|
8
|
+
|
|
9
|
+
// 2 minutes before jwt expiration
|
|
10
|
+
const TIME_BEFORE_EXPIRING_JWT_IN_SECONDS = 120;
|
|
11
|
+
|
|
12
|
+
export type AuthMaterial = {
|
|
13
|
+
jwtToken: string;
|
|
14
|
+
refreshToken: {
|
|
15
|
+
secret: string;
|
|
16
|
+
expiresAt: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type UseAuthTokenRefreshProps = {
|
|
21
|
+
crossmintAuthService: CrossmintAuthService;
|
|
22
|
+
setAuthMaterial: (authMaterial: AuthMaterial) => void;
|
|
23
|
+
logout: () => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function useRefreshToken({ crossmintAuthService, setAuthMaterial, logout }: UseAuthTokenRefreshProps) {
|
|
27
|
+
const refreshTaskRef = useRef<CancellableTask | null>(null);
|
|
28
|
+
|
|
29
|
+
const refreshAuthMaterial = useCallback(async () => {
|
|
30
|
+
const refreshToken = getCookie(REFRESH_TOKEN_PREFIX);
|
|
31
|
+
if (refreshToken != null) {
|
|
32
|
+
try {
|
|
33
|
+
const result = await crossmintAuthService.refreshAuthMaterial(refreshToken);
|
|
34
|
+
setAuthMaterial(result);
|
|
35
|
+
const jwtExpiration = getJWTExpiration(result.jwtToken);
|
|
36
|
+
|
|
37
|
+
if (jwtExpiration == null) {
|
|
38
|
+
throw new Error("Invalid JWT");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const currentTime = Date.now() / 1000;
|
|
42
|
+
const timeToExpire = jwtExpiration - currentTime - TIME_BEFORE_EXPIRING_JWT_IN_SECONDS;
|
|
43
|
+
if (timeToExpire > 0) {
|
|
44
|
+
const endTime = Date.now() + timeToExpire * 1000;
|
|
45
|
+
refreshTaskRef.current = queueTask(refreshAuthMaterial, endTime);
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
logout();
|
|
49
|
+
console.error(error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
refreshAuthMaterial();
|
|
56
|
+
return () => {
|
|
57
|
+
if (refreshTaskRef.current) {
|
|
58
|
+
refreshTaskRef.current.cancel();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}, []);
|
|
62
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { deleteCookie, REFRESH_TOKEN_PREFIX, SESSION_PREFIX } from "@/utils/authCookies";
|
|
1
2
|
import { fireEvent, render } from "@testing-library/react";
|
|
2
|
-
import type
|
|
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
|
|
|
7
|
+
import { CrossmintAuthService, getJWTExpiration } from "@crossmint/client-sdk-auth-core/client";
|
|
6
8
|
import { type EVMSmartWallet, SmartWalletSDK } from "@crossmint/client-sdk-smart-wallet";
|
|
7
9
|
import { createCrossmint } from "@crossmint/common-sdk-base";
|
|
8
10
|
|
|
@@ -11,8 +13,6 @@ import { CrossmintProvider, useCrossmint } from "../hooks/useCrossmint";
|
|
|
11
13
|
import { MOCK_API_KEY, waitForSettledState } from "../testUtils";
|
|
12
14
|
import { CrossmintAuthProvider, type CrossmintAuthWalletConfig } from "./CrossmintAuthProvider";
|
|
13
15
|
|
|
14
|
-
const SESSION_PREFIX = "crossmint-session";
|
|
15
|
-
|
|
16
16
|
vi.mock("@crossmint/client-sdk-smart-wallet", async () => {
|
|
17
17
|
const actual = await vi.importActual("@crossmint/client-sdk-smart-wallet");
|
|
18
18
|
return {
|
|
@@ -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,6 +96,7 @@ 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();
|
|
@@ -81,6 +106,7 @@ describe("CrossmintAuthProvider", () => {
|
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
}
|