@frak-labs/core-sdk 0.1.0 → 0.1.1-beta.2c88e0f4

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 (130) hide show
  1. package/README.md +58 -0
  2. package/cdn/bundle.js +3 -8
  3. package/dist/actions.cjs +1 -1
  4. package/dist/actions.d.cts +3 -1400
  5. package/dist/actions.d.ts +3 -1400
  6. package/dist/actions.js +1 -1
  7. package/dist/bundle.cjs +1 -13
  8. package/dist/bundle.d.cts +4 -1927
  9. package/dist/bundle.d.ts +4 -1927
  10. package/dist/bundle.js +1 -13
  11. package/dist/computeLegacyProductId-B-2-L7VT.d.cts +538 -0
  12. package/dist/computeLegacyProductId-M9k7vnUf.d.ts +538 -0
  13. package/dist/index.cjs +1 -13
  14. package/dist/index.d.cts +3 -1269
  15. package/dist/index.d.ts +3 -1269
  16. package/dist/index.js +1 -13
  17. package/dist/openSso-DTFns_5E.d.cts +1022 -0
  18. package/dist/openSso-DXL8K2tc.d.ts +1022 -0
  19. package/dist/setupClient--zfx2pFM.cjs +13 -0
  20. package/dist/setupClient-CFQDaGKh.js +13 -0
  21. package/dist/siweAuthenticate-BxRXLOpn.d.ts +511 -0
  22. package/dist/siweAuthenticate-D_AfTjZY.d.cts +511 -0
  23. package/dist/siweAuthenticate-Dfu_kubw.js +1 -0
  24. package/dist/siweAuthenticate-o0RXILdG.cjs +1 -0
  25. package/dist/trackEvent-B0S5E6_h.js +1 -0
  26. package/dist/trackEvent-BbX2d3g_.cjs +1 -0
  27. package/package.json +25 -31
  28. package/src/actions/displayEmbeddedWallet.test.ts +194 -0
  29. package/src/actions/displayEmbeddedWallet.ts +21 -0
  30. package/src/actions/displayModal.test.ts +388 -0
  31. package/src/actions/displayModal.ts +120 -0
  32. package/src/actions/getMerchantInformation.test.ts +116 -0
  33. package/src/actions/getMerchantInformation.ts +16 -0
  34. package/src/actions/index.ts +29 -0
  35. package/src/actions/openSso.ts +116 -0
  36. package/src/actions/prepareSso.test.ts +223 -0
  37. package/src/actions/prepareSso.ts +48 -0
  38. package/src/actions/referral/processReferral.test.ts +248 -0
  39. package/src/actions/referral/processReferral.ts +232 -0
  40. package/src/actions/referral/referralInteraction.test.ts +147 -0
  41. package/src/actions/referral/referralInteraction.ts +52 -0
  42. package/src/actions/sendInteraction.ts +56 -0
  43. package/src/actions/trackPurchaseStatus.test.ts +500 -0
  44. package/src/actions/trackPurchaseStatus.ts +90 -0
  45. package/src/actions/watchWalletStatus.test.ts +372 -0
  46. package/src/actions/watchWalletStatus.ts +93 -0
  47. package/src/actions/wrapper/modalBuilder.test.ts +239 -0
  48. package/src/actions/wrapper/modalBuilder.ts +203 -0
  49. package/src/actions/wrapper/sendTransaction.test.ts +164 -0
  50. package/src/actions/wrapper/sendTransaction.ts +62 -0
  51. package/src/actions/wrapper/siweAuthenticate.test.ts +290 -0
  52. package/src/actions/wrapper/siweAuthenticate.ts +94 -0
  53. package/src/bundle.ts +2 -0
  54. package/src/clients/DebugInfo.test.ts +418 -0
  55. package/src/clients/DebugInfo.ts +182 -0
  56. package/src/clients/createIFrameFrakClient.ts +290 -0
  57. package/src/clients/index.ts +3 -0
  58. package/src/clients/setupClient.test.ts +343 -0
  59. package/src/clients/setupClient.ts +73 -0
  60. package/src/clients/transports/iframeLifecycleManager.test.ts +558 -0
  61. package/src/clients/transports/iframeLifecycleManager.ts +229 -0
  62. package/src/constants/interactionTypes.ts +15 -0
  63. package/src/constants/locales.ts +14 -0
  64. package/src/index.ts +109 -0
  65. package/src/types/client.ts +14 -0
  66. package/src/types/compression.ts +22 -0
  67. package/src/types/config.ts +117 -0
  68. package/src/types/context.ts +13 -0
  69. package/src/types/index.ts +74 -0
  70. package/src/types/lifecycle/client.ts +69 -0
  71. package/src/types/lifecycle/iframe.ts +41 -0
  72. package/src/types/lifecycle/index.ts +2 -0
  73. package/src/types/rpc/displayModal.ts +82 -0
  74. package/src/types/rpc/embedded/index.ts +68 -0
  75. package/src/types/rpc/embedded/loggedIn.ts +55 -0
  76. package/src/types/rpc/embedded/loggedOut.ts +28 -0
  77. package/src/types/rpc/interaction.ts +30 -0
  78. package/src/types/rpc/merchantInformation.ts +77 -0
  79. package/src/types/rpc/modal/final.ts +46 -0
  80. package/src/types/rpc/modal/generic.ts +46 -0
  81. package/src/types/rpc/modal/index.ts +16 -0
  82. package/src/types/rpc/modal/login.ts +36 -0
  83. package/src/types/rpc/modal/siweAuthenticate.ts +37 -0
  84. package/src/types/rpc/modal/transaction.ts +33 -0
  85. package/src/types/rpc/sso.ts +80 -0
  86. package/src/types/rpc/walletStatus.ts +29 -0
  87. package/src/types/rpc.ts +150 -0
  88. package/src/types/tracking.ts +60 -0
  89. package/src/types/transport.ts +34 -0
  90. package/src/utils/FrakContext.test.ts +407 -0
  91. package/src/utils/FrakContext.ts +158 -0
  92. package/src/utils/backendUrl.test.ts +83 -0
  93. package/src/utils/backendUrl.ts +62 -0
  94. package/src/utils/clientId.test.ts +41 -0
  95. package/src/utils/clientId.ts +40 -0
  96. package/src/utils/compression/b64.test.ts +181 -0
  97. package/src/utils/compression/b64.ts +29 -0
  98. package/src/utils/compression/compress.test.ts +123 -0
  99. package/src/utils/compression/compress.ts +11 -0
  100. package/src/utils/compression/decompress.test.ts +149 -0
  101. package/src/utils/compression/decompress.ts +11 -0
  102. package/src/utils/compression/index.ts +3 -0
  103. package/src/utils/computeLegacyProductId.ts +11 -0
  104. package/src/utils/constants.test.ts +23 -0
  105. package/src/utils/constants.ts +9 -0
  106. package/src/utils/deepLinkWithFallback.test.ts +243 -0
  107. package/src/utils/deepLinkWithFallback.ts +103 -0
  108. package/src/utils/formatAmount.test.ts +113 -0
  109. package/src/utils/formatAmount.ts +24 -0
  110. package/src/utils/getCurrencyAmountKey.test.ts +44 -0
  111. package/src/utils/getCurrencyAmountKey.ts +15 -0
  112. package/src/utils/getSupportedCurrency.test.ts +51 -0
  113. package/src/utils/getSupportedCurrency.ts +14 -0
  114. package/src/utils/getSupportedLocale.test.ts +64 -0
  115. package/src/utils/getSupportedLocale.ts +16 -0
  116. package/src/utils/iframeHelper.test.ts +450 -0
  117. package/src/utils/iframeHelper.ts +147 -0
  118. package/src/utils/index.ts +36 -0
  119. package/src/utils/merchantId.test.ts +653 -0
  120. package/src/utils/merchantId.ts +143 -0
  121. package/src/utils/sso.ts +127 -0
  122. package/src/utils/ssoUrlListener.test.ts +252 -0
  123. package/src/utils/ssoUrlListener.ts +60 -0
  124. package/src/utils/trackEvent.test.ts +180 -0
  125. package/src/utils/trackEvent.ts +41 -0
  126. package/cdn/bundle.js.LICENSE.txt +0 -10
  127. package/dist/interactions.cjs +0 -1
  128. package/dist/interactions.d.cts +0 -182
  129. package/dist/interactions.d.ts +0 -182
  130. package/dist/interactions.js +0 -1
