@frak-labs/react-sdk 0.1.1 → 0.2.0-beta.7898df5b

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 +29 -68
  4. package/dist/index.d.ts +28 -67
  5. package/dist/index.js +1 -1
  6. package/package.json +17 -16
  7. package/src/hook/helper/useReferralInteraction.test.ts +309 -0
  8. package/src/hook/helper/useReferralInteraction.ts +73 -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,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";
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Tests for FrakConfigProvider
3
+ * Tests that the provider correctly provides config to child components
4
+ */
5
+
6
+ import { render } from "@testing-library/react";
7
+ import React, { createElement } from "react";
8
+ import { describe, expect, test } from "../../tests/vitest-fixtures";
9
+ import { FrakConfigContext, FrakConfigProvider } from "./FrakConfigProvider";
10
+
11
+ describe("FrakConfigProvider", () => {
12
+ test("should render children", ({ mockFrakConfig }) => {
13
+ const { container } = render(
14
+ createElement(
15
+ FrakConfigProvider,
16
+ { config: mockFrakConfig },
17
+ createElement("div", { "data-testid": "child" }, "Test Child")
18
+ )
19
+ );
20
+
21
+ expect(
22
+ container.querySelector('[data-testid="child"]')
23
+ ).toBeInTheDocument();
24
+ expect(container.textContent).toContain("Test Child");
25
+ });
26
+
27
+ test("should provide config to context consumers", ({ mockFrakConfig }) => {
28
+ let receivedConfig: any;
29
+
30
+ const Consumer = () => {
31
+ const config = React.useContext(FrakConfigContext);
32
+ receivedConfig = config;
33
+ return null;
34
+ };
35
+
36
+ render(
37
+ createElement(
38
+ FrakConfigProvider,
39
+ { config: mockFrakConfig },
40
+ createElement(Consumer)
41
+ )
42
+ );
43
+
44
+ expect(receivedConfig).toBeDefined();
45
+ expect(receivedConfig.domain).toBe(mockFrakConfig.domain);
46
+ });
47
+
48
+ test("should apply default walletUrl", () => {
49
+ const configWithoutWalletUrl = {
50
+ domain: "example.com",
51
+ metadata: {
52
+ name: "Test App",
53
+ },
54
+ };
55
+
56
+ let receivedConfig: any;
57
+
58
+ const Consumer = () => {
59
+ const config = React.useContext(FrakConfigContext);
60
+ receivedConfig = config;
61
+ return null;
62
+ };
63
+
64
+ render(
65
+ createElement(
66
+ FrakConfigProvider,
67
+ { config: configWithoutWalletUrl },
68
+ createElement(Consumer)
69
+ )
70
+ );
71
+
72
+ expect(receivedConfig.walletUrl).toBe("https://wallet.frak.id");
73
+ });
74
+
75
+ test("should preserve custom walletUrl", ({ mockFrakConfig }) => {
76
+ let receivedConfig: any;
77
+
78
+ const Consumer = () => {
79
+ const config = React.useContext(FrakConfigContext);
80
+ receivedConfig = config;
81
+ return null;
82
+ };
83
+
84
+ render(
85
+ createElement(
86
+ FrakConfigProvider,
87
+ { config: mockFrakConfig },
88
+ createElement(Consumer)
89
+ )
90
+ );
91
+
92
+ expect(receivedConfig.walletUrl).toBe(mockFrakConfig.walletUrl);
93
+ });
94
+
95
+ test("should fallback domain to window.location.host", () => {
96
+ const configWithoutDomain = {
97
+ metadata: {
98
+ name: "Test App",
99
+ },
100
+ };
101
+
102
+ let receivedConfig: any;
103
+
104
+ const Consumer = () => {
105
+ const config = React.useContext(FrakConfigContext);
106
+ receivedConfig = config;
107
+ return null;
108
+ };
109
+
110
+ render(
111
+ createElement(
112
+ FrakConfigProvider,
113
+ { config: configWithoutDomain as any },
114
+ createElement(Consumer)
115
+ )
116
+ );
117
+
118
+ // In test environment, window.location.host is "localhost:3000" (JSDOM default)
119
+ expect(receivedConfig.domain).toBe(window.location.host);
120
+ });
121
+
122
+ test("should pass through all config properties", ({ mockFrakConfig }) => {
123
+ let receivedConfig: any;
124
+
125
+ const Consumer = () => {
126
+ const config = React.useContext(FrakConfigContext);
127
+ receivedConfig = config;
128
+ return null;
129
+ };
130
+
131
+ render(
132
+ createElement(
133
+ FrakConfigProvider,
134
+ { config: mockFrakConfig },
135
+ createElement(Consumer)
136
+ )
137
+ );
138
+
139
+ expect(receivedConfig.domain).toBe(mockFrakConfig.domain);
140
+ expect(receivedConfig.walletUrl).toBe(mockFrakConfig.walletUrl);
141
+ expect(receivedConfig.metadata).toEqual(mockFrakConfig.metadata);
142
+ expect(receivedConfig.customizations).toEqual(
143
+ mockFrakConfig.customizations
144
+ );
145
+ });
146
+
147
+ test("should handle minimal config", () => {
148
+ const minimalConfig = {
149
+ domain: "minimal.com",
150
+ metadata: {
151
+ name: "Minimal Test App",
152
+ },
153
+ };
154
+
155
+ let receivedConfig: any;
156
+
157
+ const Consumer = () => {
158
+ const config = React.useContext(FrakConfigContext);
159
+ receivedConfig = config;
160
+ return null;
161
+ };
162
+
163
+ render(
164
+ createElement(
165
+ FrakConfigProvider,
166
+ { config: minimalConfig },
167
+ createElement(Consumer)
168
+ )
169
+ );
170
+
171
+ expect(receivedConfig.domain).toBe("minimal.com");
172
+ expect(receivedConfig.walletUrl).toBe("https://wallet.frak.id");
173
+ expect(receivedConfig.metadata.name).toBe("Minimal Test App");
174
+ });
175
+
176
+ test("should support nested providers with different configs", () => {
177
+ const outerConfig = {
178
+ domain: "outer.com",
179
+ walletUrl: "https://wallet-outer.frak.id",
180
+ metadata: {
181
+ name: "Outer App",
182
+ },
183
+ };
184
+
185
+ const innerConfig = {
186
+ domain: "inner.com",
187
+ walletUrl: "https://wallet-inner.frak.id",
188
+ metadata: {
189
+ name: "Inner App",
190
+ },
191
+ };
192
+
193
+ let outerReceivedConfig: any;
194
+ let innerReceivedConfig: any;
195
+
196
+ const OuterConsumer = () => {
197
+ const config = React.useContext(FrakConfigContext);
198
+ outerReceivedConfig = config;
199
+ return null;
200
+ };
201
+
202
+ const InnerConsumer = () => {
203
+ const config = React.useContext(FrakConfigContext);
204
+ innerReceivedConfig = config;
205
+ return null;
206
+ };
207
+
208
+ render(
209
+ createElement(
210
+ FrakConfigProvider,
211
+ { config: outerConfig },
212
+ createElement(OuterConsumer),
213
+ createElement(
214
+ FrakConfigProvider,
215
+ { config: innerConfig },
216
+ createElement(InnerConsumer)
217
+ )
218
+ )
219
+ );
220
+
221
+ expect(outerReceivedConfig.domain).toBe("outer.com");
222
+ expect(innerReceivedConfig.domain).toBe("inner.com");
223
+ });
224
+
225
+ test("should render multiple children", ({ mockFrakConfig }) => {
226
+ const { container } = render(
227
+ createElement(
228
+ FrakConfigProvider,
229
+ { config: mockFrakConfig },
230
+ createElement("div", { "data-testid": "child1" }, "Child 1"),
231
+ createElement("div", { "data-testid": "child2" }, "Child 2"),
232
+ createElement("div", { "data-testid": "child3" }, "Child 3")
233
+ )
234
+ );
235
+
236
+ expect(
237
+ container.querySelector('[data-testid="child1"]')
238
+ ).toBeInTheDocument();
239
+ expect(
240
+ container.querySelector('[data-testid="child2"]')
241
+ ).toBeInTheDocument();
242
+ expect(
243
+ container.querySelector('[data-testid="child3"]')
244
+ ).toBeInTheDocument();
245
+ });
246
+ });
@@ -0,0 +1,54 @@
1
+ import type { FrakWalletSdkConfig } from "@frak-labs/core-sdk";
2
+ import { createContext, createElement, type PropsWithChildren } from "react";
3
+
4
+ /**
5
+ * The context that will keep the Frak Wallet SDK configuration
6
+ * @ignore
7
+ */
8
+ export const FrakConfigContext = createContext<FrakWalletSdkConfig | undefined>(
9
+ undefined
10
+ );
11
+
12
+ /**
13
+ * Props to instantiate the Frak Wallet SDK configuration provider
14
+ *
15
+ * @group provider
16
+ */
17
+ export type FrakConfigProviderProps = {
18
+ /**
19
+ * The wanted Frak configuration
20
+ * @see {@link @frak-labs/core-sdk!index.FrakWalletSdkConfig | FrakWalletSdkConfig}
21
+ */
22
+ config: FrakWalletSdkConfig;
23
+ };
24
+
25
+ /**
26
+ * Simple config provider for the Frak Wallet SDK
27
+ *
28
+ * Should be wrapped within a {@link @tanstack/react-query!QueryClientProvider | `QueryClientProvider`}
29
+ *
30
+ * @group provider
31
+ *
32
+ * @param parameters
33
+ */
34
+ export function FrakConfigProvider(
35
+ parameters: PropsWithChildren<FrakConfigProviderProps>
36
+ ) {
37
+ const { children, config } = parameters;
38
+ return createElement(
39
+ FrakConfigContext.Provider,
40
+ {
41
+ value: {
42
+ ...config,
43
+ walletUrl: config.walletUrl ?? "https://wallet.frak.id",
44
+ domain:
45
+ config.domain ??
46
+ (typeof window !== "undefined"
47
+ ? window?.location?.host
48
+ : undefined) ??
49
+ "not-found",
50
+ },
51
+ },
52
+ children
53
+ );
54
+ }