@frak-labs/core-sdk 0.0.19 → 0.1.0-beta.00226d62

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 (148) hide show
  1. package/cdn/bundle.iife.js +14 -0
  2. package/dist/actions-CEEObPYc.js +1 -0
  3. package/dist/actions-DbQhWYx8.cjs +1 -0
  4. package/dist/actions.cjs +1 -1
  5. package/dist/actions.d.cts +3 -1400
  6. package/dist/actions.d.ts +3 -1400
  7. package/dist/actions.js +1 -1
  8. package/dist/bundle.cjs +1 -13
  9. package/dist/bundle.d.cts +6 -2022
  10. package/dist/bundle.d.ts +6 -2022
  11. package/dist/bundle.js +1 -13
  12. package/dist/index-7OZ39x1U.d.ts +195 -0
  13. package/dist/index-C6FxkWPC.d.cts +511 -0
  14. package/dist/index-UFX7xCg3.d.ts +351 -0
  15. package/dist/index-d8xS4ryI.d.ts +511 -0
  16. package/dist/index-p4FqSp8z.d.cts +351 -0
  17. package/dist/index-zDq-VlKx.d.cts +195 -0
  18. package/dist/index.cjs +1 -13
  19. package/dist/index.d.cts +4 -1373
  20. package/dist/index.d.ts +4 -1373
  21. package/dist/index.js +1 -13
  22. package/dist/interaction-DMJ3ZfaF.d.cts +45 -0
  23. package/dist/interaction-KX1h9a7V.d.ts +45 -0
  24. package/dist/interactions-DnfM3oe0.js +1 -0
  25. package/dist/interactions-EIXhNLf6.cjs +1 -0
  26. package/dist/interactions.cjs +1 -1
  27. package/dist/interactions.d.cts +2 -182
  28. package/dist/interactions.d.ts +2 -182
  29. package/dist/interactions.js +1 -1
  30. package/dist/openSso-D--Airj6.d.cts +1018 -0
  31. package/dist/openSso-DsKJ4y0j.d.ts +1018 -0
  32. package/dist/productTypes-BUkXJKZ7.cjs +1 -0
  33. package/dist/productTypes-CGb1MmBF.js +1 -0
  34. package/dist/src-B_xO0AR6.cjs +13 -0
  35. package/dist/src-D2d52OZa.js +13 -0
  36. package/dist/trackEvent-CHnYa85W.js +1 -0
  37. package/dist/trackEvent-GuQm_1Nm.cjs +1 -0
  38. package/package.json +27 -18
  39. package/src/actions/displayEmbeddedWallet.test.ts +194 -0
  40. package/src/actions/displayEmbeddedWallet.ts +20 -0
  41. package/src/actions/displayModal.test.ts +387 -0
  42. package/src/actions/displayModal.ts +131 -0
  43. package/src/actions/getProductInformation.test.ts +133 -0
  44. package/src/actions/getProductInformation.ts +14 -0
  45. package/src/actions/index.ts +29 -0
  46. package/src/actions/openSso.test.ts +407 -0
  47. package/src/actions/openSso.ts +116 -0
  48. package/src/actions/prepareSso.test.ts +223 -0
  49. package/src/actions/prepareSso.ts +48 -0
  50. package/src/actions/referral/processReferral.test.ts +357 -0
  51. package/src/actions/referral/processReferral.ts +230 -0
  52. package/src/actions/referral/referralInteraction.test.ts +153 -0
  53. package/src/actions/referral/referralInteraction.ts +57 -0
  54. package/src/actions/sendInteraction.test.ts +219 -0
  55. package/src/actions/sendInteraction.ts +32 -0
  56. package/src/actions/trackPurchaseStatus.test.ts +287 -0
  57. package/src/actions/trackPurchaseStatus.ts +53 -0
  58. package/src/actions/watchWalletStatus.test.ts +372 -0
  59. package/src/actions/watchWalletStatus.ts +94 -0
  60. package/src/actions/wrapper/modalBuilder.test.ts +253 -0
  61. package/src/actions/wrapper/modalBuilder.ts +212 -0
  62. package/src/actions/wrapper/sendTransaction.test.ts +164 -0
  63. package/src/actions/wrapper/sendTransaction.ts +62 -0
  64. package/src/actions/wrapper/siweAuthenticate.test.ts +290 -0
  65. package/src/actions/wrapper/siweAuthenticate.ts +94 -0
  66. package/src/bundle.ts +3 -0
  67. package/src/clients/DebugInfo.test.ts +418 -0
  68. package/src/clients/DebugInfo.ts +182 -0
  69. package/src/clients/createIFrameFrakClient.ts +287 -0
  70. package/src/clients/index.ts +3 -0
  71. package/src/clients/setupClient.test.ts +343 -0
  72. package/src/clients/setupClient.ts +73 -0
  73. package/src/clients/transports/iframeLifecycleManager.test.ts +399 -0
  74. package/src/clients/transports/iframeLifecycleManager.ts +90 -0
  75. package/src/constants/interactionTypes.test.ts +128 -0
  76. package/src/constants/interactionTypes.ts +44 -0
  77. package/src/constants/locales.ts +14 -0
  78. package/src/constants/productTypes.test.ts +130 -0
  79. package/src/constants/productTypes.ts +33 -0
  80. package/src/index.ts +101 -0
  81. package/src/interactions/index.ts +5 -0
  82. package/src/interactions/pressEncoder.test.ts +215 -0
  83. package/src/interactions/pressEncoder.ts +53 -0
  84. package/src/interactions/purchaseEncoder.test.ts +291 -0
  85. package/src/interactions/purchaseEncoder.ts +99 -0
  86. package/src/interactions/referralEncoder.test.ts +170 -0
  87. package/src/interactions/referralEncoder.ts +47 -0
  88. package/src/interactions/retailEncoder.test.ts +107 -0
  89. package/src/interactions/retailEncoder.ts +37 -0
  90. package/src/interactions/webshopEncoder.test.ts +56 -0
  91. package/src/interactions/webshopEncoder.ts +30 -0
  92. package/src/types/client.ts +14 -0
  93. package/src/types/compression.ts +22 -0
  94. package/src/types/config.ts +111 -0
  95. package/src/types/context.ts +13 -0
  96. package/src/types/index.ts +71 -0
  97. package/src/types/lifecycle/client.ts +46 -0
  98. package/src/types/lifecycle/iframe.ts +35 -0
  99. package/src/types/lifecycle/index.ts +2 -0
  100. package/src/types/rpc/displayModal.ts +84 -0
  101. package/src/types/rpc/embedded/index.ts +68 -0
  102. package/src/types/rpc/embedded/loggedIn.ts +55 -0
  103. package/src/types/rpc/embedded/loggedOut.ts +28 -0
  104. package/src/types/rpc/interaction.ts +43 -0
  105. package/src/types/rpc/modal/final.ts +46 -0
  106. package/src/types/rpc/modal/generic.ts +46 -0
  107. package/src/types/rpc/modal/index.ts +20 -0
  108. package/src/types/rpc/modal/login.ts +32 -0
  109. package/src/types/rpc/modal/openSession.ts +25 -0
  110. package/src/types/rpc/modal/siweAuthenticate.ts +37 -0
  111. package/src/types/rpc/modal/transaction.ts +33 -0
  112. package/src/types/rpc/productInformation.ts +59 -0
  113. package/src/types/rpc/sso.ts +80 -0
  114. package/src/types/rpc/walletStatus.ts +35 -0
  115. package/src/types/rpc.ts +158 -0
  116. package/src/types/transport.ts +34 -0
  117. package/src/utils/FrakContext.test.ts +407 -0
  118. package/src/utils/FrakContext.ts +158 -0
  119. package/src/utils/compression/b64.test.ts +181 -0
  120. package/src/utils/compression/b64.ts +29 -0
  121. package/src/utils/compression/compress.test.ts +123 -0
  122. package/src/utils/compression/compress.ts +11 -0
  123. package/src/utils/compression/decompress.test.ts +145 -0
  124. package/src/utils/compression/decompress.ts +11 -0
  125. package/src/utils/compression/index.ts +3 -0
  126. package/src/utils/computeProductId.test.ts +80 -0
  127. package/src/utils/computeProductId.ts +11 -0
  128. package/src/utils/constants.test.ts +23 -0
  129. package/src/utils/constants.ts +4 -0
  130. package/src/utils/formatAmount.test.ts +113 -0
  131. package/src/utils/formatAmount.ts +18 -0
  132. package/src/utils/getCurrencyAmountKey.test.ts +44 -0
  133. package/src/utils/getCurrencyAmountKey.ts +15 -0
  134. package/src/utils/getSupportedCurrency.test.ts +51 -0
  135. package/src/utils/getSupportedCurrency.ts +14 -0
  136. package/src/utils/getSupportedLocale.test.ts +64 -0
  137. package/src/utils/getSupportedLocale.ts +16 -0
  138. package/src/utils/iframeHelper.test.ts +450 -0
  139. package/src/utils/iframeHelper.ts +143 -0
  140. package/src/utils/index.ts +21 -0
  141. package/src/utils/sso.test.ts +361 -0
  142. package/src/utils/sso.ts +119 -0
  143. package/src/utils/ssoUrlListener.test.ts +252 -0
  144. package/src/utils/ssoUrlListener.ts +60 -0
  145. package/src/utils/trackEvent.test.ts +162 -0
  146. package/src/utils/trackEvent.ts +26 -0
  147. package/cdn/bundle.js +0 -19
  148. package/cdn/bundle.js.LICENSE.txt +0 -10
