@frak-labs/core-sdk 0.2.1 → 1.0.0-beta.61e6fb99

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 (92) hide show
  1. package/README.md +1 -2
  2. package/cdn/bundle.js +3 -3
  3. package/dist/actions-Di4welXI.cjs +1 -0
  4. package/dist/actions-DyMkUe65.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-B_Uj-puh.d.ts} +249 -73
  14. package/dist/{computeLegacyProductId-b5cUWdAm.d.ts → index-ByVpu25D.d.cts} +249 -73
  15. package/dist/{siweAuthenticate-CnCZ7mok.d.ts → index-CGyEOo9J.d.cts} +122 -8
  16. package/dist/{siweAuthenticate-CVigMOxz.d.cts → index-Cdf5j2_W.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-B6pD2oA6.d.ts} +380 -46
  22. package/dist/{openSso-CMzwvaCa.d.ts → openSso-qjaccFd0.d.cts} +379 -45
  23. package/dist/sdkConfigStore-DvwFc6Ym.cjs +1 -0
  24. package/dist/sdkConfigStore-M37skmM8.js +1 -0
  25. package/dist/src-BqpqVHCq.cjs +13 -0
  26. package/dist/src-BxRYON49.js +13 -0
  27. package/package.json +12 -13
  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/processReferral.test.ts +4 -8
  38. package/src/actions/referral/processReferral.ts +5 -11
  39. package/src/actions/referral/setupReferral.test.ts +79 -0
  40. package/src/actions/referral/setupReferral.ts +32 -0
  41. package/src/actions/trackPurchaseStatus.test.ts +32 -20
  42. package/src/actions/trackPurchaseStatus.ts +3 -5
  43. package/src/actions/wrapper/modalBuilder.test.ts +4 -2
  44. package/src/actions/wrapper/modalBuilder.ts +6 -8
  45. package/src/clients/createIFrameFrakClient.ts +233 -28
  46. package/src/clients/transports/iframeLifecycleManager.test.ts +14 -94
  47. package/src/clients/transports/iframeLifecycleManager.ts +35 -53
  48. package/src/index.ts +25 -5
  49. package/src/stubs/rrweb.ts +9 -0
  50. package/src/types/config.ts +19 -3
  51. package/src/types/index.ts +15 -1
  52. package/src/types/lifecycle/client.ts +29 -27
  53. package/src/types/lifecycle/iframe.ts +7 -8
  54. package/src/types/resolvedConfig.ts +138 -0
  55. package/src/types/rpc/displaySharingPage.ts +100 -0
  56. package/src/types/rpc/embedded/index.ts +1 -1
  57. package/src/types/rpc/interaction.ts +4 -0
  58. package/src/types/rpc/userReferralStatus.ts +20 -0
  59. package/src/types/rpc.ts +54 -5
  60. package/src/types/tracking.ts +36 -0
  61. package/src/utils/FrakContext.test.ts +151 -0
  62. package/src/utils/FrakContext.ts +67 -1
  63. package/src/utils/analytics/events/component.ts +58 -0
  64. package/src/utils/analytics/events/index.ts +20 -0
  65. package/src/utils/analytics/events/lifecycle.ts +26 -0
  66. package/src/utils/analytics/events/referral.ts +10 -0
  67. package/src/utils/analytics/index.ts +8 -0
  68. package/src/utils/{trackEvent.test.ts → analytics/trackEvent.test.ts} +22 -30
  69. package/src/utils/analytics/trackEvent.ts +34 -0
  70. package/src/utils/backendUrl.test.ts +2 -2
  71. package/src/utils/backendUrl.ts +1 -1
  72. package/src/utils/cache/index.ts +7 -0
  73. package/src/utils/cache/lruMap.test.ts +55 -0
  74. package/src/utils/cache/lruMap.ts +38 -0
  75. package/src/utils/cache/withCache.test.ts +168 -0
  76. package/src/utils/cache/withCache.ts +124 -0
  77. package/src/utils/inAppBrowser.ts +60 -0
  78. package/src/utils/index.ts +11 -5
  79. package/src/utils/mergeAttribution.test.ts +153 -0
  80. package/src/utils/mergeAttribution.ts +75 -0
  81. package/src/utils/sdkConfigStore.test.ts +405 -0
  82. package/src/utils/sdkConfigStore.ts +263 -0
  83. package/src/utils/sso.ts +3 -7
  84. package/dist/setupClient-BduY6Sym.cjs +0 -13
  85. package/dist/setupClient-ftmdQ-I8.js +0 -13
  86. package/dist/siweAuthenticate-BWmI2_TN.cjs +0 -1
  87. package/dist/siweAuthenticate-zczqxm0a.js +0 -1
  88. package/dist/trackEvent-CeLFVzZn.js +0 -1
  89. package/dist/trackEvent-Ew5r5zfI.cjs +0 -1
  90. package/src/utils/merchantId.test.ts +0 -653
  91. package/src/utils/merchantId.ts +0 -143
  92. package/src/utils/trackEvent.ts +0 -41
