@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,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
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Tests for FrakIFrameClientProvider
3
+ * Tests iframe creation and FrakClient provider
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
+ createIFrameFrakClient: vi.fn(),
15
+ };
16
+ });
17
+
18
+ import type { FrakClient } from "@frak-labs/core-sdk";
19
+ import { createIFrameFrakClient } from "@frak-labs/core-sdk";
20
+ import { render, waitFor } from "@testing-library/react";
21
+ import { createElement, type ReactNode } from "react";
22
+ import { describe, expect, test } from "../../tests/vitest-fixtures";
23
+ import { FrakConfigProvider } from "./FrakConfigProvider";
24
+ import { FrakIFrameClientProvider } from "./FrakIFrameClientProvider";
25
+
26
+ describe("FrakIFrameClientProvider", () => {
27
+ test("should render iframe with correct src", ({ mockFrakConfig }) => {
28
+ const Wrapper = ({ children }: { children: ReactNode }) =>
29
+ createElement(
30
+ FrakConfigProvider,
31
+ { config: mockFrakConfig },
32
+ children
33
+ );
34
+
35
+ render(createElement(FrakIFrameClientProvider), {
36
+ wrapper: Wrapper,
37
+ });
38
+
39
+ const iframe = document.querySelector("iframe");
40
+ expect(iframe).toBeDefined();
41
+ expect(iframe?.src).toContain(`${mockFrakConfig.walletUrl}/listener`);
42
+ });
43
+
44
+ test("should apply custom styles to iframe", ({ mockFrakConfig }) => {
45
+ const Wrapper = ({ children }: { children: ReactNode }) =>
46
+ createElement(
47
+ FrakConfigProvider,
48
+ { config: mockFrakConfig },
49
+ children
50
+ );
51
+
52
+ const customStyle = {
53
+ width: "500px",
54
+ height: "600px",
55
+ };
56
+
57
+ render(
58
+ createElement(FrakIFrameClientProvider, { style: customStyle }),
59
+ {
60
+ wrapper: Wrapper,
61
+ }
62
+ );
63
+
64
+ const iframe = document.querySelector("iframe");
65
+ expect(iframe?.style.width).toBe("500px");
66
+ expect(iframe?.style.height).toBe("600px");
67
+ });
68
+
69
+ test("should create FrakClient when iframe ref is set", async ({
70
+ mockFrakConfig,
71
+ }) => {
72
+ const Wrapper = ({ children }: { children: ReactNode }) =>
73
+ createElement(
74
+ FrakConfigProvider,
75
+ { config: mockFrakConfig },
76
+ children
77
+ );
78
+
79
+ const mockClient = { config: mockFrakConfig } as FrakClient;
80
+ vi.mocked(createIFrameFrakClient).mockReturnValue(mockClient);
81
+
82
+ render(createElement(FrakIFrameClientProvider), {
83
+ wrapper: Wrapper,
84
+ });
85
+
86
+ await waitFor(() => {
87
+ expect(createIFrameFrakClient).toHaveBeenCalled();
88
+ });
89
+ });
90
+
91
+ test("should not recreate client if already exists", async ({
92
+ mockFrakConfig,
93
+ }) => {
94
+ const Wrapper = ({ children }: { children: ReactNode }) =>
95
+ createElement(
96
+ FrakConfigProvider,
97
+ { config: mockFrakConfig },
98
+ children
99
+ );
100
+
101
+ const mockClient = { config: mockFrakConfig } as FrakClient;
102
+ vi.mocked(createIFrameFrakClient).mockReturnValue(mockClient);
103
+
104
+ const { rerender } = render(createElement(FrakIFrameClientProvider), {
105
+ wrapper: Wrapper,
106
+ });
107
+
108
+ await waitFor(() => {
109
+ expect(createIFrameFrakClient).toHaveBeenCalledTimes(1);
110
+ });
111
+
112
+ // Rerender the component
113
+ rerender(createElement(FrakIFrameClientProvider));
114
+
115
+ // Should still only be called once
116
+ expect(createIFrameFrakClient).toHaveBeenCalledTimes(1);
117
+ });
118
+
119
+ test("should render without children", ({ mockFrakConfig }) => {
120
+ const Wrapper = ({ children }: { children: ReactNode }) =>
121
+ createElement(
122
+ FrakConfigProvider,
123
+ { config: mockFrakConfig },
124
+ children
125
+ );
126
+
127
+ const { container } = render(createElement(FrakIFrameClientProvider), {
128
+ wrapper: Wrapper,
129
+ });
130
+
131
+ const iframe = container.querySelector("iframe");
132
+ expect(iframe).toBeDefined();
133
+ });
134
+
135
+ test("should use baseIframeProps for iframe attributes", ({
136
+ mockFrakConfig,
137
+ }) => {
138
+ const Wrapper = ({ children }: { children: ReactNode }) =>
139
+ createElement(
140
+ FrakConfigProvider,
141
+ { config: mockFrakConfig },
142
+ children
143
+ );
144
+
145
+ render(createElement(FrakIFrameClientProvider), {
146
+ wrapper: Wrapper,
147
+ });
148
+
149
+ const iframe = document.querySelector("iframe");
150
+ expect(iframe).toBeDefined();
151
+ // baseIframeProps should set these attributes
152
+ expect(iframe?.getAttribute("sandbox")).toBeDefined();
153
+ });
154
+
155
+ test("should handle iframe ref callback correctly", ({
156
+ mockFrakConfig,
157
+ }) => {
158
+ const Wrapper = ({ children }: { children: ReactNode }) =>
159
+ createElement(
160
+ FrakConfigProvider,
161
+ { config: mockFrakConfig },
162
+ children
163
+ );
164
+
165
+ const mockClient = { config: mockFrakConfig } as FrakClient;
166
+ let callCount = 0;
167
+
168
+ vi.mocked(createIFrameFrakClient).mockImplementation(() => {
169
+ callCount++;
170
+ return mockClient;
171
+ });
172
+
173
+ render(createElement(FrakIFrameClientProvider), {
174
+ wrapper: Wrapper,
175
+ });
176
+
177
+ // Client creation should be called at most once
178
+ expect(callCount).toBeLessThanOrEqual(1);
179
+ });
180
+
181
+ test("should pass config to createIFrameFrakClient", async ({
182
+ mockFrakConfig,
183
+ }) => {
184
+ const Wrapper = ({ children }: { children: ReactNode }) =>
185
+ createElement(
186
+ FrakConfigProvider,
187
+ { config: mockFrakConfig },
188
+ children
189
+ );
190
+
191
+ const mockClient = { config: mockFrakConfig } as FrakClient;
192
+ vi.mocked(createIFrameFrakClient).mockReturnValue(mockClient);
193
+
194
+ render(createElement(FrakIFrameClientProvider), {
195
+ wrapper: Wrapper,
196
+ });
197
+
198
+ await waitFor(() => {
199
+ expect(createIFrameFrakClient).toHaveBeenCalledWith(
200
+ expect.objectContaining({
201
+ config: expect.objectContaining({
202
+ domain: "example.com",
203
+ walletUrl: "https://wallet-test.frak.id",
204
+ }),
205
+ })
206
+ );
207
+ });
208
+ });
209
+ });
@@ -0,0 +1,86 @@
1
+ import {
2
+ baseIframeProps,
3
+ createIFrameFrakClient,
4
+ type FrakClient,
5
+ type FrakWalletSdkConfig,
6
+ } from "@frak-labs/core-sdk";
7
+ import {
8
+ type CSSProperties,
9
+ createContext,
10
+ createElement,
11
+ Fragment,
12
+ type ReactNode,
13
+ useState,
14
+ } from "react";
15
+ import { useFrakConfig } from "../hook";
16
+
17
+ /**
18
+ * The context that will keep the Frak Wallet SDK client
19
+ * @ignore
20
+ */
21
+ export const FrakIFrameClientContext = createContext<FrakClient | undefined>(
22
+ undefined
23
+ );
24
+
25
+ /**
26
+ * Props to instantiate the Frak Wallet SDK configuration provider
27
+ *
28
+ * @group provider
29
+ */
30
+ export type FrakIFrameClientProps = {
31
+ config: FrakWalletSdkConfig;
32
+ };
33
+
34
+ /**
35
+ * IFrame client provider for the Frak Wallet SDK
36
+ * It will automatically create the frak wallet iFrame (required for the wallet to communicate with the SDK securely), and provide it in the context
37
+ *
38
+ * @group provider
39
+ *
40
+ * @remarks
41
+ * This provider must be wrapped within a {@link FrakConfigProvider} to work properly
42
+ *
43
+ * @param args
44
+ * @param args.style - Some custom styles to apply to the iFrame
45
+ * @param args.children - Descedant components that will have access to the Frak Client
46
+ */
47
+ export function FrakIFrameClientProvider({
48
+ style,
49
+ children,
50
+ }: {
51
+ style?: CSSProperties;
52
+ children?: ReactNode;
53
+ }) {
54
+ const config = useFrakConfig();
55
+
56
+ // Using a state for the client since using directly a client built inside the ref cause re-render loop
57
+ const [client, setClient] = useState<FrakClient | undefined>(undefined);
58
+
59
+ // Create the iframe that will be used to communicate with the wallet
60
+ const iFrame = createElement("iframe", {
61
+ ...baseIframeProps,
62
+ src: `${config.walletUrl}/listener`,
63
+ style: style ?? baseIframeProps.style,
64
+ ref: (iframe: HTMLIFrameElement) => {
65
+ if (!iframe || client) {
66
+ return;
67
+ }
68
+ setClient(
69
+ createIFrameFrakClient({
70
+ iframe,
71
+ config,
72
+ })
73
+ );
74
+ },
75
+ });
76
+
77
+ // Create the component that will provide the client
78
+ const providerComponent = createElement(
79
+ FrakIFrameClientContext.Provider,
80
+ { value: client },
81
+ children
82
+ );
83
+
84
+ // Return both components
85
+ return createElement(Fragment, null, iFrame, providerComponent);
86
+ }
@@ -0,0 +1,7 @@
1
+ export type { FrakConfigProviderProps } from "./FrakConfigProvider";
2
+ export { FrakConfigContext, FrakConfigProvider } from "./FrakConfigProvider";
3
+ export type { FrakIFrameClientProps } from "./FrakIFrameClientProvider";
4
+ export {
5
+ FrakIFrameClientContext,
6
+ FrakIFrameClientProvider,
7
+ } from "./FrakIFrameClientProvider";