@frak-labs/core-sdk 0.2.1 → 1.0.0
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-D4aBXbdp.cjs +1 -0
- package/dist/actions-Dq_uN-wn.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-BV5D9DsW.d.ts} +91 -37
- package/dist/{siweAuthenticate-CnCZ7mok.d.ts → index-BphwTmKA.d.cts} +122 -8
- package/dist/{computeLegacyProductId-b5cUWdAm.d.ts → index-Dwmo109y.d.cts} +91 -37
- package/dist/{siweAuthenticate-CVigMOxz.d.cts → index-_f8EuN_1.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-BwEK2M98.d.cts} +283 -44
- package/dist/{openSso-CMzwvaCa.d.ts → openSso-C1Wzl5-i.d.ts} +283 -44
- package/dist/src-B1eliIi6.cjs +13 -0
- package/dist/src-C0UH1GsN.js +13 -0
- package/dist/trackEvent-BqJqRZ-u.cjs +1 -0
- package/dist/trackEvent-Bqq4jd6R.js +1 -0
- package/package.json +11 -12
- 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/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 +151 -27
- package/src/clients/transports/iframeLifecycleManager.test.ts +14 -94
- package/src/clients/transports/iframeLifecycleManager.ts +35 -53
- package/src/index.ts +17 -4
- package/src/stubs/rrweb.ts +9 -0
- 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 +128 -0
- package/src/types/rpc/displaySharingPage.ts +82 -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/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 +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-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
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { sdkConfigStore } from "./sdkConfigStore";
|
|
3
|
+
|
|
4
|
+
vi.mock("./backendUrl", () => ({
|
|
5
|
+
getBackendUrl: vi.fn((walletUrl?: string) => {
|
|
6
|
+
if (walletUrl?.includes("localhost")) {
|
|
7
|
+
return "http://localhost:3030";
|
|
8
|
+
}
|
|
9
|
+
return "https://backend.frak.id";
|
|
10
|
+
}),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe("sdkConfigStore", () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
sdkConfigStore.clearCache();
|
|
16
|
+
window.sessionStorage.clear();
|
|
17
|
+
window.localStorage.clear();
|
|
18
|
+
window.__frakSdkConfig = undefined;
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.restoreAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("sdkConfigStore.resolve", () => {
|
|
28
|
+
it("should fetch from backend when not cached", async () => {
|
|
29
|
+
const mockResponse = {
|
|
30
|
+
merchantId: "merchant-123",
|
|
31
|
+
name: "Test",
|
|
32
|
+
domain: "shop.example.com",
|
|
33
|
+
allowedDomains: [],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
37
|
+
ok: true,
|
|
38
|
+
json: async () => mockResponse,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const result = await sdkConfigStore.resolve("shop.example.com");
|
|
42
|
+
|
|
43
|
+
expect(result).toEqual(mockResponse);
|
|
44
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
45
|
+
"https://backend.frak.id/user/merchant/resolve?domain=shop.example.com"
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should return cached response on subsequent calls", async () => {
|
|
50
|
+
const mockResponse = {
|
|
51
|
+
merchantId: "merchant-456",
|
|
52
|
+
name: "Test",
|
|
53
|
+
domain: "shop.example.com",
|
|
54
|
+
allowedDomains: [],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
58
|
+
ok: true,
|
|
59
|
+
json: async () => mockResponse,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const result1 = await sdkConfigStore.resolve("shop.example.com");
|
|
63
|
+
expect(result1).toEqual(mockResponse);
|
|
64
|
+
|
|
65
|
+
const result2 = await sdkConfigStore.resolve("shop.example.com");
|
|
66
|
+
expect(result2).toEqual(mockResponse);
|
|
67
|
+
|
|
68
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should deduplicate concurrent requests", async () => {
|
|
72
|
+
const mockResponse = {
|
|
73
|
+
merchantId: "merchant-789",
|
|
74
|
+
name: "Test",
|
|
75
|
+
domain: "shop.example.com",
|
|
76
|
+
allowedDomains: [],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
80
|
+
ok: true,
|
|
81
|
+
json: async () => mockResponse,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const [result1, result2, result3] = await Promise.all([
|
|
85
|
+
sdkConfigStore.resolve("shop.example.com"),
|
|
86
|
+
sdkConfigStore.resolve("shop.example.com"),
|
|
87
|
+
sdkConfigStore.resolve("shop.example.com"),
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
expect(result1).toEqual(mockResponse);
|
|
91
|
+
expect(result2).toEqual(mockResponse);
|
|
92
|
+
expect(result3).toEqual(mockResponse);
|
|
93
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should use window.location.hostname when domain not provided", async () => {
|
|
97
|
+
const mockResponse = {
|
|
98
|
+
merchantId: "merchant-default",
|
|
99
|
+
name: "Test",
|
|
100
|
+
domain: "example.com",
|
|
101
|
+
allowedDomains: [],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
105
|
+
ok: true,
|
|
106
|
+
json: async () => mockResponse,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
Object.defineProperty(window, "location", {
|
|
110
|
+
value: { hostname: "example.com" },
|
|
111
|
+
writable: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const result = await sdkConfigStore.resolve();
|
|
115
|
+
|
|
116
|
+
expect(result).toEqual(mockResponse);
|
|
117
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
118
|
+
"https://backend.frak.id/user/merchant/resolve?domain=example.com"
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should return undefined when domain is empty", async () => {
|
|
123
|
+
global.fetch = vi.fn();
|
|
124
|
+
|
|
125
|
+
const result = await sdkConfigStore.resolve("");
|
|
126
|
+
|
|
127
|
+
expect(result).toBeUndefined();
|
|
128
|
+
expect(global.fetch).not.toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should handle fetch errors gracefully", async () => {
|
|
132
|
+
global.fetch = vi
|
|
133
|
+
.fn()
|
|
134
|
+
.mockRejectedValueOnce(new Error("Network error"));
|
|
135
|
+
|
|
136
|
+
const result = await sdkConfigStore.resolve("shop.example.com");
|
|
137
|
+
|
|
138
|
+
expect(result).toBeUndefined();
|
|
139
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
140
|
+
"[Frak SDK] Failed to fetch merchant config:",
|
|
141
|
+
expect.any(Error)
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should handle non-ok response (404, 500)", async () => {
|
|
146
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
147
|
+
ok: false,
|
|
148
|
+
status: 404,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const result = await sdkConfigStore.resolve("nonexistent.com");
|
|
152
|
+
|
|
153
|
+
expect(result).toBeUndefined();
|
|
154
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
155
|
+
"[Frak SDK] Merchant lookup failed for domain nonexistent.com: 404"
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should use custom walletUrl to derive backend URL", async () => {
|
|
160
|
+
const mockResponse = {
|
|
161
|
+
merchantId: "merchant-local",
|
|
162
|
+
name: "Test",
|
|
163
|
+
domain: "shop.example.com",
|
|
164
|
+
allowedDomains: [],
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
168
|
+
ok: true,
|
|
169
|
+
json: async () => mockResponse,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const result = await sdkConfigStore.resolve(
|
|
173
|
+
"shop.example.com",
|
|
174
|
+
"http://localhost:3000"
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(result).toEqual(mockResponse);
|
|
178
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
179
|
+
"http://localhost:3030/user/merchant/resolve?domain=shop.example.com"
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should encode domain in URL query parameter", async () => {
|
|
184
|
+
const mockResponse = {
|
|
185
|
+
merchantId: "merchant-encoded",
|
|
186
|
+
name: "Test",
|
|
187
|
+
domain: "shop.example.com",
|
|
188
|
+
allowedDomains: [],
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
192
|
+
ok: true,
|
|
193
|
+
json: async () => mockResponse,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const domainWithSpecialChars = "shop.example.com?test=1&foo=bar";
|
|
197
|
+
await sdkConfigStore.resolve(domainWithSpecialChars);
|
|
198
|
+
|
|
199
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
200
|
+
"https://backend.frak.id/user/merchant/resolve?domain=shop.example.com%3Ftest%3D1%26foo%3Dbar"
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should write merchantId to sessionStorage as 'frak-merchant-id'", async () => {
|
|
205
|
+
const mockResponse = {
|
|
206
|
+
merchantId: "merchant-persisted",
|
|
207
|
+
name: "Test",
|
|
208
|
+
domain: "shop.example.com",
|
|
209
|
+
allowedDomains: [],
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
213
|
+
ok: true,
|
|
214
|
+
json: async () => mockResponse,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await sdkConfigStore.resolve("shop.example.com");
|
|
218
|
+
|
|
219
|
+
expect(window.sessionStorage.getItem("frak-merchant-id")).toBe(
|
|
220
|
+
"merchant-persisted"
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should pass lang parameter to backend when provided", async () => {
|
|
225
|
+
const mockResponse = {
|
|
226
|
+
merchantId: "merchant-lang",
|
|
227
|
+
name: "Test",
|
|
228
|
+
domain: "shop.example.com",
|
|
229
|
+
allowedDomains: [],
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
233
|
+
ok: true,
|
|
234
|
+
json: async () => mockResponse,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await sdkConfigStore.resolve("shop.example.com", undefined, "fr");
|
|
238
|
+
|
|
239
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
240
|
+
"https://backend.frak.id/user/merchant/resolve?domain=shop.example.com&lang=fr"
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("sdkConfigStore.getMerchantId", () => {
|
|
246
|
+
it("should return merchantId from resolved config", () => {
|
|
247
|
+
window.__frakSdkConfig = {
|
|
248
|
+
isResolved: true,
|
|
249
|
+
merchantId: "config-merchant-123",
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const result = sdkConfigStore.getMerchantId();
|
|
253
|
+
|
|
254
|
+
expect(result).toBe("config-merchant-123");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should fall back to sessionStorage", () => {
|
|
258
|
+
window.__frakSdkConfig = {
|
|
259
|
+
isResolved: false,
|
|
260
|
+
merchantId: "",
|
|
261
|
+
};
|
|
262
|
+
window.sessionStorage.setItem(
|
|
263
|
+
"frak-merchant-id",
|
|
264
|
+
"session-merchant-456"
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const result = sdkConfigStore.getMerchantId();
|
|
268
|
+
|
|
269
|
+
expect(result).toBe("session-merchant-456");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("should return undefined when nothing cached", () => {
|
|
273
|
+
window.__frakSdkConfig = {
|
|
274
|
+
isResolved: false,
|
|
275
|
+
merchantId: "",
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const result = sdkConfigStore.getMerchantId();
|
|
279
|
+
|
|
280
|
+
expect(result).toBeUndefined();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("sdkConfigStore.resolveMerchantId", () => {
|
|
285
|
+
it("should return merchantId from store without fetch", async () => {
|
|
286
|
+
window.__frakSdkConfig = {
|
|
287
|
+
isResolved: true,
|
|
288
|
+
merchantId: "store-merchant-123",
|
|
289
|
+
};
|
|
290
|
+
global.fetch = vi.fn();
|
|
291
|
+
|
|
292
|
+
const result = await sdkConfigStore.resolveMerchantId();
|
|
293
|
+
|
|
294
|
+
expect(result).toBe("store-merchant-123");
|
|
295
|
+
expect(global.fetch).not.toHaveBeenCalled();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("should return merchantId from sessionStorage without fetch", async () => {
|
|
299
|
+
window.__frakSdkConfig = {
|
|
300
|
+
isResolved: false,
|
|
301
|
+
merchantId: "",
|
|
302
|
+
};
|
|
303
|
+
window.sessionStorage.setItem(
|
|
304
|
+
"frak-merchant-id",
|
|
305
|
+
"session-merchant-789"
|
|
306
|
+
);
|
|
307
|
+
global.fetch = vi.fn();
|
|
308
|
+
|
|
309
|
+
const result = await sdkConfigStore.resolveMerchantId();
|
|
310
|
+
|
|
311
|
+
expect(result).toBe("session-merchant-789");
|
|
312
|
+
expect(global.fetch).not.toHaveBeenCalled();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("should fetch from backend as last resort", async () => {
|
|
316
|
+
const mockResponse = {
|
|
317
|
+
merchantId: "fetched-merchant-456",
|
|
318
|
+
name: "Test",
|
|
319
|
+
domain: "shop.example.com",
|
|
320
|
+
allowedDomains: [],
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
324
|
+
ok: true,
|
|
325
|
+
json: async () => mockResponse,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
Object.defineProperty(window, "location", {
|
|
329
|
+
value: { hostname: "shop.example.com" },
|
|
330
|
+
writable: true,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const result = await sdkConfigStore.resolveMerchantId();
|
|
334
|
+
|
|
335
|
+
expect(result).toBe("fetched-merchant-456");
|
|
336
|
+
expect(global.fetch).toHaveBeenCalled();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("should return undefined when fetch fails", async () => {
|
|
340
|
+
global.fetch = vi
|
|
341
|
+
.fn()
|
|
342
|
+
.mockRejectedValueOnce(new Error("Network error"));
|
|
343
|
+
|
|
344
|
+
Object.defineProperty(window, "location", {
|
|
345
|
+
value: { hostname: "shop.example.com" },
|
|
346
|
+
writable: true,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const result = await sdkConfigStore.resolveMerchantId();
|
|
350
|
+
|
|
351
|
+
expect(result).toBeUndefined();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("sdkConfigStore.clearCache", () => {
|
|
356
|
+
it("should clear all caches and allow re-fetching", async () => {
|
|
357
|
+
const mockResponse = {
|
|
358
|
+
merchantId: "merchant-clear-test",
|
|
359
|
+
name: "Test",
|
|
360
|
+
domain: "shop.example.com",
|
|
361
|
+
allowedDomains: [],
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
365
|
+
ok: true,
|
|
366
|
+
json: async () => mockResponse,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const result1 = await sdkConfigStore.resolve("shop.example.com");
|
|
370
|
+
expect(result1).toEqual(mockResponse);
|
|
371
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
372
|
+
|
|
373
|
+
sdkConfigStore.clearCache();
|
|
374
|
+
|
|
375
|
+
const result2 = await sdkConfigStore.resolve("shop.example.com");
|
|
376
|
+
expect(result2).toEqual(mockResponse);
|
|
377
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("should clear sessionStorage frak-merchant-id", async () => {
|
|
381
|
+
const mockResponse = {
|
|
382
|
+
merchantId: "merchant-session-clear",
|
|
383
|
+
name: "Test",
|
|
384
|
+
domain: "shop.example.com",
|
|
385
|
+
allowedDomains: [],
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
389
|
+
ok: true,
|
|
390
|
+
json: async () => mockResponse,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
await sdkConfigStore.resolve("shop.example.com");
|
|
394
|
+
expect(window.sessionStorage.getItem("frak-merchant-id")).toBe(
|
|
395
|
+
"merchant-session-clear"
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
sdkConfigStore.clearCache();
|
|
399
|
+
|
|
400
|
+
expect(
|
|
401
|
+
window.sessionStorage.getItem("frak-merchant-id")
|
|
402
|
+
).toBeNull();
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
});
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK config store — reactive singleton for the resolved merchant config.
|
|
3
|
+
*
|
|
4
|
+
* State lives directly on `window.__frakSdkConfig`.
|
|
5
|
+
* Reactivity is handled via the `frak:config` CustomEvent on `window`.
|
|
6
|
+
* Resolved configs are cached in localStorage (30 s TTL, stale-while-revalidate).
|
|
7
|
+
*
|
|
8
|
+
* Backend fetch responses are cached and deduplicated via `withCache`.
|
|
9
|
+
* Also owns the `frak-merchant-id` sessionStorage compatibility key.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Language } from "../types/config";
|
|
13
|
+
import type {
|
|
14
|
+
MerchantConfigResponse,
|
|
15
|
+
SdkResolvedConfig,
|
|
16
|
+
} from "../types/resolvedConfig";
|
|
17
|
+
import { getBackendUrl } from "./backendUrl";
|
|
18
|
+
import { clearAllCache, withCache } from "./cache";
|
|
19
|
+
|
|
20
|
+
const GLOBAL_KEY = "__frakSdkConfig";
|
|
21
|
+
const CACHE_TTL = 30_000; // 30 seconds
|
|
22
|
+
const DEFAULT_CACHE_KEY = "frak-config-cache";
|
|
23
|
+
const MERCHANT_ID_KEY = "frak-merchant-id";
|
|
24
|
+
|
|
25
|
+
const cacheState = { key: DEFAULT_CACHE_KEY };
|
|
26
|
+
|
|
27
|
+
const isBrowser = typeof window !== "undefined";
|
|
28
|
+
|
|
29
|
+
type CacheEntry = { config: SdkResolvedConfig; timestamp: number };
|
|
30
|
+
|
|
31
|
+
declare global {
|
|
32
|
+
interface Window {
|
|
33
|
+
[GLOBAL_KEY]?: SdkResolvedConfig;
|
|
34
|
+
}
|
|
35
|
+
interface WindowEventMap {
|
|
36
|
+
"frak:config": CustomEvent<SdkResolvedConfig>;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function freshEmptyConfig(): SdkResolvedConfig {
|
|
41
|
+
return { isResolved: false, merchantId: "" };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// localStorage cache (with in-memory parsed copy)
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
let memoryEntry: CacheEntry | null = null;
|
|
49
|
+
|
|
50
|
+
function loadCacheEntry(): CacheEntry | null {
|
|
51
|
+
if (!isBrowser) return null;
|
|
52
|
+
try {
|
|
53
|
+
const raw = localStorage.getItem(cacheState.key);
|
|
54
|
+
if (!raw) return null;
|
|
55
|
+
const entry: CacheEntry = JSON.parse(raw);
|
|
56
|
+
if (!entry.config?.isResolved) return null;
|
|
57
|
+
memoryEntry = entry;
|
|
58
|
+
return entry;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readCache(): SdkResolvedConfig | undefined {
|
|
65
|
+
return (memoryEntry ?? loadCacheEntry())?.config;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isCacheFresh(): boolean {
|
|
69
|
+
const entry = memoryEntry ?? loadCacheEntry();
|
|
70
|
+
if (!entry) return false;
|
|
71
|
+
return Date.now() - entry.timestamp < CACHE_TTL;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function writeCache(config: SdkResolvedConfig): void {
|
|
75
|
+
if (!isBrowser || !config.isResolved) return;
|
|
76
|
+
try {
|
|
77
|
+
const entry: CacheEntry = { config, timestamp: Date.now() };
|
|
78
|
+
localStorage.setItem(cacheState.key, JSON.stringify(entry));
|
|
79
|
+
memoryEntry = entry;
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function removeCache(): void {
|
|
84
|
+
if (!isBrowser) return;
|
|
85
|
+
memoryEntry = null;
|
|
86
|
+
try {
|
|
87
|
+
localStorage.removeItem(cacheState.key);
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Initialise window-backed config (once per bundle boundary)
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
function initConfig(): void {
|
|
96
|
+
if (!isBrowser) return;
|
|
97
|
+
if (window[GLOBAL_KEY]) return;
|
|
98
|
+
window[GLOBAL_KEY] = readCache() ?? freshEmptyConfig();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
initConfig();
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Helpers
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
function getConfig(): SdkResolvedConfig {
|
|
108
|
+
if (!isBrowser) return freshEmptyConfig();
|
|
109
|
+
return window[GLOBAL_KEY] ?? freshEmptyConfig();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function dispatch(config: SdkResolvedConfig): void {
|
|
113
|
+
if (!isBrowser) return;
|
|
114
|
+
window.dispatchEvent(new CustomEvent("frak:config", { detail: config }));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getTargetDomain(domain?: string): string {
|
|
118
|
+
return domain ?? (isBrowser ? window.location.hostname : "");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Merchant config fetching (resolve)
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
async function fetchFromBackend(
|
|
126
|
+
targetDomain: string,
|
|
127
|
+
walletUrl?: string,
|
|
128
|
+
lang?: Language
|
|
129
|
+
): Promise<MerchantConfigResponse | undefined> {
|
|
130
|
+
try {
|
|
131
|
+
const backendUrl = getBackendUrl(walletUrl);
|
|
132
|
+
const langParam = lang ? `&lang=${encodeURIComponent(lang)}` : "";
|
|
133
|
+
const response = await fetch(
|
|
134
|
+
`${backendUrl}/user/merchant/resolve?domain=${encodeURIComponent(targetDomain)}${langParam}`
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
console.warn(
|
|
139
|
+
`[Frak SDK] Merchant lookup failed for domain ${targetDomain}: ${response.status}`
|
|
140
|
+
);
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const data = (await response.json()) as MerchantConfigResponse;
|
|
145
|
+
|
|
146
|
+
// Write compatibility sessionStorage key
|
|
147
|
+
if (isBrowser) {
|
|
148
|
+
try {
|
|
149
|
+
sessionStorage.setItem(MERCHANT_ID_KEY, data.merchantId);
|
|
150
|
+
} catch {}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return data;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.warn("[Frak SDK] Failed to fetch merchant config:", error);
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Public API
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
export const sdkConfigStore = {
|
|
165
|
+
getConfig,
|
|
166
|
+
|
|
167
|
+
get isResolved(): boolean {
|
|
168
|
+
return getConfig().isResolved;
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
get isCacheFresh(): boolean {
|
|
172
|
+
return isCacheFresh();
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
setCacheScope(domain: string, lang?: string): void {
|
|
176
|
+
const suffix = `${domain}:${lang ?? ""}`;
|
|
177
|
+
cacheState.key = `${DEFAULT_CACHE_KEY}:${suffix}`;
|
|
178
|
+
memoryEntry = null;
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
setConfig(config: SdkResolvedConfig): void {
|
|
182
|
+
if (isBrowser) window[GLOBAL_KEY] = config;
|
|
183
|
+
writeCache(config);
|
|
184
|
+
dispatch(config);
|
|
185
|
+
|
|
186
|
+
// Keep sessionStorage merchantId in sync
|
|
187
|
+
if (isBrowser && config.merchantId) {
|
|
188
|
+
try {
|
|
189
|
+
sessionStorage.setItem(MERCHANT_ID_KEY, config.merchantId);
|
|
190
|
+
} catch {}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
reset(): void {
|
|
195
|
+
const next = readCache() ?? freshEmptyConfig();
|
|
196
|
+
if (isBrowser) window[GLOBAL_KEY] = next;
|
|
197
|
+
dispatch(next);
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
clearCache(): void {
|
|
201
|
+
removeCache();
|
|
202
|
+
clearAllCache();
|
|
203
|
+
if (isBrowser) {
|
|
204
|
+
try {
|
|
205
|
+
sessionStorage.removeItem(MERCHANT_ID_KEY);
|
|
206
|
+
} catch {}
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
resolve(
|
|
211
|
+
domain?: string,
|
|
212
|
+
walletUrl?: string,
|
|
213
|
+
lang?: Language
|
|
214
|
+
): Promise<MerchantConfigResponse | undefined> {
|
|
215
|
+
const targetDomain = getTargetDomain(domain);
|
|
216
|
+
if (!targetDomain) {
|
|
217
|
+
return Promise.resolve(undefined);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const cacheKey = `sdkConfig:${targetDomain}:${lang ?? ""}`;
|
|
221
|
+
|
|
222
|
+
return withCache(
|
|
223
|
+
async () => {
|
|
224
|
+
const result = await fetchFromBackend(
|
|
225
|
+
targetDomain,
|
|
226
|
+
walletUrl,
|
|
227
|
+
lang
|
|
228
|
+
);
|
|
229
|
+
// Throw on failure so withCache doesn't cache undefined
|
|
230
|
+
if (!result) {
|
|
231
|
+
throw new Error("Config resolution returned empty");
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
},
|
|
235
|
+
{ cacheKey, cacheTime: Number.POSITIVE_INFINITY }
|
|
236
|
+
).catch(() => undefined);
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
getMerchantId(): string | undefined {
|
|
240
|
+
const config = getConfig();
|
|
241
|
+
if (config.isResolved && config.merchantId) {
|
|
242
|
+
return config.merchantId;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (isBrowser) {
|
|
246
|
+
try {
|
|
247
|
+
return sessionStorage.getItem(MERCHANT_ID_KEY) ?? undefined;
|
|
248
|
+
} catch {}
|
|
249
|
+
}
|
|
250
|
+
return undefined;
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
async resolveMerchantId(
|
|
254
|
+
domain?: string,
|
|
255
|
+
walletUrl?: string
|
|
256
|
+
): Promise<string | undefined> {
|
|
257
|
+
const fast = sdkConfigStore.getMerchantId();
|
|
258
|
+
if (fast) return fast;
|
|
259
|
+
|
|
260
|
+
const config = await sdkConfigStore.resolve(domain, walletUrl);
|
|
261
|
+
return config?.merchantId;
|
|
262
|
+
},
|
|
263
|
+
};
|
package/src/utils/sso.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { PrepareSsoParamsType, SsoMetadata } from "../types";
|
|
|
3
3
|
import { compressJsonToB64 } from "./compression/compress";
|
|
4
4
|
|
|
5
5
|
export type AppSpecificSsoMetadata = SsoMetadata & {
|
|
6
|
-
name
|
|
6
|
+
name?: string;
|
|
7
7
|
css?: string;
|
|
8
8
|
};
|
|
9
9
|
|
|
@@ -43,7 +43,7 @@ export function generateSsoUrl(
|
|
|
43
43
|
walletUrl: string,
|
|
44
44
|
params: PrepareSsoParamsType,
|
|
45
45
|
merchantId: string,
|
|
46
|
-
name: string,
|
|
46
|
+
name: string | undefined,
|
|
47
47
|
clientId: string,
|
|
48
48
|
css?: string
|
|
49
49
|
): string {
|
|
@@ -114,13 +114,9 @@ export type CompressedSsoData = {
|
|
|
114
114
|
m: string;
|
|
115
115
|
// metadata
|
|
116
116
|
md: {
|
|
117
|
-
|
|
118
|
-
n: string;
|
|
119
|
-
// custom css
|
|
117
|
+
n?: string;
|
|
120
118
|
css?: string;
|
|
121
|
-
// logo
|
|
122
119
|
l?: string;
|
|
123
|
-
// home page link
|
|
124
120
|
h?: string;
|
|
125
121
|
};
|
|
126
122
|
};
|