@frak-labs/core-sdk 0.2.0 → 0.2.1-beta.06c52c98

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 (67) 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 +2 -2
  5. package/dist/actions.d.ts +2 -2
  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-Raks6FXg.d.cts → computeLegacyProductId-BP-ciVsp.d.cts} +73 -88
  12. package/dist/{computeLegacyProductId-BkyJ4rEY.d.ts → computeLegacyProductId-DiJd7RNo.d.ts} +73 -88
  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-BCJGchIb.d.cts → openSso-B8v3Vtnh.d.ts} +157 -52
  18. package/dist/{openSso-DG-_9CED.d.ts → openSso-n_B4LSuW.d.cts} +157 -52
  19. package/dist/setupClient-Dr_UYfTD.cjs +13 -0
  20. package/dist/setupClient-TuhDjVJx.js +13 -0
  21. package/dist/siweAuthenticate-0UPcUqI1.js +1 -0
  22. package/dist/{siweAuthenticate-Btem4QHs.d.ts → siweAuthenticate-CDCsp8EJ.d.ts} +35 -36
  23. package/dist/siweAuthenticate-CfQibjZR.cjs +1 -0
  24. package/dist/{siweAuthenticate-BH7Dn7nZ.d.cts → siweAuthenticate-yITE-iKh.d.cts} +35 -36
  25. package/dist/trackEvent-5j5kkOCj.js +1 -0
  26. package/dist/trackEvent-B2uom25e.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/ensureIdentity.ts +2 -2
  31. package/src/actions/referral/processReferral.test.ts +109 -125
  32. package/src/actions/referral/processReferral.ts +134 -180
  33. package/src/actions/referral/referralInteraction.test.ts +3 -5
  34. package/src/actions/referral/referralInteraction.ts +2 -7
  35. package/src/actions/trackPurchaseStatus.test.ts +32 -20
  36. package/src/actions/trackPurchaseStatus.ts +3 -5
  37. package/src/actions/wrapper/modalBuilder.test.ts +4 -2
  38. package/src/actions/wrapper/modalBuilder.ts +6 -8
  39. package/src/clients/createIFrameFrakClient.ts +146 -25
  40. package/src/clients/transports/iframeLifecycleManager.test.ts +0 -80
  41. package/src/clients/transports/iframeLifecycleManager.ts +0 -44
  42. package/src/index.ts +8 -3
  43. package/src/types/config.ts +10 -3
  44. package/src/types/context.ts +48 -6
  45. package/src/types/index.ts +8 -2
  46. package/src/types/lifecycle/client.ts +22 -27
  47. package/src/types/lifecycle/iframe.ts +0 -8
  48. package/src/types/resolvedConfig.ts +104 -0
  49. package/src/types/rpc/interaction.ts +9 -0
  50. package/src/types/rpc.ts +7 -5
  51. package/src/types/tracking.ts +5 -34
  52. package/src/utils/FrakContext.test.ts +270 -186
  53. package/src/utils/FrakContext.ts +78 -56
  54. package/src/utils/backendUrl.test.ts +2 -2
  55. package/src/utils/backendUrl.ts +1 -1
  56. package/src/utils/index.ts +1 -5
  57. package/src/utils/sdkConfigStore.test.ts +405 -0
  58. package/src/utils/sdkConfigStore.ts +277 -0
  59. package/src/utils/sso.ts +3 -7
  60. package/dist/setupClient-CQrMDGyZ.js +0 -13
  61. package/dist/setupClient-Ccv3XxwL.cjs +0 -13
  62. package/dist/siweAuthenticate-BJHbtty4.js +0 -1
  63. package/dist/siweAuthenticate-Cwj3HP0m.cjs +0 -1
  64. package/dist/trackEvent-M2RLTQ2p.js +0 -1
  65. package/dist/trackEvent-T_R9ER2S.cjs +0 -1
  66. package/src/utils/merchantId.test.ts +0 -653
  67. package/src/utils/merchantId.ts +0 -143
@@ -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,11 @@ 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";
13
15
  import { BACKUP_KEY } from "../utils/constants";
16
+ import { sdkConfigStore } from "../utils/sdkConfigStore";
14
17
  import { setupSsoUrlListener } from "../utils/ssoUrlListener";
15
18
  import { DebugInfoGatherer } from "./DebugInfo";
