@frak-labs/core-sdk 0.2.1-beta.b38eef2e → 0.2.1-beta.d04602ec

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 (74) hide show
  1. package/README.md +1 -2
  2. package/cdn/bundle.js +55 -3
  3. package/dist/actions.cjs +1 -1
  4. package/dist/actions.d.cts +3 -3
  5. package/dist/actions.d.ts +3 -3
  6. package/dist/actions.js +1 -1
  7. package/dist/bundle.cjs +1 -1
  8. package/dist/bundle.d.cts +4 -4
  9. package/dist/bundle.d.ts +4 -4
  10. package/dist/bundle.js +1 -1
  11. package/dist/{computeLegacyProductId-CCAZvLa5.d.cts → computeLegacyProductId-fKvxbC4k.d.ts} +91 -37
  12. package/dist/{computeLegacyProductId-b5cUWdAm.d.ts → computeLegacyProductId-rYIvY4c3.d.cts} +91 -37
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.d.cts +3 -3
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +1 -1
  17. package/dist/{openSso-B0g7-807.d.cts → openSso-CMZM06uR.d.ts} +258 -46
  18. package/dist/{openSso-CMzwvaCa.d.ts → openSso-CebB8mFv.d.cts} +258 -46
  19. package/dist/setupClient-B_XMB52l.cjs +13 -0
  20. package/dist/setupClient-jYx-fbxB.js +13 -0
  21. package/dist/siweAuthenticate-CWcVvP-G.cjs +1 -0
  22. package/dist/siweAuthenticate-DQfdb5UQ.js +1 -0
  23. package/dist/{siweAuthenticate-CnCZ7mok.d.ts → siweAuthenticate-Dc_Yg9Bg.d.cts} +102 -8
  24. package/dist/{siweAuthenticate-CVigMOxz.d.cts → siweAuthenticate-Ddhl-o4N.d.ts} +102 -8
  25. package/dist/trackEvent-Ce1XlsIE.js +1 -0
  26. package/dist/trackEvent-CvbJTTqA.cjs +1 -0
  27. package/package.json +8 -8
  28. package/src/actions/displayEmbeddedWallet.ts +6 -2
  29. package/src/actions/displayModal.ts +6 -2
  30. package/src/actions/displaySharingPage.ts +49 -0
  31. package/src/actions/ensureIdentity.ts +2 -2
  32. package/src/actions/getMerchantInformation.test.ts +13 -1
  33. package/src/actions/getMerchantInformation.ts +20 -5
  34. package/src/actions/getUserReferralStatus.ts +42 -0
  35. package/src/actions/index.ts +7 -1
  36. package/src/actions/referral/setupReferral.test.ts +79 -0
  37. package/src/actions/referral/setupReferral.ts +32 -0
  38. package/src/actions/trackPurchaseStatus.test.ts +32 -20
  39. package/src/actions/trackPurchaseStatus.ts +3 -5
  40. package/src/actions/wrapper/modalBuilder.test.ts +4 -2
  41. package/src/actions/wrapper/modalBuilder.ts +6 -8
  42. package/src/clients/createIFrameFrakClient.ts +148 -25
  43. package/src/clients/transports/iframeLifecycleManager.test.ts +0 -80
  44. package/src/clients/transports/iframeLifecycleManager.ts +0 -44
  45. package/src/index.ts +17 -4
  46. package/src/types/config.ts +10 -3
  47. package/src/types/index.ts +13 -1
  48. package/src/types/lifecycle/client.ts +22 -27
  49. package/src/types/lifecycle/iframe.ts +0 -8
  50. package/src/types/resolvedConfig.ts +122 -0
  51. package/src/types/rpc/displaySharingPage.ts +77 -0
  52. package/src/types/rpc/interaction.ts +4 -0
  53. package/src/types/rpc/userReferralStatus.ts +20 -0
  54. package/src/types/rpc.ts +42 -5
  55. package/src/utils/backendUrl.test.ts +2 -2
  56. package/src/utils/backendUrl.ts +1 -1
  57. package/src/utils/cache/index.ts +7 -0
  58. package/src/utils/cache/lruMap.test.ts +55 -0
  59. package/src/utils/cache/lruMap.ts +38 -0
  60. package/src/utils/cache/withCache.test.ts +162 -0
  61. package/src/utils/cache/withCache.ts +105 -0
  62. package/src/utils/inAppBrowser.ts +60 -0
  63. package/src/utils/index.ts +6 -4
  64. package/src/utils/sdkConfigStore.test.ts +405 -0
  65. package/src/utils/sdkConfigStore.ts +263 -0
  66. package/src/utils/sso.ts +3 -7
  67. package/dist/setupClient-CqTHGvVa.cjs +0 -13
  68. package/dist/setupClient-DTyvAPgh.js +0 -13
  69. package/dist/siweAuthenticate-BWmI2_TN.cjs +0 -1
  70. package/dist/siweAuthenticate-zczqxm0a.js +0 -1
  71. package/dist/trackEvent-CeLFVzZn.js +0 -1
  72. package/dist/trackEvent-Ew5r5zfI.cjs +0 -1
  73. package/src/utils/merchantId.test.ts +0 -653
  74. package/src/utils/merchantId.ts +0 -143
