@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.
- package/README.md +1 -2
- package/cdn/bundle.js +55 -3
- package/dist/actions.cjs +1 -1
- package/dist/actions.d.cts +2 -2
- package/dist/actions.d.ts +2 -2
- package/dist/actions.js +1 -1
- package/dist/bundle.cjs +1 -1
- package/dist/bundle.d.cts +4 -4
- package/dist/bundle.d.ts +4 -4
- package/dist/bundle.js +1 -1
- package/dist/{computeLegacyProductId-Raks6FXg.d.cts → computeLegacyProductId-BP-ciVsp.d.cts} +73 -88
- package/dist/{computeLegacyProductId-BkyJ4rEY.d.ts → computeLegacyProductId-DiJd7RNo.d.ts} +73 -88
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/{openSso-BCJGchIb.d.cts → openSso-B8v3Vtnh.d.ts} +157 -52
- package/dist/{openSso-DG-_9CED.d.ts → openSso-n_B4LSuW.d.cts} +157 -52
- package/dist/setupClient-Dr_UYfTD.cjs +13 -0
- package/dist/setupClient-TuhDjVJx.js +13 -0
- package/dist/siweAuthenticate-0UPcUqI1.js +1 -0
- package/dist/{siweAuthenticate-Btem4QHs.d.ts → siweAuthenticate-CDCsp8EJ.d.ts} +35 -36
- package/dist/siweAuthenticate-CfQibjZR.cjs +1 -0
- package/dist/{siweAuthenticate-BH7Dn7nZ.d.cts → siweAuthenticate-yITE-iKh.d.cts} +35 -36
- package/dist/trackEvent-5j5kkOCj.js +1 -0
- package/dist/trackEvent-B2uom25e.cjs +1 -0
- package/package.json +8 -8
- package/src/actions/displayEmbeddedWallet.ts +6 -2
- package/src/actions/displayModal.ts +6 -2
- package/src/actions/ensureIdentity.ts +2 -2
- package/src/actions/referral/processReferral.test.ts +109 -125
- package/src/actions/referral/processReferral.ts +134 -180
- package/src/actions/referral/referralInteraction.test.ts +3 -5
- package/src/actions/referral/referralInteraction.ts +2 -7
- package/src/actions/trackPurchaseStatus.test.ts +32 -20
- package/src/actions/trackPurchaseStatus.ts +3 -5
- package/src/actions/wrapper/modalBuilder.test.ts +4 -2
- package/src/actions/wrapper/modalBuilder.ts +6 -8
- package/src/clients/createIFrameFrakClient.ts +146 -25
- package/src/clients/transports/iframeLifecycleManager.test.ts +0 -80
- package/src/clients/transports/iframeLifecycleManager.ts +0 -44
- package/src/index.ts +8 -3
- package/src/types/config.ts +10 -3
- package/src/types/context.ts +48 -6
- package/src/types/index.ts +8 -2
- package/src/types/lifecycle/client.ts +22 -27
- package/src/types/lifecycle/iframe.ts +0 -8
- package/src/types/resolvedConfig.ts +104 -0
- package/src/types/rpc/interaction.ts +9 -0
- package/src/types/rpc.ts +7 -5
- package/src/types/tracking.ts +5 -34
- package/src/utils/FrakContext.test.ts +270 -186
- package/src/utils/FrakContext.ts +78 -56
- package/src/utils/backendUrl.test.ts +2 -2
- package/src/utils/backendUrl.ts +1 -1
- package/src/utils/index.ts +1 -5
- package/src/utils/sdkConfigStore.test.ts +405 -0
- package/src/utils/sdkConfigStore.ts +277 -0
- package/src/utils/sso.ts +3 -7
- package/dist/setupClient-CQrMDGyZ.js +0 -13
- package/dist/setupClient-Ccv3XxwL.cjs +0 -13
- package/dist/siweAuthenticate-BJHbtty4.js +0 -1
- package/dist/siweAuthenticate-Cwj3HP0m.cjs +0 -1
- package/dist/trackEvent-M2RLTQ2p.js +0 -1
- package/dist/trackEvent-T_R9ER2S.cjs +0 -1
- package/src/utils/merchantId.test.ts +0 -653
- 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
|
|
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
|
-
|
|
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
|
-
|
|
109
|
+
sdkConfigStore,
|
|
105
110
|
toAndroidIntentUrl,
|
|
106
111
|
trackEvent,
|
|
107
112
|
triggerDeepLinkWithFallback,
|
package/src/types/config.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
124
|
+
export type LocalizedI18nConfig = { [key: string]: string };
|
package/src/types/context.ts
CHANGED
|
@@ -1,13 +1,55 @@
|
|
|
1
1
|
import type { Address } from "viem";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
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
|
|
11
|
-
|
|
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
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
+
};
|