@@ -0,0 +1,290 @@
1
+ import {
2
+ createRpcClient,
3
+ FrakRpcError,
4
+ type RpcClient,
5
+ RpcErrorCodes,
6
+ } from "@frak-labs/frame-connector";
7
+ import { OpenPanel } from "@openpanel/web";
8
+ import type { FrakLifecycleEvent } from "../types";
9
+ import type { FrakClient } from "../types/client";
10
+ import type { FrakWalletSdkConfig } from "../types/config";
11
+ import type { IFrameRpcSchema } from "../types/rpc";
12
+ import { BACKUP_KEY } from "../utils/constants";
13
+ import { setupSsoUrlListener } from "../utils/ssoUrlListener";
14
+
15
+ import { DebugInfoGatherer } from "./DebugInfo";
16
+ import {
17
+ createIFrameLifecycleManager,
18
+ type IframeLifecycleManager,
19
+ } from "./transports/iframeLifecycleManager";
20
+
21
+ type SdkRpcClient = RpcClient<IFrameRpcSchema, FrakLifecycleEvent>;
22
+
23
+ /**
24
+ * Create a new iframe Frak client
25
+ * @param args
26
+ * @param args.config - The configuration to use for the Frak Wallet SDK.
27
+ * When `config.domain` is set, it is forwarded to the iframe handshake so the listener resolves the correct merchant in tunneled/proxied environments (e.g. Shopify dev with Cloudflare tunnel).
28
+ * @param args.iframe - The iframe to use for the communication
29
+ * @returns The created Frak Client
30
+ *
31
+ * @example
32
+ * const frakConfig: FrakWalletSdkConfig = {
33
+ * metadata: {
34
+ * name: "My app title",
35
+ * },
36
+ * }
37
+ * const iframe = await createIframe({ config: frakConfig });
38
+ * const client = createIFrameFrakClient({ config: frakConfig, iframe });
39
+ */
40
+ export function createIFrameFrakClient({
41
+ config,
42
+ iframe,
43
+ }: {
44
+ config: FrakWalletSdkConfig;
45
+ iframe: HTMLIFrameElement;
46
+ }): FrakClient {
47
+ const frakWalletUrl = config?.walletUrl ?? "https://wallet.frak.id";
48
+
49
+ // Create lifecycle manager
50
+ const lifecycleManager = createIFrameLifecycleManager({
51
+ iframe,
52
+ targetOrigin: frakWalletUrl,
53
+ configDomain: config.domain,
54
+ });
55
+
56
+ // Create our debug info gatherer
57
+ const debugInfo = new DebugInfoGatherer(config, iframe);
58
+
59
+ // Validate iframe
60
+ if (!iframe.contentWindow) {
61
+ throw new FrakRpcError(
62
+ RpcErrorCodes.configError,
63
+ "The iframe does not have a content window"
64
+ );
65
+ }
66
+
67
+ // Create RPC client with middleware and lifecycle handlers
68
+ const rpcClient = createRpcClient<IFrameRpcSchema, FrakLifecycleEvent>({
69
+ emittingTransport: iframe.contentWindow,
70
+ listeningTransport: window,
71
+ targetOrigin: frakWalletUrl,
72
+ middleware: [
73
+ // Ensure we are connected before sending request
74
+ {
75
+ async onRequest(_message, ctx) {
76
+ // Ensure the iframe is connected
77
+ const isConnected = await lifecycleManager.isConnected;
78
+ if (!isConnected) {
79
+ throw new FrakRpcError(
80
+ RpcErrorCodes.clientNotConnected,
81
+ "The iframe provider isn't connected yet"
82
+ );
83
+ }
84
+ return ctx;
85
+ },
86
+ },
87
+ // Save debug info
88
+ {
89
+ onRequest(message, ctx) {
90
+ debugInfo.setLastRequest(message);
91
+ return ctx;
92
+ },
93
+ onResponse(message, response) {
94
+ debugInfo.setLastResponse(message, response);
95
+ return response;
96
+ },
97
+ },
98
+ ],
99
+ // Add lifecycle handlers to process iframe lifecycle events
100
+ lifecycleHandlers: {
101
+ iframeLifecycle: async (event, _context) => {
102
+ // Delegate to lifecycle manager (cast for type compatibility)
103
+ await lifecycleManager.handleEvent(event);
104
+ },
105
+ },
106
+ });
107
+
108
+ // Setup heartbeat
109
+ const stopHeartbeat = setupHeartbeat(rpcClient, lifecycleManager);
110
+
111
+ // Build our destroy function
112
+ const destroy = async () => {
113
+ // Stop heartbeat
114
+ stopHeartbeat();
115
+ // Cleanup the RPC client
116
+ rpcClient.cleanup();
117
+ // Remove the iframe
118
+ iframe.remove();
119
+ };
120
+
121
+ // Init open panel
122
+ let openPanel: OpenPanel | undefined;
123
+ if (
124
+ process.env.OPEN_PANEL_API_URL &&
125
+ process.env.OPEN_PANEL_SDK_CLIENT_ID
126
+ ) {
127
+ console.log("[Frak SDK] Initializing OpenPanel");
128
+ openPanel = new OpenPanel({
129
+ apiUrl: process.env.OPEN_PANEL_API_URL,
130
+ clientId: process.env.OPEN_PANEL_SDK_CLIENT_ID,
131
+ trackScreenViews: true,
132
+ trackOutgoingLinks: true,
133
+ trackAttributes: false,
134
+ // We use a filter to ensure we got the open panel instance initialized
135
+ // A bit hacky, but this way we are sure that we got everything needed for the first event ever sent
136
+ filter: ({ type, payload }) => {
137
+ if (type !== "track") return true;
138
+ if (!payload?.properties) return true;
139
+
140
+ // Check if we we got the properties once initialized
141
+ if (!("sdkVersion" in payload.properties)) {
142
+ payload.properties = {
143
+ ...payload.properties,
144
+ sdkVersion: process.env.SDK_VERSION,
145
+ };
146
+ }
147
+
148
+ return true;
149
+ },
150
+ });
151
+ openPanel.setGlobalProperties({
152
+ sdkVersion: process.env.SDK_VERSION,
153
+ });
154
+ openPanel.init();
155
+ }
156
+
157
+ // Perform the post connection setup
158
+ const waitForSetup = postConnectionSetup({
159
+ config,
160
+ rpcClient,
161
+ lifecycleManager,
162
+ }).then(() => debugInfo.updateSetupStatus(true));
163
+
164
+ return {
165
+ config,
166
+ debugInfo,
167
+ waitForConnection: lifecycleManager.isConnected,
168
+ waitForSetup,
169
+ request: rpcClient.request,
170
+ listenerRequest: rpcClient.listen,
171
+ destroy,
172
+ openPanel,
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Setup the heartbeat
178
+ * @param rpcClient - RPC client to send lifecycle events
179
+ * @param lifecycleManager - Lifecycle manager to track connection
180
+ */
181
+ function setupHeartbeat(
182
+ rpcClient: SdkRpcClient,
183
+ lifecycleManager: IframeLifecycleManager
184
+ ) {
185
+ const HEARTBEAT_INTERVAL = 1_000; // Send heartbeat every 100ms until we are connected
186
+ const HEARTBEAT_TIMEOUT = 30_000; // 30 seconds timeout
187
+ let heartbeatInterval: NodeJS.Timeout;
188
+ let timeoutId: NodeJS.Timeout;
189
+
190
+ const sendHeartbeat = () =>
191
+ rpcClient.sendLifecycle({
192
+ clientLifecycle: "heartbeat",
193
+ });
194
+
195
+ // Start sending heartbeats
196
+ async function startHeartbeat() {
197
+ sendHeartbeat(); // Send initial heartbeat
198
+ heartbeatInterval = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL);
199
+
200
+ // Set up timeout
201
+ timeoutId = setTimeout(() => {
202
+ stopHeartbeat();
203
+ console.log("Heartbeat timeout: connection failed");
204
+ }, HEARTBEAT_TIMEOUT);
205
+
206
+ // Once connected, stop it
207
+ await lifecycleManager.isConnected;
208
+
209
+ // We are now connected, stop the heartbeat
210
+ stopHeartbeat();
211
+ }
212
+
213
+ // Stop sending heartbeats
214
+ function stopHeartbeat() {
215
+ if (heartbeatInterval) {
216
+ clearInterval(heartbeatInterval);
217
+ }
218
+ if (timeoutId) {
219
+ clearTimeout(timeoutId);
220
+ }
221
+ }
222
+
223
+ startHeartbeat();
224
+
225
+ // Return cleanup function
226
+ return stopHeartbeat;
227
+ }
228
+
229
+ /**
230
+ * Perform the post connection setup
231
+ * @param config - SDK configuration
232
+ * @param rpcClient - RPC client to send lifecycle events
233
+ * @param lifecycleManager - Lifecycle manager to track connection
234
+ */
235
+ async function postConnectionSetup({
236
+ config,
237
+ rpcClient,
238
+ lifecycleManager,
239
+ }: {
240
+ config: FrakWalletSdkConfig;
241
+ rpcClient: SdkRpcClient;
242
+ lifecycleManager: IframeLifecycleManager;
243
+ }): Promise<void> {
244
+ // Wait for the handler to be connected
245
+ await lifecycleManager.isConnected;
246
+
247
+ // Setup SSO URL listener to detect and forward SSO redirects
248
+ // This checks for ?sso= parameter and forwards compressed data to iframe
249
+ setupSsoUrlListener(rpcClient, lifecycleManager.isConnected);
250
+
251
+ // Push raw CSS if needed
252
+ async function pushCss() {
253
+ const cssLink = config.customizations?.css;
254
+ if (!cssLink) return;
255
+
256
+ const message = {
257
+ clientLifecycle: "modal-css" as const,
258
+ data: { cssLink },
259
+ };
260
+ rpcClient.sendLifecycle(message);
261
+ }
262
+
263
+ // Push i18n if needed
264
+ async function pushI18n() {
265
+ const i18n = config.customizations?.i18n;
266
+ if (!i18n) return;
267
+
268
+ const message = {
269
+ clientLifecycle: "modal-i18n" as const,
270
+ data: { i18n },
271
+ };
272
+ rpcClient.sendLifecycle(message);
273
+ }
274
+
275
+ // Push local backup if needed
276
+ async function pushBackup() {
277
+ if (typeof window === "undefined") return;
278
+
279
+ const backup = window.localStorage.getItem(BACKUP_KEY);
280
+ if (!backup) return;
281
+
282
+ const message = {
283
+ clientLifecycle: "restore-backup" as const,
284
+ data: { backup },
285
+ };
286
+ rpcClient.sendLifecycle(message);
287
+ }
288
+
289
+ await Promise.allSettled([pushCss(), pushI18n(), pushBackup()]);
290
+ }
@@ -0,0 +1,3 @@
1
+ export { createIFrameFrakClient } from "./createIFrameFrakClient";
2
+ export { DebugInfoGatherer } from "./DebugInfo";
3
+ export { setupClient } from "./setupClient";
@@ -0,0 +1,343 @@
1
+ import { beforeEach, describe, expect, test, vi } from "vitest";
2
+
3
+ // Mock dependencies
4
+ vi.mock("../utils", () => ({
5
+ createIframe: vi.fn(),
6
+ getSupportedCurrency: vi.fn((currency) => currency || "eur"),
7
+ }));
8
+
9
+ vi.mock("./createIFrameFrakClient", () => ({
10
+ createIFrameFrakClient: vi.fn(),
11
+ }));
12
+
13
+ describe("setupClient", () => {
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ describe("client setup", () => {
19
+ test("should create iframe with prepared config", async () => {
20
+ const { setupClient } = await import("./setupClient");
21
+ const { createIframe } = await import("../utils");
22
+ const { createIFrameFrakClient } = await import(
23
+ "./createIFrameFrakClient"
24
+ );
25
+
26
+ const mockIframe = document.createElement("iframe");
27
+ const mockClient = {
28
+ waitForSetup: Promise.resolve(),
29
+ waitForConnection: Promise.resolve(true),
30
+ };
31
+
32
+ vi.mocked(createIframe).mockResolvedValue(mockIframe);
33
+ vi.mocked(createIFrameFrakClient).mockReturnValue(
34
+ mockClient as any
35
+ );
36
+
37
+ const config = {
38
+ metadata: {
39
+ name: "Test App",
40
+ },
41
+ };
42
+
43
+ await setupClient({ config });
44
+
45
+ expect(createIframe).toHaveBeenCalledWith({
46
+ config: expect.objectContaining({
47
+ metadata: expect.objectContaining({
48
+ name: "Test App",
49
+ currency: "eur",
50
+ }),
51
+ }),
52
+ });
53
+ });
54
+
55
+ test("should preserve custom currency in config", async () => {
56
+ const { setupClient } = await import("./setupClient");
57
+ const { createIframe } = await import("../utils");
58
+ const { createIFrameFrakClient } = await import(
59
+ "./createIFrameFrakClient"
60
+ );
61
+ const { getSupportedCurrency } = await import("../utils");
62
+
63
+ const mockIframe = document.createElement("iframe");
64
+ const mockClient = {
65
+ waitForSetup: Promise.resolve(),
66
+ waitForConnection: Promise.resolve(true),
67
+ };
68
+
69
+ vi.mocked(getSupportedCurrency).mockReturnValue("usd");
70
+ vi.mocked(createIframe).mockResolvedValue(mockIframe);
71
+ vi.mocked(createIFrameFrakClient).mockReturnValue(
72
+ mockClient as any
73
+ );
74
+
75
+ const config = {
76
+ metadata: {
77
+ name: "Test App",
78
+ currency: "usd" as const,
79
+ },
80
+ };
81
+
82
+ await setupClient({ config });
83
+
84
+ expect(getSupportedCurrency).toHaveBeenCalledWith("usd");
85
+ expect(createIframe).toHaveBeenCalledWith({
86
+ config: expect.objectContaining({
87
+ metadata: expect.objectContaining({
88
+ currency: "usd",
89
+ }),
90
+ }),
91
+ });
92
+ });
93
+
94
+ test("should return undefined when iframe creation fails", async () => {
95
+ const { setupClient } = await import("./setupClient");
96
+ const { createIframe } = await import("../utils");
97
+
98
+ vi.mocked(createIframe).mockResolvedValue(undefined);
99
+
100
+ const config = {
101
+ metadata: {
102
+ name: "Test App",
103
+ },
104
+ };
105
+
106
+ const result = await setupClient({ config });
107
+
108
+ expect(result).toBeUndefined();
109
+ });
110
+
111
+ test("should create FrakClient with iframe", async () => {
112
+ const { setupClient } = await import("./setupClient");
113
+ const { createIframe } = await import("../utils");
114
+ const { createIFrameFrakClient } = await import(
115
+ "./createIFrameFrakClient"
116
+ );
117
+
118
+ const mockIframe = document.createElement("iframe");
119
+ const mockClient = {
120
+ waitForSetup: Promise.resolve(),
121
+ waitForConnection: Promise.resolve(true),
122
+ };
123
+
124
+ vi.mocked(createIframe).mockResolvedValue(mockIframe);
125
+ vi.mocked(createIFrameFrakClient).mockReturnValue(
126
+ mockClient as any
127
+ );
128
+
129
+ const config = {
130
+ metadata: {
131
+ name: "Test App",
132
+ },
133
+ };
134
+
135
+ await setupClient({ config });
136
+
137
+ expect(createIFrameFrakClient).toHaveBeenCalledWith({
138
+ config: expect.objectContaining({
139
+ metadata: expect.objectContaining({
140
+ name: "Test App",
141
+ }),
142
+ }),
143
+ iframe: mockIframe,
144
+ });
145
+ });
146
+
147
+ test("should wait for client setup", async () => {
148
+ const { setupClient } = await import("./setupClient");
149
+ const { createIframe } = await import("../utils");
150
+ const { createIFrameFrakClient } = await import(
151
+ "./createIFrameFrakClient"
152
+ );
153
+
154
+ const mockIframe = document.createElement("iframe");
155
+ const setupPromise = Promise.resolve();
156
+ const mockClient = {
157
+ waitForSetup: setupPromise,
158
+ waitForConnection: Promise.resolve(true),
159
+ };
160
+
161
+ vi.mocked(createIframe).mockResolvedValue(mockIframe);
162
+ vi.mocked(createIFrameFrakClient).mockReturnValue(
163
+ mockClient as any
164
+ );
165
+
166
+ const config = {
167
+ metadata: {
168
+ name: "Test App",
169
+ },
170
+ };
171
+
172
+ await setupClient({ config });
173
+
174
+ // Verify setup was awaited
175
+ await expect(setupPromise).resolves.toBeUndefined();
176
+ });
177
+
178
+ test("should wait for connection", async () => {
179
+ const { setupClient } = await import("./setupClient");
180
+ const { createIframe } = await import("../utils");
181
+ const { createIFrameFrakClient } = await import(
182
+ "./createIFrameFrakClient"
183
+ );
184
+
185
+ const mockIframe = document.createElement("iframe");
186
+ const connectionPromise = Promise.resolve(true);
187
+ const mockClient = {
188
+ waitForSetup: Promise.resolve(),
189
+ waitForConnection: connectionPromise,
190
+ };
191
+
192
+ vi.mocked(createIframe).mockResolvedValue(mockIframe);
193
+ vi.mocked(createIFrameFrakClient).mockReturnValue(
194
+ mockClient as any
195
+ );
196
+
197
+ const config = {
198
+ metadata: {
199
+ name: "Test App",
200
+ },
201
+ };
202
+
203
+ await setupClient({ config });
204
+
205
+ // Verify connection was awaited
206
+ await expect(connectionPromise).resolves.toBe(true);
207
+ });
208
+
209
+ test("should return undefined when connection fails", async () => {
210
+ const { setupClient } = await import("./setupClient");
211
+ const { createIframe } = await import("../utils");
212
+ const { createIFrameFrakClient } = await import(
213
+ "./createIFrameFrakClient"
214
+ );
215
+
216
+ const mockIframe = document.createElement("iframe");
217
+ const mockClient = {
218
+ waitForSetup: Promise.resolve(),
219
+ waitForConnection: Promise.resolve(false),
220
+ };
221
+
222
+ vi.mocked(createIframe).mockResolvedValue(mockIframe);
223
+ vi.mocked(createIFrameFrakClient).mockReturnValue(
224
+ mockClient as any
225
+ );
226
+
227
+ const config = {
228
+ metadata: {
229
+ name: "Test App",
230
+ },
231
+ };
232
+
233
+ const result = await setupClient({ config });
234
+
235
+ expect(result).toBeUndefined();
236
+ });
237
+
238
+ test("should return client when setup successful", async () => {
239
+ const { setupClient } = await import("./setupClient");
240
+ const { createIframe } = await import("../utils");
241
+ const { createIFrameFrakClient } = await import(
242
+ "./createIFrameFrakClient"
243
+ );
244
+
245
+ const mockIframe = document.createElement("iframe");
246
+ const mockClient = {
247
+ waitForSetup: Promise.resolve(),
248
+ waitForConnection: Promise.resolve(true),
249
+ request: vi.fn(),
250
+ };
251
+
252
+ vi.mocked(createIframe).mockResolvedValue(mockIframe);
253
+ vi.mocked(createIFrameFrakClient).mockReturnValue(
254
+ mockClient as any
255
+ );
256
+
257
+ const config = {
258
+ metadata: {
259
+ name: "Test App",
260
+ },
261
+ };
262
+
263
+ const result = await setupClient({ config });
264
+
265
+ expect(result).toBe(mockClient);
266
+ });
267
+ });
268
+
269
+ describe("config preparation", () => {
270
+ test("should use default currency when none provided", async () => {
271
+ const { setupClient } = await import("./setupClient");
272
+ const { createIframe } = await import("../utils");
273
+ const { createIFrameFrakClient } = await import(
274
+ "./createIFrameFrakClient"
275
+ );
276
+ const { getSupportedCurrency } = await import("../utils");
277
+
278
+ const mockIframe = document.createElement("iframe");
279
+ const mockClient = {
280
+ waitForSetup: Promise.resolve(),
281
+ waitForConnection: Promise.resolve(true),
282
+ };
283
+
284
+ vi.mocked(getSupportedCurrency).mockReturnValue("eur");
285
+ vi.mocked(createIframe).mockResolvedValue(mockIframe);
286
+ vi.mocked(createIFrameFrakClient).mockReturnValue(
287
+ mockClient as any
288
+ );
289
+
290
+ const config = {
291
+ metadata: {
292
+ name: "Test App",
293
+ },
294
+ };
295
+
296
+ await setupClient({ config });
297
+
298
+ expect(getSupportedCurrency).toHaveBeenCalledWith(undefined);
299
+ });
300
+
301
+ test("should merge metadata with prepared config", async () => {
302
+ const { setupClient } = await import("./setupClient");
303
+ const { createIframe } = await import("../utils");
304
+ const { createIFrameFrakClient } = await import(
305
+ "./createIFrameFrakClient"
306
+ );
307
+
308
+ const mockIframe = document.createElement("iframe");
309
+ const mockClient = {
310
+ waitForSetup: Promise.resolve(),
311
+ waitForConnection: Promise.resolve(true),
312
+ };
313
+
314
+ vi.mocked(createIframe).mockResolvedValue(mockIframe);
315
+ vi.mocked(createIFrameFrakClient).mockReturnValue(
316
+ mockClient as any
317
+ );
318
+
319
+ const config = {
320
+ metadata: {
321
+ name: "Test App",
322
+ css: {
323
+ primaryColor: "#ff0000",
324
+ },
325
+ },
326
+ };
327
+
328
+ await setupClient({ config });
329
+
330
+ expect(createIframe).toHaveBeenCalledWith({
331
+ config: expect.objectContaining({
332
+ metadata: expect.objectContaining({
333
+ name: "Test App",
334
+ css: {
335
+ primaryColor: "#ff0000",
336
+ },
337
+ currency: "eur",
338
+ }),
339
+ }),
340
+ });
341
+ });
342
+ });
343
+ });
@@ -0,0 +1,73 @@
1
+ import { createIFrameFrakClient } from "../clients";
2
+ import type { FrakClient, FrakWalletSdkConfig } from "../types";
3
+ import { createIframe, getSupportedCurrency } from "../utils";
4
+
5
+ /**
6
+ * Directly setup the Frak client with an iframe
7
+ * Return when the FrakClient is ready (setup and communication estbalished with the wallet)
8
+ *
9
+ * @param config - The configuration to use for the Frak Wallet SDK
10
+ * @returns a Promise with the Frak Client
11
+ *
12
+ * @example
13
+ * const frakConfig: FrakWalletSdkConfig = {
14
+ * metadata: {
15
+ * name: "My app title",
16
+ * },
17
+ * }
18
+ * const client = await setupClient({ config: frakConfig });
19
+ */
20
+ export async function setupClient({
21
+ config,
22
+ }: {
23
+ config: FrakWalletSdkConfig;
24
+ }): Promise<FrakClient | undefined> {
25
+ // Prepare the config
26
+ const preparedConfig = prepareConfig(config);
27
+
28
+ // Create our iframe
29
+ const iframe = await createIframe({
30
+ config: preparedConfig,
31
+ });
32
+
33
+ if (!iframe) {
34
+ console.error("Failed to create iframe");
35
+ return;
36
+ }
37
+
38
+ // Create our client
39
+ const client = createIFrameFrakClient({
40
+ config: preparedConfig,
41
+ iframe,
42
+ });
43
+
44
+ // Wait for the client to be all setup
45
+ await client.waitForSetup;
46
+
47
+ // Wait for the connection to be established
48
+ const waitForConnection = await client.waitForConnection;
49
+ if (!waitForConnection) {
50
+ console.error("Failed to connect to client");
51
+ return;
52
+ }
53
+
54
+ return client;
55
+ }
56
+
57
+ /**
58
+ * Prepare the config for the Frak Client
59
+ * @param config - The configuration to use for the Frak Wallet SDK
60
+ * @returns The prepared configuration with the supported currency
61
+ */
62
+ function prepareConfig(config: FrakWalletSdkConfig): FrakWalletSdkConfig {
63
+ // Get the supported currency (e.g. "eur")
64
+ const supportedCurrency = getSupportedCurrency(config.metadata?.currency);
65
+
66
+ return {
67
+ ...config,
68
+ metadata: {
69
+ ...config.metadata,
70
+ currency: supportedCurrency,
71
+ },
72
+ };
73
+ }