@@ -0,0 +1,252 @@
1
+ import type { RpcClient } from "@frak-labs/frame-connector";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ it,
8
+ vi,
9
+ } from "../../tests/vitest-fixtures";
10
+ import type { FrakLifecycleEvent } from "../types";
11
+ import type { IFrameRpcSchema } from "../types/rpc";
12
+ import { setupSsoUrlListener } from "./ssoUrlListener";
13
+
14
+ describe("setupSsoUrlListener", () => {
15
+ let mockRpcClient: RpcClient<IFrameRpcSchema, FrakLifecycleEvent>;
16
+ let mockWaitForConnection: Promise<boolean>;
17
+ let originalLocation: Location;
18
+ let originalHistory: History;
19
+ let consoleLogSpy: ReturnType<typeof vi.spyOn>;
20
+ let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
21
+
22
+ beforeEach(() => {
23
+ // Save original values
24
+ originalLocation = window.location;
25
+ originalHistory = window.history;
26
+
27
+ // Mock RPC client
28
+ mockRpcClient = {
29
+ sendLifecycle: vi.fn(),
30
+ } as any;
31
+
32
+ // Mock console methods
33
+ consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
34
+ consoleErrorSpy = vi
35
+ .spyOn(console, "error")
36
+ .mockImplementation(() => {});
37
+
38
+ // Mock history.replaceState
39
+ window.history.replaceState = vi.fn();
40
+ });
41
+
42
+ afterEach(() => {
43
+ vi.clearAllMocks();
44
+ consoleLogSpy.mockRestore();
45
+ consoleErrorSpy.mockRestore();
46
+
47
+ // Restore original values
48
+ Object.defineProperty(window, "location", {
49
+ value: originalLocation,
50
+ writable: true,
51
+ });
52
+ Object.defineProperty(window, "history", {
53
+ value: originalHistory,
54
+ writable: true,
55
+ });
56
+ });
57
+
58
+ it("should do nothing when window is undefined", () => {
59
+ const originalWindow = global.window;
60
+ // @ts-expect-error - intentionally removing window for test
61
+ delete global.window;
62
+
63
+ setupSsoUrlListener(mockRpcClient, Promise.resolve(true));
64
+
65
+ expect(mockRpcClient.sendLifecycle).not.toHaveBeenCalled();
66
+
67
+ // Restore window
68
+ global.window = originalWindow;
69
+ });
70
+
71
+ it("should do nothing when no SSO parameter in URL", () => {
72
+ Object.defineProperty(window, "location", {
73
+ value: {
74
+ href: "https://example.com/test",
75
+ },
76
+ writable: true,
77
+ });
78
+
79
+ setupSsoUrlListener(mockRpcClient, Promise.resolve(true));
80
+
81
+ expect(mockRpcClient.sendLifecycle).not.toHaveBeenCalled();
82
+ expect(window.history.replaceState).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it("should forward SSO data to iframe when connection is ready", async () => {
86
+ const compressedSso = "compressed-sso-data";
87
+ Object.defineProperty(window, "location", {
88
+ value: {
89
+ href: `https://example.com/test?sso=${compressedSso}`,
90
+ },
91
+ writable: true,
92
+ });
93
+
94
+ mockWaitForConnection = Promise.resolve(true);
95
+
96
+ setupSsoUrlListener(mockRpcClient, mockWaitForConnection);
97
+
98
+ await mockWaitForConnection;
99
+
100
+ // Wait for promise to resolve
101
+ await new Promise((resolve) => setTimeout(resolve, 0));
102
+
103
+ expect(mockRpcClient.sendLifecycle).toHaveBeenCalledWith({
104
+ clientLifecycle: "sso-redirect-complete",
105
+ data: { compressed: compressedSso },
106
+ });
107
+
108
+ expect(consoleLogSpy).toHaveBeenCalledWith(
109
+ "[SSO URL Listener] Forwarded compressed SSO data to iframe"
110
+ );
111
+ });
112
+
113
+ it("should clean URL immediately after detecting SSO parameter", async () => {
114
+ const compressedSso = "compressed-sso-data";
115
+ const originalUrl = `https://example.com/test?sso=${compressedSso}`;
116
+ Object.defineProperty(window, "location", {
117
+ value: {
118
+ href: originalUrl,
119
+ },
120
+ writable: true,
121
+ });
122
+
123
+ mockWaitForConnection = Promise.resolve(true);
124
+
125
+ setupSsoUrlListener(mockRpcClient, mockWaitForConnection);
126
+
127
+ // URL should be cleaned immediately, before connection resolves
128
+ expect(window.history.replaceState).toHaveBeenCalledWith(
129
+ {},
130
+ "",
131
+ "https://example.com/test"
132
+ );
133
+
134
+ expect(consoleLogSpy).toHaveBeenCalledWith(
135
+ "[SSO URL Listener] SSO parameter detected and URL cleaned"
136
+ );
137
+ });
138
+
139
+ it("should handle connection promise rejection", async () => {
140
+ const compressedSso = "compressed-sso-data";
141
+ Object.defineProperty(window, "location", {
142
+ value: {
143
+ href: `https://example.com/test?sso=${compressedSso}`,
144
+ },
145
+ writable: true,
146
+ });
147
+
148
+ const connectionError = new Error("Connection failed");
149
+ mockWaitForConnection = Promise.reject(connectionError);
150
+
151
+ setupSsoUrlListener(mockRpcClient, mockWaitForConnection);
152
+
153
+ await mockWaitForConnection.catch(() => {});
154
+
155
+ // Wait for promise to reject
156
+ await new Promise((resolve) => setTimeout(resolve, 0));
157
+
158
+ expect(mockRpcClient.sendLifecycle).not.toHaveBeenCalled();
159
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
160
+ "[SSO URL Listener] Failed to forward SSO data:",
161
+ connectionError
162
+ );
163
+ });
164
+
165
+ it("should handle URL with multiple parameters", async () => {
166
+ const compressedSso = "compressed-sso-data";
167
+ Object.defineProperty(window, "location", {
168
+ value: {
169
+ href: `https://example.com/test?param1=value1&sso=${compressedSso}&param2=value2`,
170
+ },
171
+ writable: true,
172
+ });
173
+
174
+ mockWaitForConnection = Promise.resolve(true);
175
+
176
+ setupSsoUrlListener(mockRpcClient, mockWaitForConnection);
177
+
178
+ await mockWaitForConnection;
179
+ await new Promise((resolve) => setTimeout(resolve, 0));
180
+
181
+ expect(mockRpcClient.sendLifecycle).toHaveBeenCalledWith({
182
+ clientLifecycle: "sso-redirect-complete",
183
+ data: { compressed: compressedSso },
184
+ });
185
+
186
+ // Should remove only the sso parameter
187
+ expect(window.history.replaceState).toHaveBeenCalledWith(
188
+ {},
189
+ "",
190
+ "https://example.com/test?param1=value1&param2=value2"
191
+ );
192
+ });
193
+
194
+ it("should not process empty SSO parameter", () => {
195
+ Object.defineProperty(window, "location", {
196
+ value: {
197
+ href: "https://example.com/test?sso=",
198
+ },
199
+ writable: true,
200
+ });
201
+
202
+ setupSsoUrlListener(mockRpcClient, Promise.resolve(true));
203
+
204
+ // Empty string is falsy, so it should not process
205
+ expect(window.history.replaceState).not.toHaveBeenCalled();
206
+ expect(mockRpcClient.sendLifecycle).not.toHaveBeenCalled();
207
+ });
208
+
209
+ it("should clean URL even if connection fails", async () => {
210
+ const compressedSso = "compressed-sso-data";
211
+ Object.defineProperty(window, "location", {
212
+ value: {
213
+ href: `https://example.com/test?sso=${compressedSso}`,
214
+ },
215
+ writable: true,
216
+ });
217
+
218
+ const connectionError = new Error("Connection failed");
219
+ mockWaitForConnection = Promise.reject(connectionError);
220
+
221
+ setupSsoUrlListener(mockRpcClient, mockWaitForConnection);
222
+
223
+ // URL should still be cleaned even if connection fails
224
+ expect(window.history.replaceState).toHaveBeenCalledWith(
225
+ {},
226
+ "",
227
+ "https://example.com/test"
228
+ );
229
+
230
+ await mockWaitForConnection.catch(() => {});
231
+ await new Promise((resolve) => setTimeout(resolve, 0));
232
+ });
233
+
234
+ it("should handle URL with hash", () => {
235
+ const compressedSso = "compressed-sso-data";
236
+ Object.defineProperty(window, "location", {
237
+ value: {
238
+ href: `https://example.com/test?sso=${compressedSso}#section`,
239
+ },
240
+ writable: true,
241
+ });
242
+
243
+ setupSsoUrlListener(mockRpcClient, Promise.resolve(true));
244
+
245
+ // Should preserve hash when cleaning URL
246
+ expect(window.history.replaceState).toHaveBeenCalledWith(
247
+ {},
248
+ "",
249
+ "https://example.com/test#section"
250
+ );
251
+ });
252
+ });
@@ -0,0 +1,60 @@
1
+ import type { RpcClient } from "@frak-labs/frame-connector";
2
+ import type { FrakLifecycleEvent } from "../types";
3
+ import type { IFrameRpcSchema } from "../types/rpc";
4
+
5
+ /**
6
+ * Listen for SSO redirect with compressed data in URL
7
+ * Forwards compressed data to iframe via lifecycle event
8
+ * Cleans URL immediately after detection
9
+ *
10
+ * Performance: One-shot URL check, no polling, no re-renders
11
+ *
12
+ * @param rpcClient - RPC client instance to send lifecycle events
13
+ * @param waitForConnection - Promise that resolves when iframe is connected
14
+ */
15
+ export function setupSsoUrlListener(
16
+ rpcClient: RpcClient<IFrameRpcSchema, FrakLifecycleEvent>,
17
+ waitForConnection: Promise<boolean>
18
+ ): void {
19
+ if (typeof window === "undefined") {
20
+ return;
21
+ }
22
+
23
+ // One-shot URL check - no need for MutationObserver or polling
24
+ const url = new URL(window.location.href);
25
+ const compressedSso = url.searchParams.get("sso");
26
+
27
+ // Early return if no SSO parameter
28
+ if (!compressedSso) {
29
+ return;
30
+ }
31
+
32
+ // Forward compressed data directly to iframe (no decompression on SDK side)
33
+ // Iframe will decompress and process
34
+ waitForConnection
35
+ .then(() => {
36
+ // Send lifecycle event with compressed string
37
+ // This is a one-way notification, no response expected
38
+ rpcClient.sendLifecycle({
39
+ clientLifecycle: "sso-redirect-complete",
40
+ data: { compressed: compressedSso },
41
+ });
42
+
43
+ console.log(
44
+ "[SSO URL Listener] Forwarded compressed SSO data to iframe"
45
+ );
46
+ })
47
+ .catch((error) => {
48
+ console.error(
49
+ "[SSO URL Listener] Failed to forward SSO data:",
50
+ error
51
+ );
52
+ });
53
+
54
+ // Clean URL immediately to prevent exposure in browser history
55
+ // Use replaceState to avoid navigation/re-render
56
+ url.searchParams.delete("sso");
57
+ window.history.replaceState({}, "", url.toString());
58
+
59
+ console.log("[SSO URL Listener] SSO parameter detected and URL cleaned");
60
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Tests for trackEvent utility function
3
+ * Tests OpenPanel event tracking wrapper
4
+ */
5
+
6
+ import {
7
+ afterEach,
8
+ beforeEach,
9
+ describe,
10
+ expect,
11
+ it,
12
+ vi,
13
+ } from "../../tests/vitest-fixtures";
14
+ import type { FrakClient } from "../types";
15
+ import { trackEvent } from "./trackEvent";
16
+
17
+ describe("trackEvent", () => {
18
+ let mockClient: FrakClient;
19
+ let consoleDebugSpy: any;
20
+
21
+ beforeEach(() => {
22
+ // Create mock client
23
+ mockClient = {
24
+ openPanel: {
25
+ track: vi.fn(),
26
+ },
27
+ } as unknown as FrakClient;
28
+
29
+ // Spy on console.debug
30
+ consoleDebugSpy = vi
31
+ .spyOn(console, "debug")
32
+ .mockImplementation(() => {});
33
+ });
34
+
35
+ afterEach(() => {
36
+ consoleDebugSpy.mockRestore();
37
+ });
38
+
39
+ describe("success cases", () => {
40
+ it("should track event with client", () => {
41
+ trackEvent(mockClient, "share_button_clicked");
42
+
43
+ expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
44
+ "share_button_clicked",
45
+ {}
46
+ );
47
+ });
48
+
49
+ it("should track event with props", () => {
50
+ const props = { userId: "123", page: "home" };
51
+ trackEvent(mockClient, "wallet_button_clicked", props);
52
+
53
+ expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
54
+ "wallet_button_clicked",
55
+ props
56
+ );
57
+ });
58
+
59
+ it("should track share_modal_error event", () => {
60
+ const props = { error: "Network error" };
61
+ trackEvent(mockClient, "share_modal_error", props);
62
+
63
+ expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
64
+ "share_modal_error",
65
+ props
66
+ );
67
+ });
68
+
69
+ it("should track user_referred event", () => {
70
+ trackEvent(mockClient, "user_referred");
71
+
72
+ expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
73
+ "user_referred",
74
+ {}
75
+ );
76
+ });
77
+ });
78
+
79
+ describe("without client", () => {
80
+ it("should not throw when client is undefined", () => {
81
+ expect(() => {
82
+ trackEvent(undefined, "share_button_clicked");
83
+ }).not.toThrow();
84
+ });
85
+
86
+ it("should log debug message when client is undefined", () => {
87
+ trackEvent(undefined, "wallet_button_clicked");
88
+
89
+ expect(consoleDebugSpy).toHaveBeenCalledWith(
90
+ "[Frak] No client provided, skipping event tracking"
91
+ );
92
+ });
93
+
94
+ it("should not call track when client is undefined", () => {
95
+ const trackMock = vi.fn();
96
+ const undefinedClient = undefined;
97
+
98
+ trackEvent(undefinedClient, "share_button_clicked");
99
+
100
+ expect(trackMock).not.toHaveBeenCalled();
101
+ });
102
+ });
103
+
104
+ describe("error handling", () => {
105
+ it("should catch and log errors from track()", () => {
106
+ const error = new Error("Track failed");
107
+ mockClient.openPanel = {
108
+ track: vi.fn().mockImplementation(() => {
109
+ throw error;
110
+ }),
111
+ } as any;
112
+
113
+ expect(() => {
114
+ trackEvent(mockClient, "share_button_clicked");
115
+ }).not.toThrow();
116
+
117
+ expect(consoleDebugSpy).toHaveBeenCalledWith(
118
+ "[Frak] Failed to track event:",
119
+ "share_button_clicked",
120
+ error
121
+ );
122
+ });
123
+
124
+ it("should not throw when openPanel is undefined", () => {
125
+ const clientWithoutPanel = {} as FrakClient;
126
+
127
+ expect(() => {
128
+ trackEvent(clientWithoutPanel, "wallet_button_clicked");
129
+ }).not.toThrow();
130
+ });
131
+ });
132
+
133
+ describe("edge cases", () => {
134
+ it("should handle empty props object", () => {
135
+ trackEvent(mockClient, "share_button_clicked", {});
136
+
137
+ expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
138
+ "share_button_clicked",
139
+ {}
140
+ );
141
+ });
142
+
143
+ it("should handle complex props object", () => {
144
+ const complexProps = {
145
+ userId: "123",
146
+ metadata: {
147
+ page: "home",
148
+ section: "header",
149
+ },
150
+ tags: ["tag1", "tag2"],
151
+ timestamp: Date.now(),
152
+ };
153
+
154
+ trackEvent(mockClient, "user_referred", complexProps);
155
+
156
+ expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
157
+ "user_referred",
158
+ complexProps
159
+ );
160
+ });
161
+ });
162
+ });
@@ -0,0 +1,26 @@
1
+ import type { FrakClient } from "../types";
2
+
3
+ export type FrakEvent =
4
+ | "share_button_clicked"
5
+ | "wallet_button_clicked"
6
+ | "share_modal_error"
7
+ | "user_referred";
8
+
9
+ type EventProps = Record<string, unknown>;
10
+
11
+ export function trackEvent(
12
+ client: FrakClient | undefined,
13
+ event: FrakEvent,
14
+ props: EventProps = {}
15
+ ): void {
16
+ if (!client) {
17
+ console.debug("[Frak] No client provided, skipping event tracking");
18
+ return;
19
+ }
20
+
21
+ try {
22
+ client.openPanel?.track(event, props);
23
+ } catch (e) {
24
+ console.debug("[Frak] Failed to track event:", event, e);
25
+ }
26
+ }