@frak-labs/react-sdk 0.1.1 → 0.2.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 (38) hide show
  1. package/README.md +25 -0
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.cts +28 -64
  4. package/dist/index.d.ts +28 -64
  5. package/dist/index.js +1 -1
  6. package/package.json +17 -16
  7. package/src/hook/helper/useReferralInteraction.test.ts +358 -0
  8. package/src/hook/helper/useReferralInteraction.ts +78 -0
  9. package/src/hook/index.ts +10 -0
  10. package/src/hook/useDisplayModal.test.ts +275 -0
  11. package/src/hook/useDisplayModal.ts +68 -0
  12. package/src/hook/useFrakClient.test.ts +119 -0
  13. package/src/hook/useFrakClient.ts +11 -0
  14. package/src/hook/useFrakConfig.test.ts +184 -0
  15. package/src/hook/useFrakConfig.ts +22 -0
  16. package/src/hook/useGetMerchantInformation.ts +56 -0
  17. package/src/hook/useOpenSso.test.ts +202 -0
  18. package/src/hook/useOpenSso.ts +51 -0
  19. package/src/hook/usePrepareSso.test.ts +197 -0
  20. package/src/hook/usePrepareSso.ts +55 -0
  21. package/src/hook/useSendTransaction.test.ts +218 -0
  22. package/src/hook/useSendTransaction.ts +62 -0
  23. package/src/hook/useSiweAuthenticate.test.ts +258 -0
  24. package/src/hook/useSiweAuthenticate.ts +66 -0
  25. package/src/hook/useWalletStatus.test.ts +112 -0
  26. package/src/hook/useWalletStatus.ts +55 -0
  27. package/src/hook/utils/useFrakContext.test.ts +157 -0
  28. package/src/hook/utils/useFrakContext.ts +42 -0
  29. package/src/hook/utils/useMounted.test.ts +70 -0
  30. package/src/hook/utils/useMounted.ts +12 -0
  31. package/src/hook/utils/useWindowLocation.test.ts +54 -0
  32. package/src/hook/utils/useWindowLocation.ts +40 -0
  33. package/src/index.ts +25 -0
  34. package/src/provider/FrakConfigProvider.test.ts +246 -0
  35. package/src/provider/FrakConfigProvider.ts +54 -0
  36. package/src/provider/FrakIFrameClientProvider.test.tsx +209 -0
  37. package/src/provider/FrakIFrameClientProvider.ts +86 -0
  38. package/src/provider/index.ts +7 -0
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Tests for useWalletStatus hook
3
+ * Tests TanStack Query wrapper for watching wallet status
4
+ */
5
+
6
+ import { vi } from "vitest";
7
+
8
+ vi.mock("@frak-labs/core-sdk/actions");
9
+
10
+ import type { WalletStatusReturnType } from "@frak-labs/core-sdk";
11
+ import { watchWalletStatus } from "@frak-labs/core-sdk/actions";
12
+ import { renderHook, waitFor } from "@testing-library/react";
13
+ import { describe, expect, test } from "../../tests/vitest-fixtures";
14
+ import { useWalletStatus } from "./useWalletStatus";
15
+
16
+ describe("useWalletStatus", () => {
17
+ test("should be disabled when client is not available", ({
18
+ queryWrapper,
19
+ }) => {
20
+ const { result } = renderHook(() => useWalletStatus(), {
21
+ wrapper: queryWrapper.wrapper,
22
+ });
23
+
24
+ // Query should not run when client is not available
25
+ expect(result.current.isPending).toBe(true);
26
+ expect(result.current.isFetching).toBe(false);
27
+ expect(result.current.data).toBeUndefined();
28
+ });
29
+
30
+ test("should watch wallet status successfully", async ({
31
+ mockFrakProviders,
32
+ }) => {
33
+ const mockStatus: WalletStatusReturnType = {
34
+ key: "connected",
35
+ wallet: "0x1234567890123456789012345678901234567890",
36
+ };
37
+
38
+ vi.mocked(watchWalletStatus).mockResolvedValue(mockStatus);
39
+
40
+ const { result } = renderHook(() => useWalletStatus(), {
41
+ wrapper: mockFrakProviders,
42
+ });
43
+
44
+ await waitFor(() => {
45
+ expect(result.current.isSuccess).toBe(true);
46
+ });
47
+
48
+ expect(result.current.data).toEqual(mockStatus);
49
+ expect(watchWalletStatus).toHaveBeenCalledTimes(1);
50
+ });
51
+
52
+ test("should return not connected status", async ({
53
+ mockFrakProviders,
54
+ }) => {
55
+ const mockStatus: WalletStatusReturnType = {
56
+ key: "not-connected",
57
+ };
58
+
59
+ vi.mocked(watchWalletStatus).mockResolvedValue(mockStatus);
60
+
61
+ const { result } = renderHook(() => useWalletStatus(), {
62
+ wrapper: mockFrakProviders,
63
+ });
64
+
65
+ await waitFor(() => {
66
+ expect(result.current.isSuccess).toBe(true);
67
+ });
68
+
69
+ expect(result.current.data).toEqual(mockStatus);
70
+ expect(result.current.data?.key).toBe("not-connected");
71
+ });
72
+
73
+ test("should handle RPC errors", async ({ mockFrakProviders }) => {
74
+ const error = new Error("Wallet status watch failed");
75
+ vi.mocked(watchWalletStatus).mockRejectedValue(error);
76
+
77
+ const { result } = renderHook(() => useWalletStatus(), {
78
+ wrapper: mockFrakProviders,
79
+ });
80
+
81
+ await waitFor(() => {
82
+ expect(result.current.isError).toBe(true);
83
+ });
84
+
85
+ expect(result.current.error).toEqual(error);
86
+ });
87
+
88
+ test("should pass callback to watchWalletStatus", async ({
89
+ mockFrakProviders,
90
+ mockFrakClient,
91
+ }) => {
92
+ const mockStatus: WalletStatusReturnType = {
93
+ key: "connected",
94
+ wallet: "0x1234567890123456789012345678901234567890",
95
+ };
96
+
97
+ vi.mocked(watchWalletStatus).mockResolvedValue(mockStatus);
98
+
99
+ const { result } = renderHook(() => useWalletStatus(), {
100
+ wrapper: mockFrakProviders,
101
+ });
102
+
103
+ await waitFor(() => {
104
+ expect(result.current.isSuccess).toBe(true);
105
+ });
106
+
107
+ expect(watchWalletStatus).toHaveBeenCalledWith(
108
+ mockFrakClient,
109
+ expect.any(Function)
110
+ );
111
+ });
112
+ });
@@ -0,0 +1,55 @@
1
+ import type { WalletStatusReturnType } from "@frak-labs/core-sdk";
2
+ import { watchWalletStatus } from "@frak-labs/core-sdk/actions";
3
+ import { ClientNotFound } from "@frak-labs/frame-connector";
4
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
5
+ import { useCallback } from "react";
6
+ import { useFrakClient } from "./useFrakClient";
7
+
8
+ /**
9
+ * Hook that return a query helping to get the current wallet status.
10
+ *
11
+ * It's a {@link @tanstack/react-query!home | `tanstack`} wrapper around the {@link @frak-labs/core-sdk!actions.watchWalletStatus | `watchWalletStatus()`} action
12
+ *
13
+ * @group hooks
14
+ *
15
+ * @returns
16
+ * The query hook wrapping the `watchWalletStatus()` action
17
+ * The `data` result is a {@link @frak-labs/core-sdk!index.WalletStatusReturnType | `WalletStatusReturnType`}
18
+ *
19
+ * @see {@link @frak-labs/core-sdk!actions.watchWalletStatus | `watchWalletStatus()`} for more info about the underlying action
20
+ * @see {@link @tanstack/react-query!useQuery | `useQuery()`} for more info about the useQuery response
21
+ */
22
+ export function useWalletStatus() {
23
+ const queryClient = useQueryClient();
24
+ const client = useFrakClient();
25
+
26
+ /**
27
+ * Callback hook when we receive an updated wallet status
28
+ */
29
+ const newStatusUpdated = useCallback(
30
+ (event: WalletStatusReturnType) => {
31
+ queryClient.setQueryData(
32
+ ["frak-sdk", "wallet-status-listener"],
33
+ event
34
+ );
35
+ },
36
+ [queryClient]
37
+ );
38
+
39
+ /**
40
+ * Setup the query listener
41
+ */
42
+ return useQuery<WalletStatusReturnType>({
43
+ gcTime: 0,
44
+ staleTime: 0,
45
+ queryKey: ["frak-sdk", "wallet-status-listener"],
46
+ queryFn: async () => {
47
+ if (!client) {
48
+ throw new ClientNotFound();
49
+ }
50
+
51
+ return watchWalletStatus(client, newStatusUpdated);
52
+ },
53
+ enabled: !!client,
54
+ });
55
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Tests for useFrakContext hook
3
+ * Tests hook that extracts and manages Frak context from URL
4
+ */
5
+
6
+ import { vi } from "vitest";
7
+
8
+ vi.mock("@frak-labs/core-sdk", async () => {
9
+ const actual = await vi.importActual<typeof import("@frak-labs/core-sdk")>(
10
+ "@frak-labs/core-sdk"
11
+ );
12
+ return {
13
+ ...actual,
14
+ FrakContextManager: {
15
+ parse: vi.fn(),
16
+ replaceUrl: vi.fn(),
17
+ },
18
+ };
19
+ });
20
+
21
+ vi.mock("./useWindowLocation");
22
+
23
+ import type { FrakContext } from "@frak-labs/core-sdk";
24
+ import { FrakContextManager } from "@frak-labs/core-sdk";
25
+ import { renderHook } from "@testing-library/react";
26
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
27
+ import { useFrakContext } from "./useFrakContext";
28
+ import { useWindowLocation } from "./useWindowLocation";
29
+
30
+ describe("useFrakContext", () => {
31
+ const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
32
+
33
+ beforeEach(() => {
34
+ vi.clearAllMocks();
35
+ });
36
+
37
+ afterEach(() => {
38
+ consoleLogSpy.mockClear();
39
+ });
40
+
41
+ test("should return null when no location href", () => {
42
+ vi.mocked(useWindowLocation).mockReturnValue({
43
+ location: undefined,
44
+ href: undefined,
45
+ });
46
+
47
+ const { result } = renderHook(() => useFrakContext());
48
+
49
+ expect(result.current.frakContext).toBeNull();
50
+ });
51
+
52
+ test("should parse frak context from URL", () => {
53
+ const mockContext: FrakContext = {
54
+ r: "0x1234567890123456789012345678901234567890",
55
+ };
56
+
57
+ vi.mocked(useWindowLocation).mockReturnValue({
58
+ location: { href: "https://example.com?frak=test" } as Location,
59
+ href: "https://example.com?frak=test",
60
+ });
61
+
62
+ vi.mocked(FrakContextManager.parse).mockReturnValue(mockContext);
63
+
64
+ const { result } = renderHook(() => useFrakContext());
65
+
66
+ expect(FrakContextManager.parse).toHaveBeenCalledWith({
67
+ url: "https://example.com?frak=test",
68
+ });
69
+ expect(result.current.frakContext).toEqual(mockContext);
70
+ });
71
+
72
+ test("should update context with new values", () => {
73
+ vi.mocked(useWindowLocation).mockReturnValue({
74
+ location: { href: "https://example.com" } as Location,
75
+ href: "https://example.com",
76
+ });
77
+
78
+ vi.mocked(FrakContextManager.parse).mockReturnValue(null);
79
+
80
+ const { result } = renderHook(() => useFrakContext());
81
+
82
+ const newContext: Partial<FrakContext> = {
83
+ r: "0x4567890123456789012345678901234567890123",
84
+ };
85
+
86
+ result.current.updateContext(newContext);
87
+
88
+ expect(console.log).toHaveBeenCalledWith("Updating context", {
89
+ newContext,
90
+ });
91
+ expect(FrakContextManager.replaceUrl).toHaveBeenCalledWith({
92
+ url: "https://example.com",
93
+ context: newContext,
94
+ });
95
+ });
96
+
97
+ test("should memoize frak context based on href", () => {
98
+ const mockContext: FrakContext = {
99
+ r: "0x7890123456789012345678901234567890123456",
100
+ };
101
+
102
+ vi.mocked(useWindowLocation).mockReturnValue({
103
+ location: { href: "https://example.com?test=1" } as Location,
104
+ href: "https://example.com?test=1",
105
+ });
106
+
107
+ vi.mocked(FrakContextManager.parse).mockReturnValue(mockContext);
108
+
109
+ const { result, rerender } = renderHook(() => useFrakContext());
110
+
111
+ const firstContext = result.current.frakContext;
112
+
113
+ // Rerender without changing href
114
+ rerender();
115
+
116
+ expect(result.current.frakContext).toBe(firstContext);
117
+ expect(FrakContextManager.parse).toHaveBeenCalledTimes(1);
118
+ });
119
+
120
+ test("should reparse context when href changes", () => {
121
+ const mockContext1: FrakContext = {
122
+ r: "0x1111111111111111111111111111111111111111",
123
+ };
124
+ const mockContext2: FrakContext = {
125
+ r: "0x2222222222222222222222222222222222222222",
126
+ };
127
+
128
+ const { rerender } = renderHook(() => useFrakContext());
129
+
130
+ vi.mocked(useWindowLocation).mockReturnValue({
131
+ location: { href: "https://example.com?v=1" } as Location,
132
+ href: "https://example.com?v=1",
133
+ });
134
+
135
+ vi.mocked(FrakContextManager.parse).mockReturnValue(mockContext1);
136
+
137
+ rerender();
138
+
139
+ expect(FrakContextManager.parse).toHaveBeenCalledWith({
140
+ url: "https://example.com?v=1",
141
+ });
142
+
143
+ // Change href
144
+ vi.mocked(useWindowLocation).mockReturnValue({
145
+ location: { href: "https://example.com?v=2" } as Location,
146
+ href: "https://example.com?v=2",
147
+ });
148
+
149
+ vi.mocked(FrakContextManager.parse).mockReturnValue(mockContext2);
150
+
151
+ rerender();
152
+
153
+ expect(FrakContextManager.parse).toHaveBeenCalledWith({
154
+ url: "https://example.com?v=2",
155
+ });
156
+ });
157
+ });
@@ -0,0 +1,42 @@
1
+ import { type FrakContext, FrakContextManager } from "@frak-labs/core-sdk";
2
+ import { useCallback, useMemo } from "react";
3
+ import { useWindowLocation } from "./useWindowLocation";
4
+
5
+ /**
6
+ * Extract the current frak context from the url
7
+ * @ignore
8
+ */
9
+ export function useFrakContext() {
10
+ // Get the current window location
11
+ const { location } = useWindowLocation();
12
+
13
+ /**
14
+ * Fetching and parsing the current frak context
15
+ */
16
+ const frakContext = useMemo(() => {
17
+ // If no url extracted yet, early exit
18
+ if (!location?.href) return null;
19
+
20
+ // Parse the current context
21
+ return FrakContextManager.parse({ url: location.href });
22
+ }, [location?.href]);
23
+
24
+ /**
25
+ * Update the current context
26
+ */
27
+ const updateContext = useCallback(
28
+ (newContext: Partial<FrakContext>) => {
29
+ console.log("Updating context", { newContext });
30
+ FrakContextManager.replaceUrl({
31
+ url: location?.href,
32
+ context: newContext,
33
+ });
34
+ },
35
+ [location?.href]
36
+ );
37
+
38
+ return {
39
+ frakContext,
40
+ updateContext,
41
+ };
42
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Tests for useMounted hook
3
+ * Tests that the hook correctly tracks component mount state
4
+ */
5
+
6
+ import { renderHook, waitFor } from "@testing-library/react";
7
+ import { describe, expect, it } from "../../../tests/vitest-fixtures";
8
+ import { useMounted } from "./useMounted";
9
+
10
+ describe("useMounted", () => {
11
+ it("should return true after mount", async () => {
12
+ const { result } = renderHook(() => useMounted());
13
+
14
+ // In React Testing Library, effects run synchronously after render
15
+ // So by the time we check result.current, the effect has already run
16
+ await waitFor(() => {
17
+ expect(result.current).toBe(true);
18
+ });
19
+ });
20
+
21
+ it("should remain true after multiple re-renders", async () => {
22
+ const { result, rerender } = renderHook(() => useMounted());
23
+
24
+ // Wait for initial mount
25
+ await waitFor(() => {
26
+ expect(result.current).toBe(true);
27
+ });
28
+
29
+ // Re-render multiple times
30
+ rerender();
31
+ expect(result.current).toBe(true);
32
+
33
+ rerender();
34
+ expect(result.current).toBe(true);
35
+
36
+ rerender();
37
+ expect(result.current).toBe(true);
38
+ });
39
+
40
+ it("should be stable across re-renders", async () => {
41
+ const { result, rerender } = renderHook(() => useMounted());
42
+
43
+ // Wait for mount
44
+ await waitFor(() => {
45
+ expect(result.current).toBe(true);
46
+ });
47
+
48
+ const firstValue = result.current;
49
+
50
+ // Re-render
51
+ rerender();
52
+ const secondValue = result.current;
53
+
54
+ // Values should be the same
55
+ expect(firstValue).toBe(secondValue);
56
+ expect(firstValue).toBe(true);
57
+ });
58
+
59
+ it("should handle unmounting gracefully", async () => {
60
+ const { result, unmount } = renderHook(() => useMounted());
61
+
62
+ // Wait for mount
63
+ await waitFor(() => {
64
+ expect(result.current).toBe(true);
65
+ });
66
+
67
+ // Unmount should not throw
68
+ expect(() => unmount()).not.toThrow();
69
+ });
70
+ });
@@ -0,0 +1,12 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ /** @ignore */
4
+ export function useMounted() {
5
+ const [mounted, setMounted] = useState(false);
6
+
7
+ useEffect(() => {
8
+ setMounted(true);
9
+ }, []);
10
+
11
+ return mounted;
12
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Tests for useWindowLocation hook
3
+ * Tests hook that tracks window.location changes
4
+ */
5
+
6
+ import { renderHook } from "@testing-library/react";
7
+ import { describe, expect, test, vi } from "vitest";
8
+ import { useWindowLocation } from "./useWindowLocation";
9
+
10
+ describe("useWindowLocation", () => {
11
+ test("should return current window location", () => {
12
+ const { result } = renderHook(() => useWindowLocation());
13
+
14
+ expect(result.current.location).toBeDefined();
15
+ expect(result.current.href).toBeDefined();
16
+ expect(typeof result.current.href).toBe("string");
17
+ });
18
+
19
+ test("should derive href from location", () => {
20
+ const { result } = renderHook(() => useWindowLocation());
21
+
22
+ if (result.current.location) {
23
+ expect(result.current.href).toBe(result.current.location.href);
24
+ }
25
+ });
26
+
27
+ test("should register popstate listener", () => {
28
+ const addEventListenerSpy = vi.spyOn(window, "addEventListener");
29
+
30
+ renderHook(() => useWindowLocation());
31
+
32
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
33
+ "popstate",
34
+ expect.any(Function)
35
+ );
36
+
37
+ addEventListenerSpy.mockRestore();
38
+ });
39
+
40
+ test("should cleanup popstate listener on unmount", () => {
41
+ const removeEventListenerSpy = vi.spyOn(window, "removeEventListener");
42
+
43
+ const { unmount } = renderHook(() => useWindowLocation());
44
+
45
+ unmount();
46
+
47
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
48
+ "popstate",
49
+ expect.any(Function)
50
+ );
51
+
52
+ removeEventListenerSpy.mockRestore();
53
+ });
54
+ });
@@ -0,0 +1,40 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { useMounted } from "./useMounted";
3
+
4
+ /**
5
+ * Returns the current window location and href
6
+ * @ignore
7
+ */
8
+ export function useWindowLocation(): {
9
+ location: Location | undefined;
10
+ href: string | undefined;
11
+ } {
12
+ const isMounted = useMounted();
13
+ const [location, setLocation] = useState<Location | undefined>(
14
+ isMounted ? window.location : undefined
15
+ );
16
+
17
+ useEffect(() => {
18
+ if (!isMounted) return;
19
+
20
+ // Method to the current window location
21
+ function setWindowLocation() {
22
+ setLocation(window.location);
23
+ }
24
+
25
+ if (!location) {
26
+ setWindowLocation();
27
+ }
28
+
29
+ window.addEventListener("popstate", setWindowLocation);
30
+
31
+ return () => {
32
+ window.removeEventListener("popstate", setWindowLocation);
33
+ };
34
+ }, [isMounted, location]);
35
+
36
+ // Derive the href from the location
37
+ const href = useMemo(() => location?.href, [location?.href]);
38
+
39
+ return { location, href };
40
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ // Providers export
2
+
3
+ // Hooks export
4
+ export {
5
+ useDisplayModal,
6
+ useFrakClient,
7
+ useFrakConfig,
8
+ useGetMerchantInformation,
9
+ useOpenSso,
10
+ usePrepareSso,
11
+ useReferralInteraction,
12
+ useSendTransactionAction,
13
+ useSiweAuthenticate,
14
+ useWalletStatus,
15
+ } from "./hook";
16
+ export type {
17
+ FrakConfigProviderProps,
18
+ FrakIFrameClientProps,
19
+ } from "./provider";
20
+ export {
21
+ FrakConfigContext,
22
+ FrakConfigProvider,
23
+ FrakIFrameClientContext,
24
+ FrakIFrameClientProvider,
25
+ } from "./provider";