@@ -1,15 +1,21 @@
1
1
  export { displayEmbeddedWallet } from "./displayEmbeddedWallet";
2
2
  export { displayModal } from "./displayModal";
3
+ export { displaySharingPage } from "./displaySharingPage";
3
4
  export { ensureIdentity } from "./ensureIdentity";
4
5
  export { getMerchantInformation } from "./getMerchantInformation";
6
+ export { getUserReferralStatus } from "./getUserReferralStatus";
5
7
  export { openSso } from "./openSso";
6
8
  export { prepareSso } from "./prepareSso";
7
9
  export {
8
10
  type ProcessReferralOptions,
9
11
  processReferral,
10
12
  } from "./referral/processReferral";
11
- // Referral interaction
13
+ // Referral
12
14
  export { referralInteraction } from "./referral/referralInteraction";
15
+ export {
16
+ REFERRAL_SUCCESS_EVENT,
17
+ setupReferral,
18
+ } from "./referral/setupReferral";
13
19
  export { sendInteraction } from "./sendInteraction";
14
20
  // Helper to track the purchase status
15
21
  export { trackPurchaseStatus } from "./trackPurchaseStatus";
@@ -0,0 +1,79 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { REFERRAL_SUCCESS_EVENT, setupReferral } from "./setupReferral";
3
+
4
+ vi.mock("./referralInteraction", () => ({
5
+ referralInteraction: vi.fn(),
6
+ }));
7
+
8
+ import { referralInteraction } from "./referralInteraction";
9
+
10
+ describe("setupReferral", () => {
11
+ beforeEach(() => {
12
+ vi.clearAllMocks();
13
+ });
14
+
15
+ afterEach(() => {
16
+ vi.restoreAllMocks();
17
+ });
18
+
19
+ it("should dispatch referral success event on successful referral", async () => {
20
+ vi.mocked(referralInteraction).mockResolvedValue("success");
21
+ const listener = vi.fn();
22
+ window.addEventListener(REFERRAL_SUCCESS_EVENT, listener);
23
+
24
+ await setupReferral({ config: {} } as any);
25
+
26
+ expect(listener).toHaveBeenCalledTimes(1);
27
+ expect(listener).toHaveBeenCalledWith(expect.any(Event));
28
+
29
+ window.removeEventListener(REFERRAL_SUCCESS_EVENT, listener);
30
+ });
31
+
32
+ it("should not dispatch event when referral state is not success", async () => {
33
+ vi.mocked(referralInteraction).mockResolvedValue("no-referrer");
34
+ const listener = vi.fn();
35
+ window.addEventListener(REFERRAL_SUCCESS_EVENT, listener);
36
+
37
+ await setupReferral({ config: {} } as any);
38
+
39
+ expect(listener).not.toHaveBeenCalled();
40
+
41
+ window.removeEventListener(REFERRAL_SUCCESS_EVENT, listener);
42
+ });
43
+
44
+ it("should not dispatch event when referral returns undefined", async () => {
45
+ vi.mocked(referralInteraction).mockResolvedValue(undefined);
46
+ const listener = vi.fn();
47
+ window.addEventListener(REFERRAL_SUCCESS_EVENT, listener);
48
+
49
+ await setupReferral({ config: {} } as any);
50
+
51
+ expect(listener).not.toHaveBeenCalled();
52
+
53
+ window.removeEventListener(REFERRAL_SUCCESS_EVENT, listener);
54
+ });
55
+
56
+ it("should silently catch errors and log warning", async () => {
57
+ vi.mocked(referralInteraction).mockRejectedValue(
58
+ new Error("network failure")
59
+ );
60
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
61
+ const listener = vi.fn();
62
+ window.addEventListener(REFERRAL_SUCCESS_EVENT, listener);
63
+
64
+ await setupReferral({ config: {} } as any);
65
+
66
+ expect(listener).not.toHaveBeenCalled();
67
+ expect(warnSpy).toHaveBeenCalledWith(
68
+ "[Frak] Referral setup failed",
69
+ expect.any(Error)
70
+ );
71
+
72
+ window.removeEventListener(REFERRAL_SUCCESS_EVENT, listener);
73
+ warnSpy.mockRestore();
74
+ });
75
+
76
+ it("should export the correct event name constant", () => {
77
+ expect(REFERRAL_SUCCESS_EVENT).toBe("frak:referral-success");
78
+ });
79
+ });
@@ -0,0 +1,32 @@
1
+ import type { FrakClient } from "../../types";
2
+ import { referralInteraction } from "./referralInteraction";
3
+
4
+ /**
5
+ * Custom event name dispatched on successful referral processing.
6
+ *
7
+ * Fired once per page load when a valid referral context is found in the URL
8
+ * and successfully tracked. Consumers (e.g. `<frak-banner>`) listen for this
9
+ * to display a referral success message.
10
+ */
11
+ export const REFERRAL_SUCCESS_EVENT = "frak:referral-success";
12
+
13
+ /**
14
+ * Process referral context and emit a DOM event on success.
15
+ *
16
+ * - Calls {@link referralInteraction} to detect and track any referral in the URL
17
+ * - On `"success"`, dispatches a bare {@link REFERRAL_SUCCESS_EVENT} on `window`
18
+ * - Silently swallows errors (fire-and-forget during SDK init)
19
+ *
20
+ * @param client - The initialized Frak client
21
+ */
22
+ export async function setupReferral(client: FrakClient): Promise<void> {
23
+ try {
24
+ const state = await referralInteraction(client);
25
+
26
+ if (state === "success") {
27
+ window.dispatchEvent(new Event(REFERRAL_SUCCESS_EVENT));
28
+ }
29
+ } catch (error) {
30
+ console.warn("[Frak] Referral setup failed", error);
31
+ }
32
+ }
@@ -11,12 +11,14 @@ vi.mock("../utils/clientId", () => ({
11
11
  getClientId: vi.fn().mockReturnValue("test-client-id"),
12
12
  }));
