@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.
Files changed (74) hide show
  1. package/README.md +1 -2
  2. package/cdn/bundle.js +55 -3
  3. package/dist/actions.cjs +1 -1
  4. package/dist/actions.d.cts +3 -3
  5. package/dist/actions.d.ts +3 -3
  6. package/dist/actions.js +1 -1
  7. package/dist/bundle.cjs +1 -1
  8. package/dist/bundle.d.cts +4 -4
  9. package/dist/bundle.d.ts +4 -4
  10. package/dist/bundle.js +1 -1
  11. package/dist/{computeLegacyProductId-CCAZvLa5.d.cts → computeLegacyProductId-C35yITjX.d.ts} +91 -37
  12. package/dist/{computeLegacyProductId-b5cUWdAm.d.ts → computeLegacyProductId-mG4x5Cq0.d.cts} +91 -37
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.d.cts +3 -3
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +1 -1
  17. package/dist/{openSso-B0g7-807.d.cts → openSso-7e-OKMhg.d.ts} +266 -46
  18. package/dist/{openSso-CMzwvaCa.d.ts → openSso-BebjXCq0.d.cts} +266 -46
  19. package/dist/setupClient-CUYyoXtI.js +13 -0
  20. package/dist/setupClient-LgwHIFJc.cjs +13 -0
  21. package/dist/{siweAuthenticate-CnCZ7mok.d.ts → siweAuthenticate-BQEMZRg3.d.cts} +102 -8
  22. package/dist/siweAuthenticate-CWcVvP-G.cjs +1 -0
  23. package/dist/{siweAuthenticate-CVigMOxz.d.cts → siweAuthenticate-CdLD-9W2.d.ts} +102 -8
  24. package/dist/siweAuthenticate-DQfdb5UQ.js +1 -0
  25. package/dist/trackEvent-Ce1XlsIE.js +1 -0
  26. package/dist/trackEvent-CvbJTTqA.cjs +1 -0
  27. package/package.json +8 -8
  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/getUserReferralStatus.ts +42 -0
  35. package/src/actions/index.ts +7 -1
  36. package/src/actions/referral/setupReferral.test.ts +79 -0
  37. package/src/actions/referral/setupReferral.ts +32 -0
  38. package/src/actions/trackPurchaseStatus.test.ts +32 -20
  39. package/src/actions/trackPurchaseStatus.ts +3 -5
  40. package/src/actions/wrapper/modalBuilder.test.ts +4 -2
  41. package/src/actions/wrapper/modalBuilder.ts +6 -8
  42. package/src/clients/createIFrameFrakClient.ts +150 -27
  43. package/src/clients/transports/iframeLifecycleManager.test.ts +14 -94
  44. package/src/clients/transports/iframeLifecycleManager.ts +15 -48
  45. package/src/index.ts +17 -4
  46. package/src/types/config.ts +10 -3
  47. package/src/types/index.ts +13 -1
  48. package/src/types/lifecycle/client.ts +22 -27
  49. package/src/types/lifecycle/iframe.ts +7 -8
  50. package/src/types/resolvedConfig.ts +123 -0
  51. package/src/types/rpc/displaySharingPage.ts +77 -0
  52. package/src/types/rpc/interaction.ts +4 -0
  53. package/src/types/rpc/userReferralStatus.ts +20 -0
  54. package/src/types/rpc.ts +42 -5
  55. package/src/utils/backendUrl.test.ts +2 -2
  56. package/src/utils/backendUrl.ts +1 -1
  57. package/src/utils/cache/index.ts +7 -0
  58. package/src/utils/cache/lruMap.test.ts +55 -0
  59. package/src/utils/cache/lruMap.ts +38 -0
  60. package/src/utils/cache/withCache.test.ts +162 -0
  61. package/src/utils/cache/withCache.ts +105 -0
  62. package/src/utils/inAppBrowser.ts +60 -0
  63. package/src/utils/index.ts +6 -4
  64. package/src/utils/sdkConfigStore.test.ts +405 -0
  65. package/src/utils/sdkConfigStore.ts +263 -0
  66. package/src/utils/sso.ts +3 -7
  67. package/dist/setupClient-CqTHGvVa.cjs +0 -13
  68. package/dist/setupClient-DTyvAPgh.js +0 -13
  69. package/dist/siweAuthenticate-BWmI2_TN.cjs +0 -1
  70. package/dist/siweAuthenticate-zczqxm0a.js +0 -1
  71. package/dist/trackEvent-CeLFVzZn.js +0 -1
  72. package/dist/trackEvent-Ew5r5zfI.cjs +0 -1
  73. package/src/utils/merchantId.test.ts +0 -653
  74. package/src/utils/merchantId.ts +0 -143
