@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.
Files changed (77) hide show
  1. package/README.md +1 -2
  2. package/cdn/bundle.js +3 -3
  3. package/dist/actions-D4aBXbdp.cjs +1 -0
  4. package/dist/actions-Dq_uN-wn.js +1 -0
  5. package/dist/actions.cjs +1 -1
  6. package/dist/actions.d.cts +3 -3
  7. package/dist/actions.d.ts +3 -3
  8. package/dist/actions.js +1 -1
  9. package/dist/bundle.cjs +1 -1
  10. package/dist/bundle.d.cts +4 -4
  11. package/dist/bundle.d.ts +4 -4
  12. package/dist/bundle.js +1 -1
  13. package/dist/{computeLegacyProductId-CCAZvLa5.d.cts → index-BV5D9DsW.d.ts} +91 -37
  14. package/dist/{siweAuthenticate-CnCZ7mok.d.ts → index-BphwTmKA.d.cts} +122 -8
  15. package/dist/{computeLegacyProductId-b5cUWdAm.d.ts → index-Dwmo109y.d.cts} +91 -37
  16. package/dist/{siweAuthenticate-CVigMOxz.d.cts → index-_f8EuN_1.d.ts} +122 -8
  17. package/dist/index.cjs +1 -1
  18. package/dist/index.d.cts +3 -3
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js +1 -1
  21. package/dist/{openSso-B0g7-807.d.cts → openSso-BwEK2M98.d.cts} +283 -44
  22. package/dist/{openSso-CMzwvaCa.d.ts → openSso-C1Wzl5-i.d.ts} +283 -44
  23. package/dist/src-B1eliIi6.cjs +13 -0
  24. package/dist/src-C0UH1GsN.js +13 -0
  25. package/dist/trackEvent-BqJqRZ-u.cjs +1 -0
  26. package/dist/trackEvent-Bqq4jd6R.js +1 -0
  27. package/package.json +11 -12
  28. package/src/actions/displayEmbeddedWallet.ts +6 -2
  29. package/src/actions/displayModal.ts +6 -2
  30. package/src/actions/displaySharingPage.ts +49 -0
  31. package/src/actions/ensureIdentity.ts +2 -2
  32. package/src/actions/getMerchantInformation.test.ts +13 -1
  33. package/src/actions/getMerchantInformation.ts +20 -5
  34. package/src/actions/getMergeToken.ts +33 -0
  35. package/src/actions/getUserReferralStatus.ts +42 -0
  36. package/src/actions/index.ts +8 -1
  37. package/src/actions/referral/setupReferral.test.ts +79 -0
  38. package/src/actions/referral/setupReferral.ts +32 -0
  39. package/src/actions/trackPurchaseStatus.test.ts +32 -20
  40. package/src/actions/trackPurchaseStatus.ts +3 -5
  41. package/src/actions/wrapper/modalBuilder.test.ts +4 -2
  42. package/src/actions/wrapper/modalBuilder.ts +6 -8
  43. package/src/clients/createIFrameFrakClient.ts +151 -27
  44. package/src/clients/transports/iframeLifecycleManager.test.ts +14 -94
  45. package/src/clients/transports/iframeLifecycleManager.ts +35 -53
  46. package/src/index.ts +17 -4
  47. package/src/stubs/rrweb.ts +9 -0
  48. package/src/types/config.ts +10 -3
  49. package/src/types/index.ts +13 -1
  50. package/src/types/lifecycle/client.ts +22 -27
  51. package/src/types/lifecycle/iframe.ts +7 -8
  52. package/src/types/resolvedConfig.ts +128 -0
  53. package/src/types/rpc/displaySharingPage.ts +82 -0
  54. package/src/types/rpc/embedded/index.ts +1 -1
  55. package/src/types/rpc/interaction.ts +4 -0
  56. package/src/types/rpc/userReferralStatus.ts +20 -0
  57. package/src/types/rpc.ts +54 -5
  58. package/src/utils/backendUrl.test.ts +2 -2
  59. package/src/utils/backendUrl.ts +1 -1
  60. package/src/utils/cache/index.ts +7 -0
  61. package/src/utils/cache/lruMap.test.ts +55 -0
  62. package/src/utils/cache/lruMap.ts +38 -0
  63. package/src/utils/cache/withCache.test.ts +168 -0
  64. package/src/utils/cache/withCache.ts +124 -0
  65. package/src/utils/inAppBrowser.ts +60 -0
  66. package/src/utils/index.ts +6 -4
  67. package/src/utils/sdkConfigStore.test.ts +405 -0
  68. package/src/utils/sdkConfigStore.ts +263 -0
  69. package/src/utils/sso.ts +3 -7
  70. package/dist/setupClient-BduY6Sym.cjs +0 -13
  71. package/dist/setupClient-ftmdQ-I8.js +0 -13
  72. package/dist/siweAuthenticate-BWmI2_TN.cjs +0 -1
  73. package/dist/siweAuthenticate-zczqxm0a.js +0 -1
  74. package/dist/trackEvent-CeLFVzZn.js +0 -1
  75. package/dist/trackEvent-Ew5r5zfI.cjs +0 -1
  76. package/src/utils/merchantId.test.ts +0 -653
  77. 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: string;
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
- // merchant name
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
  };