@frak-labs/core-sdk 0.2.1-beta.b38eef2e → 0.2.1-beta.d2556d47

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 (59) 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 +2 -2
  5. package/dist/actions.d.ts +2 -2
  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-b5cUWdAm.d.ts → computeLegacyProductId-BP-ciVsp.d.cts} +30 -44
  12. package/dist/{computeLegacyProductId-CCAZvLa5.d.cts → computeLegacyProductId-DiJd7RNo.d.ts} +30 -44
  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-B8v3Vtnh.d.ts} +118 -46
  18. package/dist/{openSso-CMzwvaCa.d.ts → openSso-n_B4LSuW.d.cts} +118 -46
  19. package/dist/setupClient-Dr_UYfTD.cjs +13 -0
  20. package/dist/setupClient-TuhDjVJx.js +13 -0
  21. package/dist/siweAuthenticate-0UPcUqI1.js +1 -0
  22. package/dist/{siweAuthenticate-CVigMOxz.d.cts → siweAuthenticate-CDCsp8EJ.d.ts} +8 -5
  23. package/dist/siweAuthenticate-CfQibjZR.cjs +1 -0
  24. package/dist/{siweAuthenticate-CnCZ7mok.d.ts → siweAuthenticate-yITE-iKh.d.cts} +8 -5
  25. package/dist/trackEvent-5j5kkOCj.js +1 -0
  26. package/dist/trackEvent-B2uom25e.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/ensureIdentity.ts +2 -2
  31. package/src/actions/trackPurchaseStatus.test.ts +32 -20
  32. package/src/actions/trackPurchaseStatus.ts +3 -5
  33. package/src/actions/wrapper/modalBuilder.test.ts +4 -2
  34. package/src/actions/wrapper/modalBuilder.ts +6 -8
  35. package/src/clients/createIFrameFrakClient.ts +146 -25
  36. package/src/clients/transports/iframeLifecycleManager.test.ts +0 -80
  37. package/src/clients/transports/iframeLifecycleManager.ts +0 -44
  38. package/src/index.ts +5 -3
  39. package/src/types/config.ts +10 -3
  40. package/src/types/index.ts +6 -1
  41. package/src/types/lifecycle/client.ts +22 -27
  42. package/src/types/lifecycle/iframe.ts +0 -8
  43. package/src/types/resolvedConfig.ts +104 -0
  44. package/src/types/rpc/interaction.ts +4 -0
  45. package/src/types/rpc.ts +7 -5
  46. package/src/utils/backendUrl.test.ts +2 -2
  47. package/src/utils/backendUrl.ts +1 -1
  48. package/src/utils/index.ts +1 -5
  49. package/src/utils/sdkConfigStore.test.ts +405 -0
  50. package/src/utils/sdkConfigStore.ts +277 -0
  51. package/src/utils/sso.ts +3 -7
  52. package/dist/setupClient-CqTHGvVa.cjs +0 -13
  53. package/dist/setupClient-DTyvAPgh.js +0 -13
  54. package/dist/siweAuthenticate-BWmI2_TN.cjs +0 -1
  55. package/dist/siweAuthenticate-zczqxm0a.js +0 -1
  56. package/dist/trackEvent-CeLFVzZn.js +0 -1
  57. package/dist/trackEvent-Ew5r5zfI.cjs +0 -1
  58. package/src/utils/merchantId.test.ts +0 -653
  59. package/src/utils/merchantId.ts +0 -143