13
13
 
14
- vi.mock("../utils/merchantId", () => ({
15
- fetchMerchantId: vi.fn().mockResolvedValue(undefined),
14
+ vi.mock("../utils/sdkConfigStore", () => ({
15
+ sdkConfigStore: {
16
+ resolveMerchantId: vi.fn().mockResolvedValue(undefined),
17
+ },
16
18
  }));
17
19
 
18
20
  import { getClientId } from "../utils/clientId";
19
- import { fetchMerchantId } from "../utils/merchantId";
21
+ import { sdkConfigStore } from "../utils/sdkConfigStore";
20
22
  import { trackPurchaseStatus } from "./trackPurchaseStatus";
21
23
 
22
24
  describe.sequential("trackPurchaseStatus", () => {
@@ -100,7 +102,9 @@ describe.sequential("trackPurchaseStatus", () => {
100
102
  });
101
103
 
102
104
  vi.mocked(getClientId).mockReturnValue("test-client-id");
103
- vi.mocked(fetchMerchantId).mockResolvedValue(undefined);
105
+ vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
106
+ undefined
107
+ );
104
108
 
105
109
  fetchSpy = vi.fn().mockResolvedValue({
106
110
  ok: true,
@@ -228,12 +232,15 @@ describe.sequential("trackPurchaseStatus", () => {
228
232
  test("should resolve merchantId from explicit param first", async () => {
229
233
  setupStorage({
230
234
  interactionToken: "token-123",
231
- merchantId: "session-merchant-id",
235
+ merchantId: null,
232
236
  clientId: "test-client-id",
233
237
  });
234
- vi.mocked(fetchMerchantId).mockResolvedValue("fetched-merchant-id");
235
- const merchantLookupCallsBefore =
236
- vi.mocked(fetchMerchantId).mock.calls.length;
238
+ vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
239
+ "fetched-merchant-id"
240
+ );
241
+ const merchantLookupCallsBefore = vi.mocked(
242
+ sdkConfigStore.resolveMerchantId
243
+ ).mock.calls.length;
237
244
 
238
245
  await trackPurchaseStatus({
239
246
  customerId: "cust-1",
@@ -253,19 +260,20 @@ describe.sequential("trackPurchaseStatus", () => {
253
260
  merchantId: "explicit-merchant-id",
254
261
  })
255
262
  );
256
- expect(vi.mocked(fetchMerchantId).mock.calls.length).toBe(
257
- merchantLookupCallsBefore
258
- );
263
+ expect(
264
+ vi.mocked(sdkConfigStore.resolveMerchantId).mock.calls.length
265
+ ).toBe(merchantLookupCallsBefore);
259
266
  });
260
267
 
261
268
  test("should fall back to sessionStorage for merchantId", async () => {
269
+ vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
270
+ "session-merchant-id"
271
+ );
262
272
  setupStorage({
263
273
  interactionToken: "token-123",
264
- merchantId: "session-merchant-id",
274
+ merchantId: null,
265
275
  clientId: "test-client-id",
266
276
  });
267
- const merchantLookupCallsBefore =
268
- vi.mocked(fetchMerchantId).mock.calls.length;
269
277
 
270
278
  await trackPurchaseStatus({
271
279
  customerId: "cust-1",
@@ -284,18 +292,20 @@ describe.sequential("trackPurchaseStatus", () => {
284
292
  merchantId: "session-merchant-id",
285
293
  })
286
294
  );
287
- expect(vi.mocked(fetchMerchantId).mock.calls.length).toBe(
288
- merchantLookupCallsBefore
289
- );
295
+ expect(
296
+ vi.mocked(sdkConfigStore.resolveMerchantId)
297
+ ).toHaveBeenCalled();
290
298
  });
291
299
 
292
- test("should fall back to fetchMerchantId when no explicit or sessionStorage", async () => {
300
+ test("should fall back to resolveMerchantId when no explicit merchantId", async () => {
293
301
  setupStorage({
294
302
  interactionToken: "token-123",
295
303
  merchantId: null,
296
304
  clientId: "test-client-id",
297
305
  });
298
- vi.mocked(fetchMerchantId).mockResolvedValue("fetched-merchant-id");
306
+ vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
307
+ "fetched-merchant-id"
308
+ );
299
309
 
300
310
  await trackPurchaseStatus({
301
311
  customerId: "cust-1",
@@ -322,7 +332,9 @@ describe.sequential("trackPurchaseStatus", () => {
322
332
  merchantId: null,
323
333
  clientId: "test-client-id",
324
334
  });
325
- vi.mocked(fetchMerchantId).mockResolvedValue(undefined);
335
+ vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
336
+ undefined
337
+ );
326
338
  const callCountBefore = getTrackingRequests().length;
327
339
 
328
340
  await trackPurchaseStatus({
@@ -1,6 +1,6 @@
1
1
  import { getBackendUrl } from "../utils/backendUrl";
2
2
  import { getClientId } from "../utils/clientId";
3
- import { fetchMerchantId } from "../utils/merchantId";
3
+ import { sdkConfigStore } from "../utils/sdkConfigStore";
4
4
 
5
5
  /**
6
6
  * Function used to track the status of a purchase
@@ -26,7 +26,7 @@ import { fetchMerchantId } from "../utils/merchantId";
26
26
  * }
27
27
  *
28
28
  * @remarks
29
- * - Merchant id is resolved in this order: explicit `args.merchantId`, `frak-merchant-id` from session storage, then `fetchMerchantId()`.
29
+ * - Merchant id is resolved in this order: explicit `args.merchantId`, then `sdkConfigStore.resolveMerchantId()` (config store sessionStorage → backend fetch).
30
30
  * - This function supports anonymous users and will use the `x-frak-client-id` header when available.
31
31
  * - At least one identity source must exist (`frak-wallet-interaction-token` or `x-frak-client-id`), otherwise the tracking request is skipped.
32
32
  * - This function will print a warning if used in a non-browser environment or if no identity / merchant id can be resolved.
@@ -52,10 +52,8 @@ export async function trackPurchaseStatus(args: {
52
52
  return;
53
53
  }
54
54
 
55
- const merchantIdFromStorage =
56
- window.sessionStorage.getItem("frak-merchant-id");
57
55
  const merchantId =
58
- args.merchantId ?? merchantIdFromStorage ?? (await fetchMerchantId());
56
+ args.merchantId ?? (await sdkConfigStore.resolveMerchantId());
59
57
 
60
58
  if (!merchantId) {
61
59
  console.warn("[Frak] No merchant id found, skipping purchase check");
@@ -196,7 +196,8 @@ describe("modalBuilder", () => {
196
196
 
197
197
  expect(displayModal).toHaveBeenCalledWith(
198
198
  mockClient,
199
- builder.params
199
+ builder.params,
200
+ undefined
200
201
  );
201
202
  });
202
203
 
@@ -216,7 +217,8 @@ describe("modalBuilder", () => {
216
217
  mockClient,
217
218
  expect.objectContaining({
218
219
  metadata: { header: { title: "Overridden" } },
219
- })
220
+ }),
221
+ undefined
220
222
  );
221
223
  });
222
224
 
@@ -46,11 +46,13 @@ export type ModalStepBuilder<
46
46
  /**
47
47
  * Display the modal
48
48
  * @param metadataOverride - Function returning optional metadata to override the current modal metadata
49
+ * @param placement - Optional placement ID to associate with this modal display
49
50
  */
50
51
  display: (
51
52
  metadataOverride?: (
52
53
  current?: ModalRpcMetadata
53
- ) => ModalRpcMetadata | undefined
54
+ ) => ModalRpcMetadata | undefined,
55
+ placement?: string
54
56
  ) => Promise<ModalRpcStepsResultType<Steps>>;
55
57
  };
56
58
 
@@ -177,27 +179,23 @@ function modalStepsBuilder<CurrentSteps extends ModalStepTypes[]>(
177
179
  );
178
180
  }
179
181
 
180
- // Function to display it
181
182
  async function display(
182
183
  metadataOverride?: (
183
184
  current?: ModalRpcMetadata
184
- ) => ModalRpcMetadata | undefined
185
+ ) => ModalRpcMetadata | undefined,
186
+ placement?: string
185
187
  ) {
186
- // If we have a metadata override, apply it
187
188
  if (metadataOverride) {
188
189
  params.metadata = metadataOverride(params.metadata ?? {});
189
190
  }
190
- return await displayModal(client, params);
191
+ return await displayModal(client, params, placement);
191
192
  }
192
193
 
193
194
  return {
194
- // Access current modal params
195
195
  params,
196
- // Function to add new steps
197
196
  sendTx,
198
197
  reward,
199
198
  sharing,
200
- // Display the modal
201
199
  display,
202
200
  };
203
201
  }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  createRpcClient,
3
+ Deferred,
3
4
  FrakRpcError,
4
5
  type RpcClient,
5
6
  RpcErrorCodes,
@@ -8,9 +9,12 @@ import { OpenPanel } from "@openpanel/web";
8
9
  import type { FrakLifecycleEvent } from "../types";
9
10
  import type { FrakClient } from "../types/client";
10
11
  import type { FrakWalletSdkConfig } from "../types/config";
12
+ import type { SdkResolvedConfig } from "../types/resolvedConfig";
11
13
  import type { IFrameRpcSchema } from "../types/rpc";
12
14
  import { getClientId } from "../utils";
15
+ import { clearAllCache } from "../utils/cache";
13
16
  import { BACKUP_KEY } from "../utils/constants";
17
+ import { sdkConfigStore } from "../utils/sdkConfigStore";
14
18
  import { setupSsoUrlListener } from "../utils/ssoUrlListener";
15
19
  import { DebugInfoGatherer } from "./DebugInfo";
16
20
  import {
@@ -19,12 +23,13 @@ import {
19
23
  } from "./transports/iframeLifecycleManager";
20
24
 
21
25
  type SdkRpcClient = RpcClient<IFrameRpcSchema, FrakLifecycleEvent>;
26
+ type MerchantConfigResult = Awaited<ReturnType<typeof sdkConfigStore.resolve>>;
22
27
 
23
28
  /**
24
29
  * Create a new iframe Frak client
25
30
  * @param args
26
31
  * @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).
32
+ * When `config.domain` is set, it is used to resolve the correct merchant config in tunneled/proxied environments (e.g. Shopify dev with Cloudflare tunnel).
28
33
  * @param args.iframe - The iframe to use for the communication
29
34
  * @returns The created Frak Client
30
35
  *
@@ -46,13 +51,35 @@ export function createIFrameFrakClient({
46
51
  }): FrakClient {
47
52
  const frakWalletUrl = config?.walletUrl ?? "https://wallet.frak.id";
48
53
 
54
+ const browserLang =
55
+ typeof navigator !== "undefined"
56
+ ? navigator.language?.split("-")[0]
57
+ : undefined;
58
+ const detectedLang =
59
+ config.metadata.lang ??
60
+ (browserLang === "en" || browserLang === "fr"
61
+ ? browserLang
62
+ : undefined);
63
+ const targetDomain =
64
+ config.domain ??
65
+ (typeof window !== "undefined" ? window.location.hostname : "");
66
+ sdkConfigStore.setCacheScope(targetDomain, detectedLang);
67
+ sdkConfigStore.reset();
68
+
69
+ // Skip fetch entirely if cache is fresh, otherwise fetch (SWR)
70
+ const configPromise = sdkConfigStore.isCacheFresh
71
+ ? undefined
72
+ : sdkConfigStore.resolve(config.domain, config.walletUrl, detectedLang);
73
+
49
74
  // Create lifecycle manager
50
75
  const lifecycleManager = createIFrameLifecycleManager({
51
76
  iframe,
52
77
  targetOrigin: frakWalletUrl,
53
- configDomain: config.domain,
54
78
  });
55
79
 
80
+ // Resolved after first resolved-config is sent to iframe (prevents RPC before context exists)
81
+ const contextSent = new Deferred<void>();
82
+
56
83
  // Create our debug info gatherer
57
84
  const debugInfo = new DebugInfoGatherer(config, iframe);
58
85
 
@@ -70,10 +97,9 @@ export function createIFrameFrakClient({
70
97
  listeningTransport: window,
71
98
  targetOrigin: frakWalletUrl,
72
99
  middleware: [
73
- // Ensure we are connected before sending request
100
+ // Ensure we are connected and context is sent before sending request
74
101
  {
75
102
  async onRequest(_message, ctx) {
76
- // Ensure the iframe is connected
77
103
  const isConnected = await lifecycleManager.isConnected;
78
104
  if (!isConnected) {
79
105
  throw new FrakRpcError(
@@ -81,6 +107,7 @@ export function createIFrameFrakClient({
81
107
  "The iframe provider isn't connected yet"
82
108
  );
83
109
  }
110
+ await contextSent.promise;
84
111
  return ctx;
85
112
  },
86
113
  },
@@ -108,14 +135,13 @@ export function createIFrameFrakClient({
108
135
  // Setup heartbeat
109
136
  const stopHeartbeat = setupHeartbeat(rpcClient, lifecycleManager);
110
137
 
111
- // Build our destroy function
112
138
  const destroy = async () => {
113
- // Stop heartbeat
114
139
  stopHeartbeat();
115
- // Cleanup the RPC client
116
140
  rpcClient.cleanup();
117
- // Remove the iframe
118
141
  iframe.remove();
142
+ clearAllCache();
143
+ sdkConfigStore.clearCache();
144
+ sdkConfigStore.reset();
119
145
  };
120
146
 
121
147
  // Init open panel
@@ -161,7 +187,14 @@ export function createIFrameFrakClient({
161
187
  config,
162
188
  rpcClient,
163
189
  lifecycleManager,
164
- }).then(() => debugInfo.updateSetupStatus(true));
190
+ configPromise,
191
+ contextSent,
192
+ })
193
+ .then(() => debugInfo.updateSetupStatus(true))
194
+ .catch((err) => {
195
+ contextSent.reject(err);
196
+ throw err;
197
+ });
165
198
 
166
199
  return {
167
200
  config,
@@ -238,54 +271,144 @@ async function postConnectionSetup({
238
271
  config,
239
272
  rpcClient,
240
273
  lifecycleManager,
274
+ configPromise,
275
+ contextSent,
241
276
  }: {
242
277
  config: FrakWalletSdkConfig;
243
278
  rpcClient: SdkRpcClient;
244
279
  lifecycleManager: IframeLifecycleManager;
280
+ configPromise: Promise<MerchantConfigResult> | undefined;
281
+ contextSent: Deferred<void>;
245
282
  }): Promise<void> {
246
- // Wait for the handler to be connected
247
283
  await lifecycleManager.isConnected;
248
284
 
249
- // Setup SSO URL listener to detect and forward SSO redirects
250
- // This checks for ?sso= parameter and forwards compressed data to iframe
251
285
  setupSsoUrlListener(rpcClient, lifecycleManager.isConnected);
252
286
 
287
+ // Read and consume the pending merge token from URL (SSO identity merge)
288
+ const url = new URL(window.location.href);
289
+ const pendingMergeToken = url.searchParams.get("fmt") ?? undefined;
290
+ if (pendingMergeToken) {
291
+ url.searchParams.delete("fmt");
292
+ window.history.replaceState({}, "", url.toString());
293
+ }
294
+
295
+ // Merge a raw backend response with SDK metadata and persist to store
296
+ const mergeAndSetConfig = (merchantConfig: MerchantConfigResult) => {
297
+ const merchantId =
298
+ merchantConfig?.merchantId ?? config.metadata.merchantId ?? "";
299
+ const domain = merchantConfig?.domain ?? "";
300
+ const allowedDomains = merchantConfig?.allowedDomains ?? [];
301
+ const raw = merchantConfig?.sdkConfig;
302
+
303
+ sdkConfigStore.setConfig(
304
+ raw
305
+ ? {
306
+ isResolved: true,
307
+ merchantId,
308
+ domain,
309
+ allowedDomains,
310
+ hasRawSdkConfig: true,
311
+ name: raw.name ?? config.metadata.name,
312
+ logoUrl: raw.logoUrl ?? config.metadata.logoUrl,
313
+ homepageLink:
314
+ raw.homepageLink ?? config.metadata.homepageLink,
315
+ lang: raw.lang ?? config.metadata.lang,
316
+ currency: raw.currency ?? config.metadata.currency,
317
+ hidden: raw.hidden,
318
+ css: raw.css,
319
+ translations: raw.translations,
320
+ placements: raw.placements,
321
+ }
322
+ : {
323
+ isResolved: true,
324
+ merchantId,
325
+ domain,
326
+ allowedDomains,
327
+ name: config.metadata.name,
328
+ logoUrl: config.metadata.logoUrl,
329
+ homepageLink: config.metadata.homepageLink,
330
+ lang: config.metadata.lang,
331
+ currency: config.metadata.currency,
332
+ }
333
+ );
334
+ };
335
+
336
+ // Send the resolved-config lifecycle event to the iframe
337
+ let mergeTokenConsumed = false;
338
+ const sendLifecycleConfig = (resolved: SdkResolvedConfig) => {
339
+ const token = mergeTokenConsumed ? undefined : pendingMergeToken;
340
+ mergeTokenConsumed = true;
341
+
342
+ const sdkConfig = resolved.hasRawSdkConfig
343
+ ? {
344
+ name: resolved.name,
345
+ logoUrl: resolved.logoUrl,
346
+ homepageLink: resolved.homepageLink,
347
+ lang: resolved.lang,
348
+ currency: resolved.currency,
349
+ hidden: resolved.hidden,
350
+ css: resolved.css,
351
+ translations: resolved.translations,
352
+ placements: resolved.placements,
353
+ }
354
+ : undefined;
355
+
356
+ rpcClient.sendLifecycle({
357
+ clientLifecycle: "resolved-config",
358
+ data: {
359
+ merchantId: resolved.merchantId,
360
+ domain: resolved.domain ?? "",
361
+ allowedDomains: resolved.allowedDomains ?? [],
362
+ sourceUrl: window.location.href,
363
+ ...(token && { pendingMergeToken: token }),
364
+ ...(sdkConfig && { sdkConfig }),
365
+ },
366
+ });
367
+ };
368
+
369
+ // SWR: if we have cached data, send it to the iframe immediately
370
+ if (sdkConfigStore.isResolved) {
371
+ sendLifecycleConfig(sdkConfigStore.getConfig());
372
+ contextSent.resolve();
373
+ }
374
+
375
+ // If a fetch is running (stale/missing cache), wait for fresh data and update
376
+ if (configPromise) {
377
+ const merchantConfig = await configPromise;
378
+ mergeAndSetConfig(merchantConfig);
379
+ sendLifecycleConfig(sdkConfigStore.getConfig());
380
+ contextSent.resolve();
381
+ }
382
+
253
383
  // Push raw CSS if needed
254
384
  async function pushCss() {
255
385
  const cssLink = config.customizations?.css;
256
386
  if (!cssLink) return;
257
-
258
- const message = {
387
+ rpcClient.sendLifecycle({
259
388
  clientLifecycle: "modal-css" as const,
260
389
  data: { cssLink },
261
- };
262
- rpcClient.sendLifecycle(message);
390
+ });
263
391
  }
264
392
 
265
393
  // Push i18n if needed
266
394
  async function pushI18n() {
267
395
  const i18n = config.customizations?.i18n;
268
396
  if (!i18n) return;
269
-
270
- const message = {
397
+ rpcClient.sendLifecycle({
271
398
  clientLifecycle: "modal-i18n" as const,
272
399
  data: { i18n },
273
- };
274
- rpcClient.sendLifecycle(message);
400
+ });
275
401
  }
276
402
 
277
403
  // Push local backup if needed
278
404
  async function pushBackup() {
279
405
  if (typeof window === "undefined") return;
280
-
281
406
  const backup = window.localStorage.getItem(BACKUP_KEY);
282
407
  if (!backup) return;
283
-
284
- const message = {
408
+ rpcClient.sendLifecycle({
285
409
  clientLifecycle: "restore-backup" as const,
286
410
  data: { backup },
287
- };
288
- rpcClient.sendLifecycle(message);
411
+ });
289
412
  }
290
413
 
291
414
  await Promise.allSettled([pushCss(), pushI18n(), pushBackup()]);