@frak-labs/core-sdk 0.2.1-beta.b38eef2e → 0.2.1-beta.c7fe645d
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 +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 → computeLegacyProductId-C35yITjX.d.ts} +91 -37
- package/dist/{computeLegacyProductId-b5cUWdAm.d.ts → computeLegacyProductId-mG4x5Cq0.d.cts} +91 -37
- 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-7e-OKMhg.d.ts} +266 -46
- package/dist/{openSso-CMzwvaCa.d.ts → openSso-BebjXCq0.d.cts} +266 -46
- package/dist/setupClient-CUYyoXtI.js +13 -0
- package/dist/setupClient-LgwHIFJc.cjs +13 -0
- package/dist/{siweAuthenticate-CnCZ7mok.d.ts → siweAuthenticate-BQEMZRg3.d.cts} +102 -8
- package/dist/siweAuthenticate-CWcVvP-G.cjs +1 -0
- package/dist/{siweAuthenticate-CVigMOxz.d.cts → siweAuthenticate-CdLD-9W2.d.ts} +102 -8
- package/dist/siweAuthenticate-DQfdb5UQ.js +1 -0
- package/dist/trackEvent-Ce1XlsIE.js +1 -0
- package/dist/trackEvent-CvbJTTqA.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/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/getUserReferralStatus.ts +42 -0
- package/src/actions/index.ts +7 -1
- 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 +150 -27
- package/src/clients/transports/iframeLifecycleManager.test.ts +14 -94
- package/src/clients/transports/iframeLifecycleManager.ts +15 -48
- package/src/index.ts +17 -4
- package/src/types/config.ts +10 -3
- package/src/types/index.ts +13 -1
- package/src/types/lifecycle/client.ts +22 -27
- package/src/types/lifecycle/iframe.ts +7 -8
- package/src/types/resolvedConfig.ts +123 -0
- package/src/types/rpc/displaySharingPage.ts +77 -0
- package/src/types/rpc/interaction.ts +4 -0
- package/src/types/rpc/userReferralStatus.ts +20 -0
- package/src/types/rpc.ts +42 -5
- 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 +162 -0
- package/src/utils/cache/withCache.ts +105 -0
- package/src/utils/inAppBrowser.ts +60 -0
- package/src/utils/index.ts +6 -4
- 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-CqTHGvVa.cjs +0 -13
- package/dist/setupClient-DTyvAPgh.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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { FrakClient, UserReferralStatusType } from "../types";
|
|
2
|
+
import { withCache } from "../utils/cache";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fetch the current user's referral status on the current merchant.
|
|
6
|
+
*
|
|
7
|
+
* The listener resolves the user's identity (via clientId or wallet session)
|
|
8
|
+
* and checks whether a referral link exists where the user is the referee.
|
|
9
|
+
*
|
|
10
|
+
* Results are cached in memory for 30 seconds by default. Concurrent calls
|
|
11
|
+
* while a request is in-flight are deduplicated automatically.
|
|
12
|
+
*
|
|
13
|
+
* Returns `null` when the user's identity cannot be resolved.
|
|
14
|
+
*
|
|
15
|
+
* @param client - The current Frak Client
|
|
16
|
+
* @param options - Optional cache configuration
|
|
17
|
+
* @param options.cacheTime - Time in ms to cache the result. Default: 30_000 (30s). Set to 0 to disable.
|
|
18
|
+
* @returns The user's referral status, or `null` if identity cannot be resolved
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const status = await getUserReferralStatus(client);
|
|
23
|
+
* if (status?.isReferred) {
|
|
24
|
+
* console.log("User was referred to this merchant");
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export async function getUserReferralStatus(
|
|
29
|
+
client: FrakClient,
|
|
30
|
+
options?: { cacheTime?: number }
|
|
31
|
+
): Promise<UserReferralStatusType | null> {
|
|
32
|
+
return withCache(
|
|
33
|
+
() =>
|
|
34
|
+
client.request({
|
|
35
|
+
method: "frak_getUserReferralStatus",
|
|
36
|
+
}),
|
|
37
|
+
{
|
|
38
|
+
cacheKey: "frak_getUserReferralStatus",
|
|
39
|
+
cacheTime: options?.cacheTime,
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
}
|
package/src/actions/index.ts
CHANGED
|
@@ -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
|
|
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/
|
|
15
|
-
|
|
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 {
|
|
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(
|
|
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:
|
|
235
|
+
merchantId: null,
|
|
232
236
|
clientId: "test-client-id",
|
|
233
237
|
});
|
|
234
|
-
vi.mocked(
|
|
235
|
-
|
|
236
|
-
|
|
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(
|
|
257
|
-
|
|
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:
|
|
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(
|
|
288
|
-
|
|
289
|
-
);
|
|
295
|
+
expect(
|
|
296
|
+
vi.mocked(sdkConfigStore.resolveMerchantId)
|
|
297
|
+
).toHaveBeenCalled();
|
|
290
298
|
});
|
|
291
299
|
|
|
292
|
-
test("should fall back to
|
|
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(
|
|
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(
|
|
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 {
|
|
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,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
|
},
|
|
@@ -98,9 +125,9 @@ export function createIFrameFrakClient({
|
|
|
98
125
|
],
|
|
99
126
|
// Add lifecycle handlers to process iframe lifecycle events
|
|
100
127
|
lifecycleHandlers: {
|
|
101
|
-
iframeLifecycle:
|
|
128
|
+
iframeLifecycle: (event, _context) => {
|
|
102
129
|
// Delegate to lifecycle manager (cast for type compatibility)
|
|
103
|
-
|
|
130
|
+
lifecycleManager.handleEvent(event);
|
|
104
131
|
},
|
|
105
132
|
},
|
|
106
133
|
});
|
|
@@ -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
|
-
|
|
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()]);
|