@@ -0,0 +1,105 @@
1
+ import { LruMap } from "./lruMap";
2
+
3
+ type CacheEntry<TData> = {
4
+ data: TData;
5
+ created: number;
6
+ };
7
+
8
+ /** Global cache for in-flight promises (dedup concurrent calls) */
9
+ const promiseCache = new LruMap<Promise<unknown>>(1024);
10
+
11
+ /** Global cache for resolved responses (TTL-based) */
12
+ const responseCache = new LruMap<CacheEntry<unknown>>(1024);
13
+
14
+ /** Default cache time: 30 seconds */
15
+ export const DEFAULT_CACHE_TIME = 30_000;
16
+
17
+ type WithCacheOptions = {
18
+ /** The key to cache the data against */
19
+ cacheKey: string;
20
+ /** Time in ms that cached data will remain valid. Default: 30_000 (30s). Set to 0 to disable caching. */
21
+ cacheTime?: number;
22
+ };
23
+
24
+ /**
25
+ * Returns the result of a given promise, and caches the result for
26
+ * subsequent invocations against a provided cache key.
27
+ *
28
+ * Also deduplicates concurrent calls — if multiple callers request the same
29
+ * cache key while the promise is pending, they share the same promise.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * // First call fetches, subsequent calls return cached data for 30s
34
+ * const data = await withCache(
35
+ * () => client.request({ method: "frak_getMerchantInformation" }),
36
+ * { cacheKey: "merchantInfo", cacheTime: 30_000 }
37
+ * );
38
+ * ```
39
+ */
40
+ export async function withCache<TData>(
41
+ fn: () => Promise<TData>,
42
+ { cacheKey, cacheTime = DEFAULT_CACHE_TIME }: WithCacheOptions
43
+ ): Promise<TData> {
44
+ // Check response cache — return immediately if fresh
45
+ if (cacheTime > 0) {
46
+ const cached = responseCache.get(cacheKey) as
47
+ | CacheEntry<TData>
48
+ | undefined;
49
+ if (cached) {
50
+ const age = Date.now() - cached.created;
51
+ if (age < cacheTime) return cached.data;
52
+ }
53
+ }
54
+
55
+ // Check if there's already a pending promise (dedup concurrent calls)
56
+ let promise = promiseCache.get(cacheKey) as Promise<TData> | undefined;
57
+ if (!promise) {
58
+ promise = fn();
59
+ promiseCache.set(cacheKey, promise);
60
+ }
61
+
62
+ try {
63
+ const data = await promise;
64
+ // Store the response with a timestamp
65
+ responseCache.set(cacheKey, { data, created: Date.now() });
66
+ return data;
67
+ } finally {
68
+ // Clear the promise cache so subsequent calls can re-fetch after TTL
69
+ promiseCache.delete(cacheKey);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Get a cache handle for a specific key, useful for manual invalidation.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * // Invalidate merchant info cache after a mutation
79
+ * getCache("frak_getMerchantInformation").clear();
80
+ * ```
81
+ */
82
+ export function getCache(cacheKey: string) {
83
+ return {
84
+ /** Clear both the pending promise and the cached response */
85
+ clear: () => {
86
+ promiseCache.delete(cacheKey);
87
+ responseCache.delete(cacheKey);
88
+ },
89
+ /** Check if a non-expired response exists */
90
+ has: (cacheTime: number = DEFAULT_CACHE_TIME) => {
91
+ const cached = responseCache.get(cacheKey);
92
+ if (!cached) return false;
93
+ return Date.now() - cached.created < cacheTime;
94
+ },
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Clear all cached data (both pending promises and resolved responses).
100
+ * Called automatically when the client is destroyed.
101
+ */
102
+ export function clearAllCache() {
103
+ promiseCache.clear();
104
+ responseCache.clear();
105
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Check if the current device runs iOS (including iPadOS 13+).
3
+ */
4
+ function checkIsIOS(): boolean {
5
+ if (typeof navigator === "undefined") return false;
6
+ const ua = navigator.userAgent;
7
+ // Standard iOS devices
8
+ if (/iPhone|iPad|iPod/i.test(ua)) return true;
9
+ // iPadOS 13+ reports as Macintosh — detect via touch support
10
+ if (/Macintosh/i.test(ua) && navigator.maxTouchPoints > 1) return true;
11
+ return false;
12
+ }
13
+
14
+ /**
15
+ * Whether the current device runs iOS (including iPadOS 13+).
16
+ */
17
+ export const isIOS: boolean = checkIsIOS();
18
+
19
+ /**
20
+ * Check if the current browser is a social media in-app browser
21
+ * (Instagram, Facebook WebView).
22
+ */
23
+ function checkInAppBrowser(): boolean {
24
+ if (typeof navigator === "undefined") return false;
25
+ const ua = navigator.userAgent.toLowerCase();
26
+ return (
27
+ ua.includes("instagram") ||
28
+ ua.includes("fban") ||
29
+ ua.includes("fbav") ||
30
+ ua.includes("facebook")
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Whether the current browser is a social media in-app browser
36
+ * (Instagram, Facebook).
37
+ */
38
+ export const isInAppBrowser: boolean = checkInAppBrowser();
39
+
40
+ /**
41
+ * Redirect to external browser from in-app WebView.
42
+ *
43
+ * - **iOS**: Uses `x-safari-https://` scheme — server-side 302 redirects
44
+ * to custom URL schemes are silently swallowed by WKWebView.
45
+ * Direct `window.location.href` assignment works (confirmed iOS 17+).
46
+ *
47
+ * - **Android**: Uses backend `/common/social` endpoint which returns a PDF
48
+ * Content-Type response, forcing the WebView to hand off to the default browser.
49
+ *
50
+ * @param targetUrl - The URL to open in the external browser
51
+ */
52
+ export function redirectToExternalBrowser(targetUrl: string): void {
53
+ if (isIOS && targetUrl.startsWith("https://")) {
54
+ window.location.href = `x-safari-https://${targetUrl.slice(8)}`;
55
+ } else if (isIOS && targetUrl.startsWith("http://")) {
56
+ window.location.href = `x-safari-http://${targetUrl.slice(7)}`;
57
+ } else {
58
+ window.location.href = `${process.env.BACKEND_URL}/common/social?u=${encodeURIComponent(targetUrl)}`;
59
+ }
60
+ }
@@ -1,5 +1,6 @@
1
1
  export { Deferred } from "@frak-labs/frame-connector";
2
2
  export { getBackendUrl } from "./backendUrl";
3
+ export { clearAllCache, getCache, withCache } from "./cache";
3
4
  export { getClientId } from "./clientId";
4
5
  export { base64urlDecode, base64urlEncode } from "./compression/b64";
5
6
  export { compressJsonToB64 } from "./compression/compress";
@@ -23,10 +24,11 @@ export {
23
24
  findIframeInOpener,
24
25
  } from "./iframeHelper";
25
26
  export {
26
- clearMerchantIdCache,
27
- fetchMerchantId,
28
- resolveMerchantId,
29
- } from "./merchantId";
27
+ isInAppBrowser,
28
+ isIOS,
29
+ redirectToExternalBrowser,
30
+ } from "./inAppBrowser";
31
+ export { sdkConfigStore } from "./sdkConfigStore";
30
32
  export {
31
33
  type AppSpecificSsoMetadata,
32
34
  type CompressedSsoData,
@@ -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
+ });