@@ -0,0 +1,104 @@
1
+ import type { Currency, Language } from "./config";
2
+
3
+ /**
4
+ * Response from the merchant resolve endpoint
5
+ * @category Config
6
+ */
7
+ export type MerchantConfigResponse = {
8
+ merchantId: string;
9
+ name: string;
10
+ domain: string;
11
+ allowedDomains: string[];
12
+ sdkConfig?: ResolvedSdkConfig;
13
+ };
14
+
15
+ /**
16
+ * Resolved placement config from backend
17
+ * Translations already flattened: default + lang-specific merged into one record
18
+ * @category Config
19
+ */
20
+ export type ResolvedPlacement = {
21
+ /** Per-component configuration within this placement */
22
+ components?: {
23
+ buttonShare?: {
24
+ text?: string;
25
+ noRewardText?: string;
26
+ clickAction?: "embedded-wallet" | "share-modal";
27
+ useReward?: boolean;
28
+ css?: string;
29
+ };
30
+ buttonWallet?: {
31
+ position?: "bottom-right" | "bottom-left";
32
+ css?: string;
33
+ };
34
+ openInApp?: {
35
+ text?: string;
36
+ css?: string;
37
+ };
38
+ };
39
+ targetInteraction?: string;
40
+ /** Already flattened: default + lang-specific merged into one record */
41
+ translations?: Record<string, string>;
42
+ /** Global placement CSS (applied to modals/listener) */
43
+ css?: string;
44
+ };
45
+
46
+ /**
47
+ * Resolved SDK config from backend `/resolve` endpoint
48
+ * Language resolution and translation merging already applied
49
+ * @category Config
50
+ */
51
+ export type ResolvedSdkConfig = {
52
+ name?: string;
53
+ logoUrl?: string;
54
+ homepageLink?: string;
55
+ currency?: Currency;
56
+ lang?: Language;
57
+ /** When true, all SDK components should be hidden */
58
+ hidden?: boolean;
59
+ css?: string;
60
+ translations?: Record<string, string>;
61
+ placements?: Record<string, ResolvedPlacement>;
62
+ };
63
+
64
+ /**
65
+ * Internal SDK config store state
66
+ * Merged config: backend > SDK static > defaults
67
+ * Components subscribe to this reactively
68
+ * @category Config
69
+ */
70
+ export type SdkResolvedConfig = {
71
+ /** Whether the backend config has been resolved */
72
+ isResolved: boolean;
73
+
74
+ /** Merchant ID from resolution */
75
+ merchantId: string;
76
+
77
+ /** Domain returned by the resolve endpoint */
78
+ domain?: string;
79
+
80
+ /** Domains allowed for this merchant (used by iframe trust check) */
81
+ allowedDomains?: string[];
82
+
83
+ /** Whether the resolve returned a backend sdkConfig object */
84
+ hasRawSdkConfig?: boolean;
85
+
86
+ /** Merged metadata fields */
87
+ name?: string;
88
+ logoUrl?: string;
89
+ homepageLink?: string;
90
+ lang?: Language;
91
+ currency?: Currency;
92
+
93
+ /** When true, all SDK components should be hidden */
94
+ hidden?: boolean;
95
+
96
+ /** Global CSS from backend config (passed to iframe) */
97
+ css?: string;
98
+
99
+ /** Global translations (for reference / component fallback) */
100
+ translations?: Record<string, string>;
101
+
102
+ /** Named placements (keyed by placement ID) */
103
+ placements?: Record<string, ResolvedPlacement>;
104
+ };
@@ -26,6 +26,10 @@ export type SendInteractionParamsType =
26
26
  }
27
27
  | {
28
28
  type: "sharing";
29
+ /** Epoch seconds timestamp matching the V2 context `t` field embedded in the referral link URL, used for backend correlation */
30
+ sharingTimestamp?: number;
31
+ /** Merchant order ID linking this sharing event to a purchase (stays server-side, never in URL) */
32
+ purchaseId?: string;
29
33
  }