@@ -0,0 +1,153 @@
1
+ import { describe, expect, it } from "../../tests/vitest-fixtures";
2
+ import { mergeAttribution } from "./mergeAttribution";
3
+
4
+ describe("mergeAttribution", () => {
5
+ describe("explicit disable", () => {
6
+ it("returns undefined when perCall is null", () => {
7
+ expect(
8
+ mergeAttribution({
9
+ perCall: null,
10
+ defaults: { utmSource: "brand" },
11
+ productUtmContent: "product-123",
12
+ })
13
+ ).toBeUndefined();
14
+ });
15
+ });
16
+
17
+ describe("no inputs", () => {
18
+ it("returns undefined when all layers are empty", () => {
19
+ expect(mergeAttribution({ perCall: undefined })).toBeUndefined();
20
+ });
21
+
22
+ it("returns undefined when perCall is undefined and defaults is empty object", () => {
23
+ expect(
24
+ mergeAttribution({ perCall: undefined, defaults: {} })
25
+ ).toBeUndefined();
26
+ });
27
+ });
28
+
29
+ describe("defaults only", () => {
30
+ it("returns defaults when perCall is undefined", () => {
31
+ expect(
32
+ mergeAttribution({
33
+ perCall: undefined,
34
+ defaults: {
35
+ utmSource: "brand",
36
+ utmMedium: "newsletter",
37
+ },
38
+ })
39
+ ).toEqual({
40
+ utmSource: "brand",
41
+ utmMedium: "newsletter",
42
+ });
43
+ });
44
+ });
45
+
46
+ describe("perCall only", () => {
47
+ it("returns perCall when defaults is undefined", () => {
48
+ expect(
49
+ mergeAttribution({
50
+ perCall: { utmSource: "custom", utmContent: "hero" },
51
+ })
52
+ ).toEqual({
53
+ utmSource: "custom",
54
+ utmContent: "hero",
55
+ });
56
+ });
57
+
58
+ it("returns empty object when perCall is empty and no defaults", () => {
59
+ // perCall: {} signals \"apply attribution with hardcoded defaults downstream\"
60
+ expect(mergeAttribution({ perCall: {} })).toEqual({});
61
+ });
62
+ });
63
+
64
+ describe("per-field merge (perCall wins over defaults)", () => {
65
+ it("merges perCall over defaults field-by-field", () => {
66
+ expect(
67
+ mergeAttribution({
68
+ perCall: { utmMedium: "email" },
69
+ defaults: {
70
+ utmSource: "brand",
71
+ utmMedium: "newsletter",
72
+ utmCampaign: "spring",
73
+ },
74
+ })
75
+ ).toEqual({
76
+ utmSource: "brand",
77
+ utmMedium: "email",
78
+ utmCampaign: "spring",
79
+ });
80
+ });
81
+
82
+ it("lets perCall override every default field", () => {
83
+ expect(
84
+ mergeAttribution({
85
+ perCall: {
86
+ utmSource: "pc-src",
87
+ utmMedium: "pc-med",
88
+ utmCampaign: "pc-camp",
89
+ utmTerm: "pc-term",
90
+ via: "pc-via",
91
+ ref: "pc-ref",
92
+ },
93
+ defaults: {
94
+ utmSource: "def-src",
95
+ utmMedium: "def-med",
96
+ utmCampaign: "def-camp",
97
+ utmTerm: "def-term",
98
+ via: "def-via",
99
+ ref: "def-ref",
100
+ },
101
+ })
102
+ ).toEqual({
103
+ utmSource: "pc-src",
104
+ utmMedium: "pc-med",
105
+ utmCampaign: "pc-camp",
106
+ utmTerm: "pc-term",
107
+ via: "pc-via",
108
+ ref: "pc-ref",
109
+ });
110
+ });
111
+ });
112
+
113
+ describe("utm_content handling", () => {
114
+ it("uses productUtmContent when provided", () => {
115
+ expect(
116
+ mergeAttribution({
117
+ perCall: { utmContent: "fallback" },
118
+ productUtmContent: "product-42",
119
+ })
120
+ ).toEqual({ utmContent: "product-42" });
121
+ });
122
+
123
+ it("falls back to perCall.utmContent when productUtmContent is absent", () => {
124
+ expect(
125
+ mergeAttribution({
126
+ perCall: { utmContent: "fallback" },
127
+ })
128
+ ).toEqual({ utmContent: "fallback" });
129
+ });
130
+
131
+ it("never inherits utm_content from defaults (shape excludes it)", () => {
132
+ // Even if a backend/SDK config erroneously contained utm_content
133
+ // at runtime, the merged result must not carry it.
134
+ expect(
135
+ mergeAttribution({
136
+ perCall: {},
137
+ // @ts-expect-error — defaults typing disallows utmContent,
138
+ // but we simulate runtime data coming from a loose source.
139
+ defaults: { utmContent: "should-not-leak" },
140
+ })
141
+ ).toEqual({});
142
+ });
143
+
144
+ it("adds attribution solely to carry a productUtmContent", () => {
145
+ expect(
146
+ mergeAttribution({
147
+ perCall: undefined,
148
+ productUtmContent: "product-7",
149
+ })
150
+ ).toEqual({ utmContent: "product-7" });
151
+ });
152
+ });
153
+ });
@@ -0,0 +1,75 @@
1
+ import type { AttributionDefaults, AttributionParams } from "../types/tracking";
2
+
3
+ /**
4
+ * Inputs for {@link mergeAttribution}.
5
+ */
6
+ export type MergeAttributionInput = {
7
+ /**
8
+ * Per-call attribution override passed to actions like `displaySharingPage`.
9
+ *
10
+ * - `null` explicitly disables attribution (no UTM/ref/via params are added).
11
+ * - `undefined` means "no per-call override" — defaults apply if present.
12
+ * - An object (including `{}`) merges field-by-field with defaults.
13
+ */
14
+ perCall: AttributionParams | null | undefined;
15
+ /**
16
+ * Pre-merged merchant-level defaults (backend config > SDK static config).
17
+ * `utm_content` is intentionally absent from this shape.
18
+ */
19
+ defaults?: AttributionDefaults;
20
+ /**
21
+ * Per-product `utm_content` override (from the currently selected
22
+ * `SharingPageProduct`). Takes precedence over `perCall.utmContent`.
23
+ */
24
+ productUtmContent?: string;
25
+ };
26
+
27
+ /**
28
+ * Merge the three attribution layers into a single {@link AttributionParams}
29
+ * value suitable for `FrakContextManager.update`.
30
+ *
31
+ * Priority per field:
32
+ * 1. `perCall` (wins)
33
+ * 2. `defaults` (merchant-level, backend > SDK static, already pre-merged)
34
+ * 3. Hardcoded fallbacks resolved later by `FrakContextManager`
35
+ *
36
+ * Special rules:
37
+ * - `perCall === null` returns `undefined` (explicit disable: no UTM/ref/via).
38
+ * - `perCall === undefined` (no opinion) yields at least `{}` so `FrakContextManager`
39
+ * applies its hardcoded defaults (utm_source=frak, utm_medium=referral,
40
+ * utm_campaign=<merchantId>, ref=<clientId>, via=frak).
41
+ * - `utm_content` never comes from `defaults`; only `productUtmContent` or
42
+ * `perCall.utmContent` can populate it.
43
+ */
44
+ export function mergeAttribution({
45
+ perCall,
46
+ defaults,
47
+ productUtmContent,
48
+ }: MergeAttributionInput): AttributionParams | undefined {
49
+ // Explicit disable
50
+ if (perCall === null) return undefined;
51
+
52
+ const hasPerCall = perCall !== undefined;
53
+ const hasDefaults =
54
+ defaults !== undefined && Object.keys(defaults).length > 0;
55
+ const hasProductUtm =
56
+ productUtmContent !== undefined && productUtmContent !== "";
57
+
58
+ if (!hasPerCall && !hasDefaults && !hasProductUtm) return undefined;
59
+
60
+ // Per-field merge: per-call wins over defaults.
61
+ const merged: AttributionParams = {
62
+ ...defaults,
63
+ ...(perCall ?? {}),
64
+ };
65
+
66
+ // utm_content priority: productUtmContent > perCall.utmContent; never from defaults.
67
+ const utmContent = productUtmContent ?? perCall?.utmContent;
68
+ if (utmContent !== undefined && utmContent !== "") {
69
+ merged.utmContent = utmContent;
70
+ } else {
71
+ delete merged.utmContent;
72
+ }
73
+
74
+ return merged;
75
+ }
@@ -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
+ });