16
19
  import {
@@ -19,12 +22,13 @@ import {
19
22
  } from "./transports/iframeLifecycleManager";
20
23
 
21
24
  type SdkRpcClient = RpcClient<IFrameRpcSchema, FrakLifecycleEvent>;
25
+ type MerchantConfigResult = Awaited<ReturnType<typeof sdkConfigStore.resolve>>;
22
26
 
23
27
  /**
24
28
  * Create a new iframe Frak client
25
29
  * @param args
26
30
  * @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).
31
+ * 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
32
  * @param args.iframe - The iframe to use for the communication
29
33
  * @returns The created Frak Client
30
34
  *
@@ -46,13 +50,35 @@ export function createIFrameFrakClient({
46
50
  }): FrakClient {
47
51
  const frakWalletUrl = config?.walletUrl ?? "https://wallet.frak.id";
48
52
 
53
+ const browserLang =
54
+ typeof navigator !== "undefined"
55
+ ? navigator.language?.split("-")[0]
56
+ : undefined;
57
+ const detectedLang =
58
+ config.metadata.lang ??
59
+ (browserLang === "en" || browserLang === "fr"
60
+ ? browserLang
61
+ : undefined);
62
+ const targetDomain =
63
+ config.domain ??
64
+ (typeof window !== "undefined" ? window.location.hostname : "");
65
+ sdkConfigStore.setCacheScope(targetDomain, detectedLang);
66
+ sdkConfigStore.reset();
67
+
68
+ // Skip fetch entirely if cache is fresh, otherwise fetch (SWR)
69
+ const configPromise = sdkConfigStore.isCacheFresh
70
+ ? undefined
71
+ : sdkConfigStore.resolve(config.domain, config.walletUrl, detectedLang);
72
+
49
73
  // Create lifecycle manager
50
74
  const lifecycleManager = createIFrameLifecycleManager({
51
75
  iframe,
52
76
  targetOrigin: frakWalletUrl,
53
- configDomain: config.domain,
54
77
  });
55
78
 
79
+ // Resolved after first resolved-config is sent to iframe (prevents RPC before context exists)
80
+ const contextSent = new Deferred<void>();
81
+
56
82
  // Create our debug info gatherer
57
83
  const debugInfo = new DebugInfoGatherer(config, iframe);
58
84
 
@@ -70,10 +96,9 @@ export function createIFrameFrakClient({
70
96
  listeningTransport: window,
71
97
  targetOrigin: frakWalletUrl,
72
98
  middleware: [
73
- // Ensure we are connected before sending request
99
+ // Ensure we are connected and context is sent before sending request
74
100
  {
75
101
  async onRequest(_message, ctx) {
76
- // Ensure the iframe is connected
77
102
  const isConnected = await lifecycleManager.isConnected;
78
103
  if (!isConnected) {
79
104
  throw new FrakRpcError(
@@ -81,6 +106,7 @@ export function createIFrameFrakClient({
81
106
  "The iframe provider isn't connected yet"
82
107
  );
83
108
  }
109
+ await contextSent.promise;
84
110
  return ctx;
85
111
  },
86
112
  },
@@ -108,14 +134,12 @@ export function createIFrameFrakClient({
108
134
  // Setup heartbeat
109
135
  const stopHeartbeat = setupHeartbeat(rpcClient, lifecycleManager);
110
136
 
111
- // Build our destroy function
112
137
  const destroy = async () => {
113
- // Stop heartbeat
114
138
  stopHeartbeat();
115
- // Cleanup the RPC client
116
139
  rpcClient.cleanup();
117
- // Remove the iframe
118
140
  iframe.remove();
141
+ sdkConfigStore.clearCache();
142
+ sdkConfigStore.reset();
119
143
  };
120
144
 
121
145
  // Init open panel
@@ -161,7 +185,14 @@ export function createIFrameFrakClient({
161
185
  config,
162
186
  rpcClient,
163
187
  lifecycleManager,
164
- }).then(() => debugInfo.updateSetupStatus(true));
188
+ configPromise,
189
+ contextSent,
190
+ })
191
+ .then(() => debugInfo.updateSetupStatus(true))
192
+ .catch((err) => {
193
+ contextSent.reject(err);
194
+ throw err;
195
+ });
165
196
 
166
197
  return {
167
198
  config,
@@ -238,54 +269,144 @@ async function postConnectionSetup({
238
269
  config,
239
270
  rpcClient,
240
271
  lifecycleManager,
272
+ configPromise,
273
+ contextSent,
241
274
  }: {
242
275
  config: FrakWalletSdkConfig;
243
276
  rpcClient: SdkRpcClient;
244
277
  lifecycleManager: IframeLifecycleManager;
278
+ configPromise: Promise<MerchantConfigResult> | undefined;
279
+ contextSent: Deferred<void>;
245
280
  }): Promise<void> {
246
- // Wait for the handler to be connected
247
281
  await lifecycleManager.isConnected;
248
282
 
249
- // Setup SSO URL listener to detect and forward SSO redirects
250
- // This checks for ?sso= parameter and forwards compressed data to iframe
251
283
  setupSsoUrlListener(rpcClient, lifecycleManager.isConnected);
252
284
 
285
+ // Read and consume the pending merge token from URL (SSO identity merge)
286
+ const url = new URL(window.location.href);
287
+ const pendingMergeToken = url.searchParams.get("fmt") ?? undefined;
288
+ if (pendingMergeToken) {
289
+ url.searchParams.delete("fmt");
290
+ window.history.replaceState({}, "", url.toString());
291
+ }
292
+
293
+ // Merge a raw backend response with SDK metadata and persist to store
294
+ const mergeAndSetConfig = (merchantConfig: MerchantConfigResult) => {
295
+ const merchantId =
296
+ merchantConfig?.merchantId ?? config.metadata.merchantId ?? "";
297
+ const domain = merchantConfig?.domain ?? "";
298
+ const allowedDomains = merchantConfig?.allowedDomains ?? [];
299
+ const raw = merchantConfig?.sdkConfig;
300
+
301
+ sdkConfigStore.setConfig(
302
+ raw
303
+ ? {
304
+ isResolved: true,
305
+ merchantId,
306
+ domain,
307
+ allowedDomains,
308
+ hasRawSdkConfig: true,
309
+ name: raw.name ?? config.metadata.name,
310
+ logoUrl: raw.logoUrl ?? config.metadata.logoUrl,
311
+ homepageLink:
312
+ raw.homepageLink ?? config.metadata.homepageLink,
313
+ lang: raw.lang ?? config.metadata.lang,
314
+ currency: raw.currency ?? config.metadata.currency,
315
+ hidden: raw.hidden,
316
+ css: raw.css,
317
+ translations: raw.translations,
318
+ placements: raw.placements,
319
+ }
320
+ : {
321
+ isResolved: true,
322
+ merchantId,
323
+ domain,
324
+ allowedDomains,
325
+ name: config.metadata.name,
326
+ logoUrl: config.metadata.logoUrl,
327
+ homepageLink: config.metadata.homepageLink,
328
+ lang: config.metadata.lang,
329
+ currency: config.metadata.currency,
330
+ }
331
+ );
332
+ };
333
+
334
+ // Send the resolved-config lifecycle event to the iframe
335
+ let mergeTokenConsumed = false;
336
+ const sendLifecycleConfig = (resolved: SdkResolvedConfig) => {
337
+ const token = mergeTokenConsumed ? undefined : pendingMergeToken;
338
+ mergeTokenConsumed = true;
339
+
340
+ const sdkConfig = resolved.hasRawSdkConfig
341
+ ? {
342
+ name: resolved.name,
343
+ logoUrl: resolved.logoUrl,
344
+ homepageLink: resolved.homepageLink,
345
+ lang: resolved.lang,
346
+ currency: resolved.currency,
347
+ hidden: resolved.hidden,
348
+ css: resolved.css,
349
+ translations: resolved.translations,
350
+ placements: resolved.placements,
351
+ }
352
+ : undefined;
353
+
354
+ rpcClient.sendLifecycle({
355
+ clientLifecycle: "resolved-config",
356
+ data: {
357
+ merchantId: resolved.merchantId,
358
+ domain: resolved.domain ?? "",
359
+ allowedDomains: resolved.allowedDomains ?? [],
360
+ sourceUrl: window.location.href,
361
+ ...(token && { pendingMergeToken: token }),
362
+ ...(sdkConfig && { sdkConfig }),
363
+ },
364
+ });
365
+ };
366
+
367
+ // SWR: if we have cached data, send it to the iframe immediately
368
+ if (sdkConfigStore.isResolved) {
369
+ sendLifecycleConfig(sdkConfigStore.getConfig());
370
+ contextSent.resolve();
371
+ }
372
+
373
+ // If a fetch is running (stale/missing cache), wait for fresh data and update
374
+ if (configPromise) {
375
+ const merchantConfig = await configPromise;
376
+ mergeAndSetConfig(merchantConfig);
377
+ sendLifecycleConfig(sdkConfigStore.getConfig());
378
+ contextSent.resolve();
379
+ }
380
+
253
381
  // Push raw CSS if needed
254
382
  async function pushCss() {
255
383
  const cssLink = config.customizations?.css;
256
384
  if (!cssLink) return;
257
-
258
- const message = {
385
+ rpcClient.sendLifecycle({
259
386
  clientLifecycle: "modal-css" as const,
260
387
  data: { cssLink },
261
- };
262
- rpcClient.sendLifecycle(message);
388
+ });
263
389
  }
264
390
 
265
391
  // Push i18n if needed
266
392
  async function pushI18n() {
267
393
  const i18n = config.customizations?.i18n;
268
394
  if (!i18n) return;
269
-
270
- const message = {
395
+ rpcClient.sendLifecycle({
271
396
  clientLifecycle: "modal-i18n" as const,
272
397
  data: { i18n },
273
- };
274
- rpcClient.sendLifecycle(message);
398
+ });
275
399
  }
276
400
 
277
401
  // Push local backup if needed
278
402
  async function pushBackup() {
279
403
  if (typeof window === "undefined") return;
280
-
281
404
  const backup = window.localStorage.getItem(BACKUP_KEY);
282
405
  if (!backup) return;
283
-
284
- const message = {
406
+ rpcClient.sendLifecycle({
285
407
  clientLifecycle: "restore-backup" as const,
286
408
  data: { backup },
287
- };
288
- rpcClient.sendLifecycle(message);
409
+ });
289
410
  }
290
411
 
291
412
  await Promise.allSettled([pushCss(), pushI18n(), pushBackup()]);
@@ -233,86 +233,6 @@ describe("createIFrameLifecycleManager", () => {
233
233
  });
234
234
  });
235
235
 
236
- describe("handshake event", () => {
237
- test("should post handshake-response with token to iframe origin", async () => {
238
- const { createIFrameLifecycleManager } = await import(
239
- "./iframeLifecycleManager"
240
- );
241
-
242
- const mockPostMessage = vi.fn();
243
- const mockIframe = {
244
- src: "https://wallet.frak.id/listener",
245
- contentWindow: {
246
- postMessage: mockPostMessage,
247
- },
248
- } as any;
249
-
250
- const manager = createIFrameLifecycleManager({
251
- iframe: mockIframe,
252
- targetOrigin: WALLET_ORIGIN,
253
- });
254
-
255
- const event = {
256
- iframeLifecycle: "handshake" as const,
257
- data: { token: "handshake-token-123" },
258
- };
259
-
260
- await manager.handleEvent(event);
261
-
262
- expect(mockPostMessage).toHaveBeenCalledWith(
263
- {
264
- clientLifecycle: "handshake-response",
265
- data: {
266
- token: "handshake-token-123",
267
- currentUrl: "https://test.com",
268
- clientId: "mock-client-id",
269
- },
270
- },
271
- "https://wallet.frak.id"
272
- );
273
- });
274
-
275
- test("should include current URL in handshake response", async () => {
276
- const { createIFrameLifecycleManager } = await import(
277
- "./iframeLifecycleManager"
278
- );
279
-
280
- Object.defineProperty(window, "location", {
281
- value: { href: "https://example.com/page?param=value" },
282
- writable: true,
283
- });
284
-
285
- const mockPostMessage = vi.fn();
286
- const mockIframe = {
287
- src: "https://wallet.frak.id/listener",
288
- contentWindow: {
289
- postMessage: mockPostMessage,
290
- },
291
- } as any;
292
-
293
- const manager = createIFrameLifecycleManager({
294
- iframe: mockIframe,
295
- targetOrigin: WALLET_ORIGIN,
296
- });
297
-
298
- const event = {
299
- iframeLifecycle: "handshake" as const,
300
- data: { token: "token" },
301
- };
302
-
303
- await manager.handleEvent(event);
304
-
305
- expect(mockPostMessage).toHaveBeenCalledWith(
306
- expect.objectContaining({
307
- data: expect.objectContaining({
308
- currentUrl: "https://example.com/page?param=value",
309
- }),
310
- }),
311
- "https://wallet.frak.id"
312
- );
313
- });
314
- });
315
-
316
236
  describe("redirect event", () => {
317
237
  test("should redirect with appended current URL for HTTP URLs", async () => {
318
238
  const { createIFrameLifecycleManager } = await import(
@@ -1,6 +1,5 @@
1
1
  import { Deferred } from "@frak-labs/frame-connector";
2
2
  import type { FrakLifecycleEvent } from "../../types";
3
- import { getClientId } from "../../utils/clientId";
4
3
  import { BACKUP_KEY } from "../../utils/constants";
5
4
  import {
6
5
  isFrakDeepLink,
@@ -47,42 +46,6 @@ function handleBackup(backup: string | undefined): void {
47
46
  }
48
47
  }
49
48
 
50
- /**
51
- * Handle handshake with iframe — sends client metadata so the listener can resolve the correct merchant
52
- * @param iframe - The iframe element to post the handshake response to
53
- * @param token - The handshake token received from the iframe
54
- * @param targetOrigin - The target origin for postMessage security
55
- * @param configDomain - Optional override domain for merchant resolution in tunneled/proxied environments
56
- */
57
- function handleHandshake(
58
- iframe: HTMLIFrameElement,
59
- token: string,
60
- targetOrigin: string,
61
- configDomain?: string
62
- ): void {
63
- const url = new URL(window.location.href);
64
- const pendingMergeToken = url.searchParams.get("fmt") ?? undefined;
65
-
66
- iframe.contentWindow?.postMessage(
67
- {
68
- clientLifecycle: "handshake-response",
69
- data: {
70
- token,
71
- currentUrl: window.location.href,
72
- pendingMergeToken,
73
- configDomain,
74
- clientId: getClientId(),
75
- },
76
- },
77
- targetOrigin
78
- );
79
-
80
- if (pendingMergeToken) {
81
- url.searchParams.delete("fmt");
82
- window.history.replaceState({}, "", url.toString());
83
- }
84
- }
85
-
86
49
  /**
87
50
  * Compute final redirect URL with parameter substitution
88
51
  */
@@ -167,17 +130,14 @@ function handleRedirect(
167
130
  * @param args
168
131
  * @param args.iframe - The iframe element used for wallet communication
169
132
  * @param args.targetOrigin - The wallet URL origin for postMessage security
170
- * @param args.configDomain - Optional domain override forwarded during handshake for tunneled/proxied environments
171
133
  * @ignore
172
134
  */
173
135
  export function createIFrameLifecycleManager({
174
136
  iframe,
175
137
  targetOrigin,
176
- configDomain,
177
138
  }: {
178
139
  iframe: HTMLIFrameElement;
179
140
  targetOrigin: string;
180
- configDomain?: string;
181
141
  }): IframeLifecycleManager {
182
142
  // Create the isConnected listener
183
143
  const isConnectedDeferred = new Deferred<boolean>();
@@ -206,10 +166,6 @@ export function createIFrameLifecycleManager({
206
166
  case "hide":
207
167
  changeIframeVisibility({ iframe, isVisible: event === "show" });
208
168
  break;
209
- // Handshake handling
210
- case "handshake":
211
- handleHandshake(iframe, data.token, targetOrigin, configDomain);
212
- break;
213
169
  // Redirect handling
214
170
  case "redirect":
215
171
  handleRedirect(
package/src/index.ts CHANGED
@@ -28,6 +28,8 @@ export type {
28
28
  FrakClient,
29
29
  // Utils
30
30
  FrakContext,
31
+ FrakContextV1,
32
+ FrakContextV2,
31
33
  FrakLifecycleEvent,
32
34
  // Config
33
35
  FrakWalletSdkConfig,
@@ -45,6 +47,7 @@ export type {
45
47
  LoggedInEmbeddedView,
46
48
  LoggedOutEmbeddedView,
47
49
  LoginModalStepType,
50
+ MerchantConfigResponse,
48
51
  ModalRpcMetadata,
49
52
  ModalRpcStepsInput,
50
53
  ModalRpcStepsResultType,
@@ -56,7 +59,10 @@ export type {
56
59
  OpenSsoReturnType,
57
60
  PrepareSsoParamsType,
58
61
  PrepareSsoReturnType,
62
+ ResolvedPlacement,
63
+ ResolvedSdkConfig,
59
64
  RewardTier,
65
+ SdkResolvedConfig,
60
66
  // RPC Interaction
61
67
  SendInteractionParamsType,
62
68
  SendTransactionModalStepType,
@@ -74,6 +80,7 @@ export type {
74
80
  // Rpc
75
81
  WalletStatusReturnType,
76
82
  } from "./types";
83
+ export { isV1Context, isV2Context } from "./types";
77
84
  // Utils
78
85
  export {
79
86
  type AppSpecificSsoMetadata,
@@ -81,7 +88,6 @@ export {
81
88
  base64urlEncode,
82
89
  baseIframeProps,
83
90
  type CompressedSsoData,
84
- clearMerchantIdCache,
85
91
  compressJsonToB64,
86
92
  createIframe,
87
93
  DEEP_LINK_SCHEME,
@@ -90,7 +96,6 @@ export {
90
96
  FrakContextManager,
91
97
  type FrakEvent,
92
98
  type FullSsoParams,
93
- fetchMerchantId,
94
99
  findIframeInOpener,
95
100
  formatAmount,
96
101
  generateSsoUrl,
@@ -101,7 +106,7 @@ export {
101
106
  getSupportedLocale,
102
107
  isChromiumAndroid,
103
108
  isFrakDeepLink,
104
- resolveMerchantId,
109
+ sdkConfigStore,
105
110
  toAndroidIntentUrl,
106
111
  trackEvent,
107
112
  triggerDeepLinkWithFallback,
@@ -27,7 +27,7 @@ export type FrakWalletSdkConfig = {
27
27
  /**
28
28
  * Your application name (will be displayed in a few modals and in SSO)
29
29
  */
30
- name: string;
30
+ name?: string;
31
31
  /**
32
32
  * Your merchant ID from the Frak dashboard (UUID format)
33
33
  * Used for referral tracking and analytics
@@ -71,6 +71,13 @@ export type FrakWalletSdkConfig = {
71
71
  * @defaultValue window.location.host
72
72
  */
73
73
  domain?: string;
74
+ /**
75
+ * Wait for backend config before rendering components.
76
+ * When true (default), components show a spinner until backend config is resolved.
77
+ * When false, components render immediately with SDK static config / HTML attributes.
78
+ * @defaultValue true
79
+ */
80
+ waitForBackendConfig?: boolean;
74
81
  };
75
82
 
76
83
  /**
@@ -111,7 +118,7 @@ export type I18nConfig =
111
118
  | LocalizedI18nConfig;
112
119
 
113
120
  /**
114
- * A localized i18n config
121
+ * A localized i18n config (inline objects only — URL-based i18n removed)
115
122
  * @category Config
116
123
  */
117
- export type LocalizedI18nConfig = `${string}.css` | { [key: string]: string };
124
+ export type LocalizedI18nConfig = { [key: string]: string };
@@ -1,13 +1,55 @@
1
1
  import type { Address } from "viem";
2
2
 
3
3
  /**
4
- * The current Frak Context
5
- *
6
- * For now, only contain a referrer address.
7
- *
4
+ * V1 (legacy) Frak Context — contains only the referrer wallet address.
5
+ * Used for backward compatibility with old sharing links.
8
6
  * @ignore
9
7
  */
10
- export type FrakContext = {
11
- // Referrer address
8
+ export type FrakContextV1 = {
9
+ /** Referrer wallet address */
12
10
  r: Address;
13
11
  };
12
+
13
+ /**
14
+ * V2 Frak Context — anonymous-first referral context.
15
+ * Contains the sharer's clientId, merchantId, and link creation timestamp.
16
+ * @ignore
17
+ */
18
+ export type FrakContextV2 = {
19
+ /** Version discriminator */
20
+ v: 2;
21
+ /** Sharer's anonymous clientId (UUID from localStorage) */
22
+ c: string;
23
+ /** Merchant ID (UUID) */
24
+ m: string;
25
+ /** Link creation timestamp (epoch seconds) */
26
+ t: number;
27
+ };
28
+
29
+ /**
30
+ * The current Frak Context — union of all versions.
31
+ *
32
+ * - No `v` field → V1 (legacy wallet address)
33
+ * - `v: 2` → V2 (anonymous clientId-based)
34
+ *
35
+ * @ignore
36
+ */
37
+ export type FrakContext = FrakContextV1 | FrakContextV2;
38
+
39
+ /**
40
+ * Type guard: check if a context is V1 (legacy wallet address).
41
+ * @param ctx - The Frak context to check
42
+ * @returns True if the context is a V1 context
43
+ */
44
+ export function isV1Context(ctx: FrakContext): ctx is FrakContextV1 {
45
+ return "r" in ctx && !("v" in ctx);
46
+ }
47
+
48
+ /**
49
+ * Type guard: check if a context is V2 (anonymous clientId-based).
50
+ * @param ctx - The Frak context to check
51
+ * @returns True if the context is a V2 context
52
+ */
53
+ export function isV2Context(ctx: FrakContext): ctx is FrakContextV2 {
54
+ return "v" in ctx && ctx.v === 2;
55
+ }
@@ -15,12 +15,18 @@ export type {
15
15
  LocalizedI18nConfig,
16
16
  } from "./config";
17
17
  // Utils
18
- export type { FrakContext } from "./context";
19
-
18
+ export type { FrakContext, FrakContextV1, FrakContextV2 } from "./context";
19
+ export { isV1Context, isV2Context } from "./context";
20
20
  export type {
21
21
  ClientLifecycleEvent,
22
22
  IFrameLifecycleEvent,
23
23
  } from "./lifecycle";
24
+ export type {
25
+ MerchantConfigResponse,
26
+ ResolvedPlacement,
27
+ ResolvedSdkConfig,
28
+ SdkResolvedConfig,
29
+ } from "./resolvedConfig";
24
30
  export type { IFrameRpcSchema } from "./rpc";
25
31
  // Modal related
26
32
  export type {
@@ -1,4 +1,5 @@
1
1
  import type { I18nConfig } from "../config";
2
+ import type { ResolvedSdkConfig } from "../resolvedConfig";
2
3
 
3
4
  /**
4
5
  * Event related to the iframe lifecycle
@@ -9,9 +10,9 @@ export type ClientLifecycleEvent =
9
10
  | CustomI18nEvent
10
11
  | RestoreBackupEvent
11
12
  | HearbeatEvent
12
- | HandshakeResponse
13
13
  | SsoRedirectCompleteEvent
14
- | DeepLinkFailedEvent;
14
+ | DeepLinkFailedEvent
15
+ | ResolvedConfigEvent;
15
16
 
16
17
  type CustomCssEvent = {
17
18
  clientLifecycle: "modal-css";
@@ -33,31 +34,6 @@ type HearbeatEvent = {
33
34
  data?: never;
34
35
  };
35
36
 
36
- type HandshakeResponse = {
37
- clientLifecycle: "handshake-response";
38
- data: {
39
- token: string;
40
- currentUrl: string;
41
- /**
42
- * Pending merge token extracted from URL (?fmt= parameter)
43
- * When present, listener should execute identity merge in background
44
- * URL is cleaned after handshake response is sent
45
- */
46
- pendingMergeToken?: string;
47
- /**
48
- * Client ID for identity tracking (belt & suspenders fallback)
49
- * Primary delivery is via iframe URL query param; handshake is backup for SSR
50
- */
51
- clientId?: string;
52
- /**
53
- * Explicit domain from SDK config (FrakWalletSdkConfig.domain)
54
- * When present, listener should prefer this over URL-derived domain
55
- * for merchant resolution (handles proxied/tunneled environments)
56
- */
57
- configDomain?: string;
58
- };
59
- };
60
-
61
37
  type SsoRedirectCompleteEvent = {
62
38
  clientLifecycle: "sso-redirect-complete";
63
39
  data: { compressed: string };
@@ -67,3 +43,22 @@ type DeepLinkFailedEvent = {
67
43
  clientLifecycle: "deep-link-failed";
68
44
  data: { originalUrl: string };
69
45
  };
46
+
47
+ type ResolvedConfigEvent = {
48
+ clientLifecycle: "resolved-config";
49
+ data: {
50
+ merchantId: string;
51
+ /** The domain the backend resolved this config for */
52
+ domain: string;
53
+ /** All domains registered for this merchant (for domain proof) */
54
+ allowedDomains: string[];
55
+ /** Full URL of the parent page (for interaction tracking) */
56
+ sourceUrl: string;
57
+ /**
58
+ * Pending merge token extracted from URL (?fmt= parameter).
59
+ * When present, listener should execute identity merge in background.
60
+ */
61
+ pendingMergeToken?: string;
62
+ sdkConfig?: ResolvedSdkConfig;
63
+ };
64
+ };