@frak-labs/core-sdk 0.2.1 → 1.0.0-beta.61e6fb99
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 +3 -3
- package/dist/actions-Di4welXI.cjs +1 -0
- package/dist/actions-DyMkUe65.js +1 -0
- package/dist/actions.cjs +1 -1
- package/dist/actions.d.cts +3 -3
- package/dist/actions.d.ts +3 -3
- 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-CCAZvLa5.d.cts → index-B_Uj-puh.d.ts} +249 -73
- package/dist/{computeLegacyProductId-b5cUWdAm.d.ts → index-ByVpu25D.d.cts} +249 -73
- package/dist/{siweAuthenticate-CnCZ7mok.d.ts → index-CGyEOo9J.d.cts} +122 -8
- package/dist/{siweAuthenticate-CVigMOxz.d.cts → index-Cdf5j2_W.d.ts} +122 -8
- 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-B0g7-807.d.cts → openSso-B6pD2oA6.d.ts} +380 -46
- package/dist/{openSso-CMzwvaCa.d.ts → openSso-qjaccFd0.d.cts} +379 -45
- package/dist/sdkConfigStore-DvwFc6Ym.cjs +1 -0
- package/dist/sdkConfigStore-M37skmM8.js +1 -0
- package/dist/src-BqpqVHCq.cjs +13 -0
- package/dist/src-BxRYON49.js +13 -0
- package/package.json +12 -13
- package/src/actions/displayEmbeddedWallet.ts +6 -2
- package/src/actions/displayModal.ts +6 -2
- package/src/actions/displaySharingPage.ts +49 -0
- package/src/actions/ensureIdentity.ts +2 -2
- package/src/actions/getMerchantInformation.test.ts +13 -1
- package/src/actions/getMerchantInformation.ts +20 -5
- package/src/actions/getMergeToken.ts +33 -0
- package/src/actions/getUserReferralStatus.ts +42 -0
- package/src/actions/index.ts +8 -1
- package/src/actions/referral/processReferral.test.ts +4 -8
- package/src/actions/referral/processReferral.ts +5 -11
- package/src/actions/referral/setupReferral.test.ts +79 -0
- package/src/actions/referral/setupReferral.ts +32 -0
- 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 +233 -28
- package/src/clients/transports/iframeLifecycleManager.test.ts +14 -94
- package/src/clients/transports/iframeLifecycleManager.ts +35 -53
- package/src/index.ts +25 -5
- package/src/stubs/rrweb.ts +9 -0
- package/src/types/config.ts +19 -3
- package/src/types/index.ts +15 -1
- package/src/types/lifecycle/client.ts +29 -27
- package/src/types/lifecycle/iframe.ts +7 -8
- package/src/types/resolvedConfig.ts +138 -0
- package/src/types/rpc/displaySharingPage.ts +100 -0
- package/src/types/rpc/embedded/index.ts +1 -1
- package/src/types/rpc/interaction.ts +4 -0
- package/src/types/rpc/userReferralStatus.ts +20 -0
- package/src/types/rpc.ts +54 -5
- package/src/types/tracking.ts +36 -0
- package/src/utils/FrakContext.test.ts +151 -0
- package/src/utils/FrakContext.ts +67 -1
- package/src/utils/analytics/events/component.ts +58 -0
- package/src/utils/analytics/events/index.ts +20 -0
- package/src/utils/analytics/events/lifecycle.ts +26 -0
- package/src/utils/analytics/events/referral.ts +10 -0
- package/src/utils/analytics/index.ts +8 -0
- package/src/utils/{trackEvent.test.ts → analytics/trackEvent.test.ts} +22 -30
- package/src/utils/analytics/trackEvent.ts +34 -0
- package/src/utils/backendUrl.test.ts +2 -2
- package/src/utils/backendUrl.ts +1 -1
- package/src/utils/cache/index.ts +7 -0
- package/src/utils/cache/lruMap.test.ts +55 -0
- package/src/utils/cache/lruMap.ts +38 -0
- package/src/utils/cache/withCache.test.ts +168 -0
- package/src/utils/cache/withCache.ts +124 -0
- package/src/utils/inAppBrowser.ts +60 -0
- package/src/utils/index.ts +11 -5
- package/src/utils/mergeAttribution.test.ts +153 -0
- package/src/utils/mergeAttribution.ts +75 -0
- package/src/utils/sdkConfigStore.test.ts +405 -0
- package/src/utils/sdkConfigStore.ts +263 -0
- package/src/utils/sso.ts +3 -7
- package/dist/setupClient-BduY6Sym.cjs +0 -13
- package/dist/setupClient-ftmdQ-I8.js +0 -13
- package/dist/siweAuthenticate-BWmI2_TN.cjs +0 -1
- package/dist/siweAuthenticate-zczqxm0a.js +0 -1
- package/dist/trackEvent-CeLFVzZn.js +0 -1
- package/dist/trackEvent-Ew5r5zfI.cjs +0 -1
- package/src/utils/merchantId.test.ts +0 -653
- package/src/utils/merchantId.ts +0 -143
- package/src/utils/trackEvent.ts +0 -41
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getBackendUrl } from "../utils/backendUrl";
|
|
2
2
|
import { getClientId } from "../utils/clientId";
|
|
3
|
-
import {
|
|
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`, `
|
|
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 ??
|
|
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
|
|
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,39 @@ 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
|
+
|
|
83
|
+
// Handshake timing: measured from client creation until the iframe
|
|
84
|
+
// lifecycle manager resolves the `isConnected` promise.
|
|
85
|
+
const handshakeStartedAt = Date.now();
|
|
86
|
+
|
|
56
87
|
// Create our debug info gatherer
|
|
57
88
|
const debugInfo = new DebugInfoGatherer(config, iframe);
|
|
58
89
|
|
|
@@ -70,10 +101,9 @@ export function createIFrameFrakClient({
|
|
|
70
101
|
listeningTransport: window,
|
|
71
102
|
targetOrigin: frakWalletUrl,
|
|
72
103
|
middleware: [
|
|
73
|
-
// Ensure we are connected before sending request
|
|
104
|
+
// Ensure we are connected and context is sent before sending request
|
|
74
105
|
{
|
|
75
106
|
async onRequest(_message, ctx) {
|
|
76
|
-
// Ensure the iframe is connected
|
|
77
107
|
const isConnected = await lifecycleManager.isConnected;
|
|
78
108
|
if (!isConnected) {
|
|
79
109
|
throw new FrakRpcError(
|
|
@@ -81,6 +111,7 @@ export function createIFrameFrakClient({
|
|
|
81
111
|
"The iframe provider isn't connected yet"
|
|
82
112
|
);
|
|
83
113
|
}
|
|
114
|
+
await contextSent.promise;
|
|
84
115
|
return ctx;
|
|
85
116
|
},
|
|
86
117
|
},
|
|
@@ -98,9 +129,9 @@ export function createIFrameFrakClient({
|
|
|
98
129
|
],
|
|
99
130
|
// Add lifecycle handlers to process iframe lifecycle events
|
|
100
131
|
lifecycleHandlers: {
|
|
101
|
-
iframeLifecycle:
|
|
132
|
+
iframeLifecycle: (event, _context) => {
|
|
102
133
|
// Delegate to lifecycle manager (cast for type compatibility)
|
|
103
|
-
|
|
134
|
+
lifecycleManager.handleEvent(event);
|
|
104
135
|
},
|
|
105
136
|
},
|
|
106
137
|
});
|
|
@@ -108,14 +139,13 @@ export function createIFrameFrakClient({
|
|
|
108
139
|
// Setup heartbeat
|
|
109
140
|
const stopHeartbeat = setupHeartbeat(rpcClient, lifecycleManager);
|
|
110
141
|
|
|
111
|
-
// Build our destroy function
|
|
112
142
|
const destroy = async () => {
|
|
113
|
-
// Stop heartbeat
|
|
114
143
|
stopHeartbeat();
|
|
115
|
-
// Cleanup the RPC client
|
|
116
144
|
rpcClient.cleanup();
|
|
117
|
-
// Remove the iframe
|
|
118
145
|
iframe.remove();
|
|
146
|
+
clearAllCache();
|
|
147
|
+
sdkConfigStore.clearCache();
|
|
148
|
+
sdkConfigStore.reset();
|
|
119
149
|
};
|
|
120
150
|
|
|
121
151
|
// Init open panel
|
|
@@ -154,6 +184,38 @@ export function createIFrameFrakClient({
|
|
|
154
184
|
userAnonymousClientId: getClientId(),
|
|
155
185
|
});
|
|
156
186
|
openPanel.init();
|
|
187
|
+
openPanel.track("sdk_initialized", {
|
|
188
|
+
sdkVersion: process.env.SDK_VERSION,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Race the connection against the heartbeat timeout so we can
|
|
192
|
+
// distinguish "connected" from "timeout" cleanly without touching
|
|
193
|
+
// the heartbeat plumbing. 30s matches `HEARTBEAT_TIMEOUT`.
|
|
194
|
+
let settled = false;
|
|
195
|
+
const timeoutHandle = setTimeout(() => {
|
|
196
|
+
if (settled) return;
|
|
197
|
+
settled = true;
|
|
198
|
+
openPanel?.track("sdk_iframe_handshake_failed", {
|
|
199
|
+
reason: "timeout",
|
|
200
|
+
});
|
|
201
|
+
}, 30_000);
|
|
202
|
+
lifecycleManager.isConnected
|
|
203
|
+
.then(() => {
|
|
204
|
+
if (settled) return;
|
|
205
|
+
settled = true;
|
|
206
|
+
clearTimeout(timeoutHandle);
|
|
207
|
+
openPanel?.track("sdk_iframe_connected", {
|
|
208
|
+
handshake_duration_ms: Date.now() - handshakeStartedAt,
|
|
209
|
+
});
|
|
210
|
+
})
|
|
211
|
+
.catch(() => {
|
|
212
|
+
if (settled) return;
|
|
213
|
+
settled = true;
|
|
214
|
+
clearTimeout(timeoutHandle);
|
|
215
|
+
openPanel?.track("sdk_iframe_handshake_failed", {
|
|
216
|
+
reason: "unknown",
|
|
217
|
+
});
|
|
218
|
+
});
|
|
157
219
|
}
|
|
158
220
|
|
|
159
221
|
// Perform the post connection setup
|
|
@@ -161,7 +223,15 @@ export function createIFrameFrakClient({
|
|
|
161
223
|
config,
|
|
162
224
|
rpcClient,
|
|
163
225
|
lifecycleManager,
|
|
164
|
-
|
|
226
|
+
configPromise,
|
|
227
|
+
contextSent,
|
|
228
|
+
openPanel,
|
|
229
|
+
})
|
|
230
|
+
.then(() => debugInfo.updateSetupStatus(true))
|
|
231
|
+
.catch((err) => {
|
|
232
|
+
contextSent.reject(err);
|
|
233
|
+
throw err;
|
|
234
|
+
});
|
|
165
235
|
|
|
166
236
|
return {
|
|
167
237
|
config,
|
|
@@ -238,55 +308,190 @@ async function postConnectionSetup({
|
|
|
238
308
|
config,
|
|
239
309
|
rpcClient,
|
|
240
310
|
lifecycleManager,
|
|
311
|
+
configPromise,
|
|
312
|
+
contextSent,
|
|
313
|
+
openPanel,
|
|
241
314
|
}: {
|
|
242
315
|
config: FrakWalletSdkConfig;
|
|
243
316
|
rpcClient: SdkRpcClient;
|
|
244
317
|
lifecycleManager: IframeLifecycleManager;
|
|
318
|
+
configPromise: Promise<MerchantConfigResult> | undefined;
|
|
319
|
+
contextSent: Deferred<void>;
|
|
320
|
+
openPanel: OpenPanel | undefined;
|
|
245
321
|
}): Promise<void> {
|
|
246
|
-
// Wait for the handler to be connected
|
|
247
322
|
await lifecycleManager.isConnected;
|
|
248
323
|
|
|
249
|
-
// Setup SSO URL listener to detect and forward SSO redirects
|
|
250
|
-
// This checks for ?sso= parameter and forwards compressed data to iframe
|
|
251
324
|
setupSsoUrlListener(rpcClient, lifecycleManager.isConnected);
|
|
252
325
|
|
|
326
|
+
// Read and consume the pending merge token from URL (SSO identity merge)
|
|
327
|
+
const url = new URL(window.location.href);
|
|
328
|
+
const pendingMergeToken = url.searchParams.get("fmt") ?? undefined;
|
|
329
|
+
if (pendingMergeToken) {
|
|
330
|
+
url.searchParams.delete("fmt");
|
|
331
|
+
window.history.replaceState({}, "", url.toString());
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Merge a raw backend response with SDK metadata and persist to store
|
|
335
|
+
const mergeAndSetConfig = (merchantConfig: MerchantConfigResult) => {
|
|
336
|
+
const merchantId =
|
|
337
|
+
merchantConfig?.merchantId ?? config.metadata.merchantId ?? "";
|
|
338
|
+
const domain = merchantConfig?.domain ?? "";
|
|
339
|
+
const allowedDomains = merchantConfig?.allowedDomains ?? [];
|
|
340
|
+
const raw = merchantConfig?.sdkConfig;
|
|
341
|
+
|
|
342
|
+
// Per-field merge: backend wins over SDK static config.
|
|
343
|
+
const mergedAttribution =
|
|
344
|
+
raw?.attribution || config.attribution
|
|
345
|
+
? { ...config.attribution, ...raw?.attribution }
|
|
346
|
+
: undefined;
|
|
347
|
+
|
|
348
|
+
sdkConfigStore.setConfig(
|
|
349
|
+
raw
|
|
350
|
+
? {
|
|
351
|
+
isResolved: true,
|
|
352
|
+
merchantId,
|
|
353
|
+
domain,
|
|
354
|
+
allowedDomains,
|
|
355
|
+
hasRawSdkConfig: true,
|
|
356
|
+
name: raw.name ?? config.metadata.name,
|
|
357
|
+
logoUrl: raw.logoUrl ?? config.metadata.logoUrl,
|
|
358
|
+
homepageLink:
|
|
359
|
+
raw.homepageLink ?? config.metadata.homepageLink,
|
|
360
|
+
lang: raw.lang ?? config.metadata.lang,
|
|
361
|
+
currency: raw.currency ?? config.metadata.currency,
|
|
362
|
+
hidden: raw.hidden,
|
|
363
|
+
css: raw.css,
|
|
364
|
+
translations: raw.translations,
|
|
365
|
+
placements: raw.placements,
|
|
366
|
+
components: raw.components,
|
|
367
|
+
attribution: mergedAttribution,
|
|
368
|
+
}
|
|
369
|
+
: {
|
|
370
|
+
isResolved: true,
|
|
371
|
+
merchantId,
|
|
372
|
+
domain,
|
|
373
|
+
allowedDomains,
|
|
374
|
+
name: config.metadata.name,
|
|
375
|
+
logoUrl: config.metadata.logoUrl,
|
|
376
|
+
homepageLink: config.metadata.homepageLink,
|
|
377
|
+
lang: config.metadata.lang,
|
|
378
|
+
currency: config.metadata.currency,
|
|
379
|
+
attribution: mergedAttribution,
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// Send the resolved-config lifecycle event to the iframe.
|
|
385
|
+
// This is where we also update SDK-side OpenPanel global props with
|
|
386
|
+
// `merchantId` + `domain` (first time they are known) so every
|
|
387
|
+
// subsequent SDK event is merchant-attributed. We pass
|
|
388
|
+
// `sdkAnonymousId` through so the listener can join SDK funnels.
|
|
389
|
+
let mergeTokenConsumed = false;
|
|
390
|
+
const sendLifecycleConfig = (resolved: SdkResolvedConfig) => {
|
|
391
|
+
const token = mergeTokenConsumed ? undefined : pendingMergeToken;
|
|
392
|
+
mergeTokenConsumed = true;
|
|
393
|
+
|
|
394
|
+
const sdkConfig = resolved.hasRawSdkConfig
|
|
395
|
+
? {
|
|
396
|
+
name: resolved.name,
|
|
397
|
+
logoUrl: resolved.logoUrl,
|
|
398
|
+
homepageLink: resolved.homepageLink,
|
|
399
|
+
lang: resolved.lang,
|
|
400
|
+
currency: resolved.currency,
|
|
401
|
+
hidden: resolved.hidden,
|
|
402
|
+
css: resolved.css,
|
|
403
|
+
translations: resolved.translations,
|
|
404
|
+
placements: resolved.placements,
|
|
405
|
+
attribution: resolved.attribution,
|
|
406
|
+
}
|
|
407
|
+
: resolved.attribution
|
|
408
|
+
? { attribution: resolved.attribution }
|
|
409
|
+
: undefined;
|
|
410
|
+
|
|
411
|
+
const sdkAnonymousId = getClientId();
|
|
412
|
+
|
|
413
|
+
if (openPanel) {
|
|
414
|
+
const current = openPanel.global ?? {};
|
|
415
|
+
openPanel.setGlobalProperties({
|
|
416
|
+
...current,
|
|
417
|
+
merchantId: resolved.merchantId,
|
|
418
|
+
domain: resolved.domain ?? "",
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
rpcClient.sendLifecycle({
|
|
423
|
+
clientLifecycle: "resolved-config",
|
|
424
|
+
data: {
|
|
425
|
+
merchantId: resolved.merchantId,
|
|
426
|
+
domain: resolved.domain ?? "",
|
|
427
|
+
allowedDomains: resolved.allowedDomains ?? [],
|
|
428
|
+
sourceUrl: window.location.href,
|
|
429
|
+
...(sdkAnonymousId && { sdkAnonymousId }),
|
|
430
|
+
...(token && { pendingMergeToken: token }),
|
|
431
|
+
...(sdkConfig && { sdkConfig }),
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// SWR: if we have cached data, send it to the iframe immediately
|
|
437
|
+
if (sdkConfigStore.isResolved) {
|
|
438
|
+
sendLifecycleConfig(sdkConfigStore.getConfig());
|
|
439
|
+
contextSent.resolve();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// If a fetch is running (stale/missing cache), wait for fresh data and update
|
|
443
|
+
if (configPromise) {
|
|
444
|
+
const merchantConfig = await configPromise;
|
|
445
|
+
mergeAndSetConfig(merchantConfig);
|
|
446
|
+
sendLifecycleConfig(sdkConfigStore.getConfig());
|
|
447
|
+
contextSent.resolve();
|
|
448
|
+
}
|
|
449
|
+
|
|
253
450
|
// Push raw CSS if needed
|
|
254
451
|
async function pushCss() {
|
|
255
452
|
const cssLink = config.customizations?.css;
|
|
256
453
|
if (!cssLink) return;
|
|
257
|
-
|
|
258
|
-
const message = {
|
|
454
|
+
rpcClient.sendLifecycle({
|
|
259
455
|
clientLifecycle: "modal-css" as const,
|
|
260
456
|
data: { cssLink },
|
|
261
|
-
};
|
|
262
|
-
rpcClient.sendLifecycle(message);
|
|
457
|
+
});
|
|
263
458
|
}
|
|
264
459
|
|
|
265
460
|
// Push i18n if needed
|
|
266
461
|
async function pushI18n() {
|
|
267
462
|
const i18n = config.customizations?.i18n;
|
|
268
463
|
if (!i18n) return;
|
|
269
|
-
|
|
270
|
-
const message = {
|
|
464
|
+
rpcClient.sendLifecycle({
|
|
271
465
|
clientLifecycle: "modal-i18n" as const,
|
|
272
466
|
data: { i18n },
|
|
273
|
-
};
|
|
274
|
-
rpcClient.sendLifecycle(message);
|
|
467
|
+
});
|
|
275
468
|
}
|
|
276
469
|
|
|
277
470
|
// Push local backup if needed
|
|
278
471
|
async function pushBackup() {
|
|
279
472
|
if (typeof window === "undefined") return;
|
|
280
|
-
|
|
281
473
|
const backup = window.localStorage.getItem(BACKUP_KEY);
|
|
282
474
|
if (!backup) return;
|
|
283
|
-
|
|
284
|
-
const message = {
|
|
475
|
+
rpcClient.sendLifecycle({
|
|
285
476
|
clientLifecycle: "restore-backup" as const,
|
|
286
477
|
data: { backup },
|
|
287
|
-
};
|
|
288
|
-
rpcClient.sendLifecycle(message);
|
|
478
|
+
});
|
|
289
479
|
}
|
|
290
480
|
|
|
291
|
-
|
|
481
|
+
// Inspect each setup result — a failed CSS/i18n/backup push leaves the
|
|
482
|
+
// partner UI in a broken-but-connected state (iframe reports
|
|
483
|
+
// `sdk_iframe_connected`, user sees no modal styles / wrong locale).
|
|
484
|
+
// Surface it as a distinct handshake reason so dashboards can
|
|
485
|
+
// distinguish timeout vs. asset-push failures.
|
|
486
|
+
const results = await Promise.allSettled([
|
|
487
|
+
pushCss(),
|
|
488
|
+
pushI18n(),
|
|
489
|
+
pushBackup(),
|
|
490
|
+
]);
|
|
491
|
+
const hasFailedAssetPush = results.some((r) => r.status === "rejected");
|
|
492
|
+
if (hasFailedAssetPush) {
|
|
493
|
+
openPanel?.track("sdk_iframe_handshake_failed", {
|
|
494
|
+
reason: "asset_push",
|
|
495
|
+
});
|
|
496
|
+
}
|
|
292
497
|
}
|
|
@@ -102,7 +102,7 @@ describe("createIFrameLifecycleManager", () => {
|
|
|
102
102
|
iframeLifecycle: "connected" as const,
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
manager.handleEvent(event);
|
|
106
106
|
|
|
107
107
|
await expect(manager.isConnected).resolves.toBe(true);
|
|
108
108
|
});
|
|
@@ -126,7 +126,7 @@ describe("createIFrameLifecycleManager", () => {
|
|
|
126
126
|
data: { backup },
|
|
127
127
|
};
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
manager.handleEvent(event);
|
|
130
130
|
|
|
131
131
|
expect(localStorage.getItem("frak-backup-key")).toBe(backup);
|
|
132
132
|
});
|
|
@@ -150,7 +150,7 @@ describe("createIFrameLifecycleManager", () => {
|
|
|
150
150
|
data: {},
|
|
151
151
|
};
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
manager.handleEvent(event);
|
|
154
154
|
|
|
155
155
|
expect(localStorage.getItem("frak-backup-key")).toBeNull();
|
|
156
156
|
});
|
|
@@ -173,7 +173,7 @@ describe("createIFrameLifecycleManager", () => {
|
|
|
173
173
|
iframeLifecycle: "remove-backup" as const,
|
|
174
174
|
};
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
manager.handleEvent(event);
|
|
177
177
|
|
|
178
178
|
expect(localStorage.getItem("frak-backup-key")).toBeNull();
|
|
179
179
|
});
|
|
@@ -198,7 +198,7 @@ describe("createIFrameLifecycleManager", () => {
|
|
|
198
198
|
iframeLifecycle: "show" as const,
|
|
199
199
|
};
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
manager.handleEvent(event);
|
|
202
202
|
|
|
203
203
|
expect(changeIframeVisibility).toHaveBeenCalledWith({
|
|
204
204
|
iframe: mockIframe,
|
|
@@ -224,7 +224,7 @@ describe("createIFrameLifecycleManager", () => {
|
|
|
224
224
|
iframeLifecycle: "hide" as const,
|
|
225
225
|
};
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+
manager.handleEvent(event);
|
|
228
228
|
|
|
229
229
|
expect(changeIframeVisibility).toHaveBeenCalledWith({
|
|
230
230
|
iframe: mockIframe,
|
|
@@ -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(
|
|
@@ -339,7 +259,7 @@ describe("createIFrameLifecycleManager", () => {
|
|
|
339
259
|
},
|
|
340
260
|
};
|
|
341
261
|
|
|
342
|
-
|
|
262
|
+
manager.handleEvent(event);
|
|
343
263
|
|
|
344
264
|
expect(window.location.href).toBe(
|
|
345
265
|
"https://redirect.com/?u=https%3A%2F%2Foriginal.com"
|
|
@@ -371,7 +291,7 @@ describe("createIFrameLifecycleManager", () => {
|
|
|
371
291
|
},
|
|
372
292
|
};
|
|
373
293
|
|
|
374
|
-
|
|
294
|
+
manager.handleEvent(event);
|
|
375
295
|
|
|
376
296
|
expect(window.location.href).toBe("https://redirect.com/path");
|
|
377
297
|
});
|
|
@@ -405,7 +325,7 @@ describe("createIFrameLifecycleManager", () => {
|
|
|
405
325
|
},
|
|
406
326
|
};
|
|
407
327
|
|
|
408
|
-
|
|
328
|
+
manager.handleEvent(event);
|
|
409
329
|
|
|
410
330
|
expect(triggerDeepLinkWithFallback).toHaveBeenCalledWith(
|
|
411
331
|
"frakwallet://wallet",
|
|
@@ -450,7 +370,7 @@ describe("createIFrameLifecycleManager", () => {
|
|
|
450
370
|
},
|
|
451
371
|
};
|
|
452
372
|
|
|
453
|
-
|
|
373
|
+
manager.handleEvent(event);
|
|
454
374
|
|
|
455
375
|
// Extract the onFallback callback from the mock call
|
|
456
376
|
const callArgs = (triggerDeepLinkWithFallback as any).mock.calls[0];
|
|
@@ -499,7 +419,7 @@ describe("createIFrameLifecycleManager", () => {
|
|
|
499
419
|
},
|
|
500
420
|
};
|
|
501
421
|
|
|
502
|
-
|
|
422
|
+
manager.handleEvent(event);
|
|
503
423
|
|
|
504
424
|
// Should NOT call fallback detection
|
|
505
425
|
expect(triggerDeepLinkWithFallback).not.toHaveBeenCalled();
|
|
@@ -525,7 +445,7 @@ describe("createIFrameLifecycleManager", () => {
|
|
|
525
445
|
} as any;
|
|
526
446
|
|
|
527
447
|
// Should not throw
|
|
528
|
-
|
|
448
|
+
expect(manager.handleEvent(event)).toBeUndefined();
|
|
529
449
|
});
|
|
530
450
|
|
|
531
451
|
test("should only process events with iframeLifecycle", async () => {
|
|
@@ -543,13 +463,13 @@ describe("createIFrameLifecycleManager", () => {
|
|
|
543
463
|
});
|
|
544
464
|
|
|
545
465
|
// Event without iframeLifecycle
|
|
546
|
-
|
|
466
|
+
manager.handleEvent({ randomEvent: "show" } as any);
|
|
547
467
|
|
|
548
468
|
// changeIframeVisibility should not be called
|
|
549
469
|
expect(changeIframeVisibility).not.toHaveBeenCalled();
|
|
550
470
|
|
|
551
471
|
// Event with iframeLifecycle
|
|
552
|
-
|
|
472
|
+
manager.handleEvent({ iframeLifecycle: "show" as const });
|
|
553
473
|
|
|
554
474
|
// Now it should be called
|
|
555
475
|
expect(changeIframeVisibility).toHaveBeenCalled();
|