30
34
  | {
31
35
  type: "custom";
package/src/types/rpc.ts CHANGED
@@ -38,7 +38,7 @@ import type { WalletStatusReturnType } from "./rpc/walletStatus";
38
38
  * - Response Type: stream (emits updates when wallet status changes)
39
39
  *
40
40
  * #### frak_displayModal
41
- * - Params: [requests: {@link ModalRpcStepsInput}, metadata?: {@link ModalRpcMetadata}, configMetadata: {@link FrakWalletSdkConfig}["metadata"]]
41
+ * - Params: [requests: {@link ModalRpcStepsInput}, metadata?: {@link ModalRpcMetadata}, configMetadata: {@link FrakWalletSdkConfig}["metadata"], placement?: string]
42
42
  * - Returns: {@link ModalRpcStepsResultType}
43
43
  * - Response Type: promise (one-shot)
44
44
  *
@@ -53,7 +53,7 @@ import type { WalletStatusReturnType } from "./rpc/walletStatus";
53
53
  * - Response Type: promise (one-shot)
54
54
  *
55
55
  * #### frak_displayEmbeddedWallet
56
- * - Params: [request: {@link DisplayEmbeddedWalletParamsType}, metadata: {@link FrakWalletSdkConfig}["metadata"]]
56
+ * - Params: [request: {@link DisplayEmbeddedWalletParamsType}, metadata: {@link FrakWalletSdkConfig}["metadata"], placement?: string]
57
57
  * - Returns: {@link DisplayEmbeddedWalletResultType}
58
58
  * - Response Type: promise (one-shot)
59
59
  */
@@ -77,6 +77,7 @@ export type IFrameRpcSchema = [
77
77
  requests: ModalRpcStepsInput,
78
78
  metadata: ModalRpcMetadata | undefined,
79
79
  configMetadata: FrakWalletSdkConfig["metadata"],
80
+ placement?: string,
80
81
  ];
81
82
  ReturnType: ModalRpcStepsResultType;
82
83
  },
@@ -89,7 +90,7 @@ export type IFrameRpcSchema = [
89
90
  Method: "frak_prepareSso";
90
91
  Parameters: [
91
92
  params: PrepareSsoParamsType,
92
- name: string,
93
+ name?: string,
93
94
  customCss?: string,
94
95
  ];
95
96
  ReturnType: PrepareSsoReturnType;
@@ -104,7 +105,7 @@ export type IFrameRpcSchema = [
104
105
  Method: "frak_openSso";
105
106
  Parameters: [
106
107
  params: OpenSsoParamsType,
107
- name: string,
108
+ name?: string,
108
109
  customCss?: string,
109
110
  ];
110
111
  ReturnType: OpenSsoReturnType;
@@ -130,6 +131,7 @@ export type IFrameRpcSchema = [
130
131
  Parameters: [
131
132
  request: DisplayEmbeddedWalletParamsType,
132
133
  metadata: FrakWalletSdkConfig["metadata"],
134
+ placement?: string,
133
135
  ];
134
136
  ReturnType: DisplayEmbeddedWalletResultType;
135
137
  },
@@ -137,7 +139,7 @@ export type IFrameRpcSchema = [
137
139
  * Method to send interactions (arrival, sharing, custom events)
138
140
  * Fire-and-forget method - no return value expected
139
141
  * merchantId is resolved from context
140
- * clientId is passed via metadata as safeguard against handshake race condition
142
+ * clientId is passed via metadata as safeguard against race conditions
141
143
  */
142
144
  {
143
145
  Method: "frak_sendInteraction";
@@ -15,13 +15,13 @@ describe("getBackendUrl", () => {
15
15
  describe("with explicit walletUrl", () => {
16
16
  test("should return localhost backend for localhost:3000", () => {
17
17
  expect(getBackendUrl("https://localhost:3000")).toBe(
18
- "http://localhost:3030"
18
+ "https://localhost:3030"
19
19
  );
20
20
  });
21
21
 
22
22
  test("should return localhost backend for localhost:3010", () => {
23
23
  expect(getBackendUrl("https://localhost:3010")).toBe(
24
- "http://localhost:3030"
24
+ "https://localhost:3030"
25
25
  );
26
26
  });
27
27
 
@@ -19,7 +19,7 @@ function isLocalDevelopment(walletUrl: string): boolean {
19
19
  */
20
20
  function deriveBackendUrl(walletUrl: string): string {
21
21
  if (isLocalDevelopment(walletUrl)) {
22
- return "http://localhost:3030";
22
+ return "https://localhost:3030";
23
23
  }
24
24
  // Dev environment
25
25
  if (
@@ -22,11 +22,7 @@ export {
22
22
  createIframe,
23
23
  findIframeInOpener,
24
24
  } from "./iframeHelper";
25
- export {
26
- clearMerchantIdCache,
27
- fetchMerchantId,
28
- resolveMerchantId,
29
- } from "./merchantId";
25
+ export { sdkConfigStore } from "./sdkConfigStore";
30
26
  export {
31
27
  type AppSpecificSsoMetadata,
32
28
  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
+ });