@frak-labs/core-sdk 0.1.1 → 0.2.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 (125) hide show
  1. package/README.md +58 -0
  2. package/cdn/bundle.js +14 -0
  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 -6
  9. package/dist/bundle.d.ts +4 -6
  10. package/dist/bundle.js +1 -1
  11. package/dist/{index-CRsQWnTs.d.cts → computeLegacyProductId-BkyJ4rEY.d.ts} +197 -10
  12. package/dist/{index-Ck1hudEi.d.ts → computeLegacyProductId-Raks6FXg.d.cts} +197 -10
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.d.cts +3 -4
  15. package/dist/index.d.ts +3 -4
  16. package/dist/index.js +1 -1
  17. package/dist/{openSso-D--Airj6.d.cts → openSso-BCJGchIb.d.cts} +135 -131
  18. package/dist/{openSso-DsKJ4y0j.d.ts → openSso-DG-_9CED.d.ts} +135 -131
  19. package/dist/setupClient-CQrMDGyZ.js +13 -0
  20. package/dist/setupClient-Ccv3XxwL.cjs +13 -0
  21. package/dist/{index-d8xS4ryI.d.ts → siweAuthenticate-BH7Dn7nZ.d.cts} +90 -65
  22. package/dist/siweAuthenticate-BJHbtty4.js +1 -0
  23. package/dist/{index-C6FxkWPC.d.cts → siweAuthenticate-Btem4QHs.d.ts} +90 -65
  24. package/dist/siweAuthenticate-Cwj3HP0m.cjs +1 -0
  25. package/dist/trackEvent-M2RLTQ2p.js +1 -0
  26. package/dist/trackEvent-T_R9ER2S.cjs +1 -0
  27. package/package.json +11 -22
  28. package/src/actions/displayEmbeddedWallet.ts +1 -0
  29. package/src/actions/displayModal.test.ts +12 -11
  30. package/src/actions/displayModal.ts +7 -18
  31. package/src/actions/ensureIdentity.ts +68 -0
  32. package/src/actions/{getProductInformation.test.ts → getMerchantInformation.test.ts} +33 -50
  33. package/src/actions/getMerchantInformation.ts +16 -0
  34. package/src/actions/index.ts +3 -2
  35. package/src/actions/openSso.ts +4 -2
  36. package/src/actions/referral/processReferral.test.ts +42 -151
  37. package/src/actions/referral/processReferral.ts +18 -42
  38. package/src/actions/referral/referralInteraction.test.ts +1 -7
  39. package/src/actions/referral/referralInteraction.ts +1 -6
  40. package/src/actions/sendInteraction.ts +46 -22
  41. package/src/actions/trackPurchaseStatus.test.ts +354 -141
  42. package/src/actions/trackPurchaseStatus.ts +48 -11
  43. package/src/actions/watchWalletStatus.ts +2 -3
  44. package/src/actions/wrapper/modalBuilder.test.ts +0 -14
  45. package/src/actions/wrapper/modalBuilder.ts +3 -12
  46. package/src/bundle.ts +0 -1
  47. package/src/clients/createIFrameFrakClient.ts +10 -5
  48. package/src/clients/transports/iframeLifecycleManager.test.ts +163 -4
  49. package/src/clients/transports/iframeLifecycleManager.ts +172 -33
  50. package/src/constants/interactionTypes.ts +12 -41
  51. package/src/index.ts +24 -16
  52. package/src/types/config.ts +6 -0
  53. package/src/types/index.ts +13 -10
  54. package/src/types/lifecycle/client.ts +24 -1
  55. package/src/types/lifecycle/iframe.ts +6 -0
  56. package/src/types/rpc/displayModal.ts +2 -4
  57. package/src/types/rpc/embedded/index.ts +2 -2
  58. package/src/types/rpc/interaction.ts +26 -39
  59. package/src/types/rpc/merchantInformation.ts +77 -0
  60. package/src/types/rpc/modal/index.ts +0 -4
  61. package/src/types/rpc/modal/login.ts +5 -1
  62. package/src/types/rpc/walletStatus.ts +1 -7
  63. package/src/types/rpc.ts +22 -30
  64. package/src/types/tracking.ts +60 -0
  65. package/src/utils/backendUrl.test.ts +83 -0
  66. package/src/utils/backendUrl.ts +62 -0
  67. package/src/utils/clientId.test.ts +41 -0
  68. package/src/utils/clientId.ts +43 -0
  69. package/src/utils/compression/compress.test.ts +1 -1
  70. package/src/utils/compression/compress.ts +2 -2
  71. package/src/utils/compression/decompress.test.ts +8 -4
  72. package/src/utils/compression/decompress.ts +2 -2
  73. package/src/utils/{computeProductId.ts → computeLegacyProductId.ts} +2 -2
  74. package/src/utils/constants.ts +5 -0
  75. package/src/utils/deepLinkWithFallback.test.ts +243 -0
  76. package/src/utils/deepLinkWithFallback.ts +103 -0
  77. package/src/utils/formatAmount.ts +6 -0
  78. package/src/utils/iframeHelper.test.ts +18 -5
  79. package/src/utils/iframeHelper.ts +10 -3
  80. package/src/utils/index.ts +16 -1
  81. package/src/utils/merchantId.test.ts +653 -0
  82. package/src/utils/merchantId.ts +143 -0
  83. package/src/utils/sso.ts +18 -11
  84. package/src/utils/trackEvent.test.ts +23 -5
  85. package/src/utils/trackEvent.ts +13 -0
  86. package/cdn/bundle.iife.js +0 -14
  87. package/dist/actions-B5j-i1p0.cjs +0 -1
  88. package/dist/actions-q090Z0oR.js +0 -1
  89. package/dist/index-7OZ39x1U.d.ts +0 -195
  90. package/dist/index-zDq-VlKx.d.cts +0 -195
  91. package/dist/interaction-DMJ3ZfaF.d.cts +0 -45
  92. package/dist/interaction-KX1h9a7V.d.ts +0 -45
  93. package/dist/interactions-DnfM3oe0.js +0 -1
  94. package/dist/interactions-EIXhNLf6.cjs +0 -1
  95. package/dist/interactions.cjs +0 -1
  96. package/dist/interactions.d.cts +0 -2
  97. package/dist/interactions.d.ts +0 -2
  98. package/dist/interactions.js +0 -1
  99. package/dist/productTypes-BUkXJKZ7.cjs +0 -1
  100. package/dist/productTypes-CGb1MmBF.js +0 -1
  101. package/dist/src-1LQ4eLq5.js +0 -13
  102. package/dist/src-hW71KjPN.cjs +0 -13
  103. package/dist/trackEvent-CHnYa85W.js +0 -1
  104. package/dist/trackEvent-GuQm_1Nm.cjs +0 -1
  105. package/src/actions/getProductInformation.ts +0 -14
  106. package/src/actions/openSso.test.ts +0 -407
  107. package/src/actions/sendInteraction.test.ts +0 -219
  108. package/src/constants/interactionTypes.test.ts +0 -128
  109. package/src/constants/productTypes.test.ts +0 -130
  110. package/src/constants/productTypes.ts +0 -33
  111. package/src/interactions/index.ts +0 -5
  112. package/src/interactions/pressEncoder.test.ts +0 -215
  113. package/src/interactions/pressEncoder.ts +0 -53
  114. package/src/interactions/purchaseEncoder.test.ts +0 -291
  115. package/src/interactions/purchaseEncoder.ts +0 -99
  116. package/src/interactions/referralEncoder.test.ts +0 -170
  117. package/src/interactions/referralEncoder.ts +0 -47
  118. package/src/interactions/retailEncoder.test.ts +0 -107
  119. package/src/interactions/retailEncoder.ts +0 -37
  120. package/src/interactions/webshopEncoder.test.ts +0 -56
  121. package/src/interactions/webshopEncoder.ts +0 -30
  122. package/src/types/rpc/modal/openSession.ts +0 -25
  123. package/src/types/rpc/productInformation.ts +0 -59
  124. package/src/utils/computeProductId.test.ts +0 -80
  125. package/src/utils/sso.test.ts +0 -361
@@ -0,0 +1,653 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ clearMerchantIdCache,
4
+ fetchMerchantId,
5
+ resolveMerchantId,
6
+ } from "./merchantId";
7
+
8
+ // Mock the backendUrl module
9
+ vi.mock("./backendUrl", () => ({
10
+ getBackendUrl: vi.fn((walletUrl?: string) => {
11
+ if (walletUrl?.includes("localhost")) {
12
+ return "http://localhost:3030";
13
+ }
14
+ return "https://backend.frak.id";
15
+ }),
16
+ }));
17
+
18
+ describe("merchantId", () => {
19
+ beforeEach(() => {
20
+ // Clear cache before each test (also clears sessionStorage)
21
+ clearMerchantIdCache();
22
+ window.sessionStorage.clear();
23
+ // Clear all mocks
24
+ vi.clearAllMocks();
25
+ // Mock console methods to avoid noise in test output
26
+ vi.spyOn(console, "warn").mockImplementation(() => {});
27
+ });
28
+
29
+ afterEach(() => {
30
+ vi.restoreAllMocks();
31
+ });
32
+
33
+ describe("fetchMerchantId", () => {
34
+ it("should fetch merchantId from backend when not cached", async () => {
35
+ const mockResponse = {
36
+ merchantId: "merchant-123",
37
+ name: "Test Merchant",
38
+ domain: "shop.example.com",
39
+ };
40
+
41
+ global.fetch = vi.fn().mockResolvedValueOnce({
42
+ ok: true,
43
+ json: async () => mockResponse,
44
+ });
45
+
46
+ const result = await fetchMerchantId("shop.example.com");
47
+
48
+ expect(result).toBe("merchant-123");
49
+ expect(global.fetch).toHaveBeenCalledWith(
50
+ "https://backend.frak.id/user/merchant/resolve?domain=shop.example.com"
51
+ );
52
+ });
53
+
54
+ it("should return cached merchantId on subsequent calls", async () => {
55
+ const mockResponse = {
56
+ merchantId: "merchant-456",
57
+ name: "Test Merchant",
58
+ domain: "shop.example.com",
59
+ };
60
+
61
+ global.fetch = vi.fn().mockResolvedValueOnce({
62
+ ok: true,
63
+ json: async () => mockResponse,
64
+ });
65
+
66
+ // First call
67
+ const result1 = await fetchMerchantId("shop.example.com");
68
+ expect(result1).toBe("merchant-456");
69
+
70
+ // Second call should use cache
71
+ const result2 = await fetchMerchantId("shop.example.com");
72
+ expect(result2).toBe("merchant-456");
73
+
74
+ // Fetch should only be called once
75
+ expect(global.fetch).toHaveBeenCalledTimes(1);
76
+ });
77
+
78
+ it("should deduplicate concurrent requests", async () => {
79
+ const mockResponse = {
80
+ merchantId: "merchant-789",
81
+ name: "Test Merchant",
82
+ domain: "shop.example.com",
83
+ };
84
+
85
+ global.fetch = vi.fn().mockResolvedValueOnce({
86
+ ok: true,
87
+ json: async () => mockResponse,
88
+ });
89
+
90
+ // Start multiple concurrent requests
91
+ const [result1, result2, result3] = await Promise.all([
92
+ fetchMerchantId("shop.example.com"),
93
+ fetchMerchantId("shop.example.com"),
94
+ fetchMerchantId("shop.example.com"),
95
+ ]);
96
+
97
+ expect(result1).toBe("merchant-789");
98
+ expect(result2).toBe("merchant-789");
99
+ expect(result3).toBe("merchant-789");
100
+
101
+ // Fetch should only be called once despite concurrent requests
102
+ expect(global.fetch).toHaveBeenCalledTimes(1);
103
+ });
104
+
105
+ it("should use window.location.hostname as fallback when domain not provided", async () => {
106
+ const mockResponse = {
107
+ merchantId: "merchant-default",
108
+ name: "Test Merchant",
109
+ domain: "example.com",
110
+ };
111
+
112
+ global.fetch = vi.fn().mockResolvedValueOnce({
113
+ ok: true,
114
+ json: async () => mockResponse,
115
+ });
116
+
117
+ // Mock window.location.hostname
118
+ Object.defineProperty(window, "location", {
119
+ value: {
120
+ hostname: "example.com",
121
+ },
122
+ writable: true,
123
+ });
124
+
125
+ const result = await fetchMerchantId();
126
+
127
+ expect(result).toBe("merchant-default");
128
+ expect(global.fetch).toHaveBeenCalledWith(
129
+ "https://backend.frak.id/user/merchant/resolve?domain=example.com"
130
+ );
131
+ });
132
+
133
+ it("should return undefined when domain is empty and no hostname available", async () => {
134
+ global.fetch = vi.fn();
135
+
136
+ const result = await fetchMerchantId("");
137
+
138
+ expect(result).toBeUndefined();
139
+ expect(global.fetch).not.toHaveBeenCalled();
140
+ });
141
+
142
+ it("should handle fetch errors gracefully", async () => {
143
+ global.fetch = vi
144
+ .fn()
145
+ .mockRejectedValueOnce(new Error("Network error"));
146
+
147
+ const result = await fetchMerchantId("shop.example.com");
148
+
149
+ expect(result).toBeUndefined();
150
+ expect(console.warn).toHaveBeenCalledWith(
151
+ "[Frak SDK] Failed to fetch merchantId:",
152
+ expect.any(Error)
153
+ );
154
+ });
155
+
156
+ it("should handle non-ok response status", async () => {
157
+ global.fetch = vi.fn().mockResolvedValueOnce({
158
+ ok: false,
159
+ status: 404,
160
+ });
161
+
162
+ const result = await fetchMerchantId("nonexistent.com");
163
+
164
+ expect(result).toBeUndefined();
165
+ expect(console.warn).toHaveBeenCalledWith(
166
+ "[Frak SDK] Merchant lookup failed for domain nonexistent.com: 404"
167
+ );
168
+ });
169
+
170
+ it("should handle 500 server error", async () => {
171
+ global.fetch = vi.fn().mockResolvedValueOnce({
172
+ ok: false,
173
+ status: 500,
174
+ });
175
+
176
+ const result = await fetchMerchantId("shop.example.com");
177
+
178
+ expect(result).toBeUndefined();
179
+ expect(console.warn).toHaveBeenCalledWith(
180
+ "[Frak SDK] Merchant lookup failed for domain shop.example.com: 500"
181
+ );
182
+ });
183
+
184
+ it("should handle invalid JSON response", async () => {
185
+ global.fetch = vi.fn().mockResolvedValueOnce({
186
+ ok: true,
187
+ json: async () => {
188
+ throw new Error("Invalid JSON");
189
+ },
190
+ });
191
+
192
+ const result = await fetchMerchantId("shop.example.com");
193
+
194
+ expect(result).toBeUndefined();
195
+ expect(console.warn).toHaveBeenCalledWith(
196
+ "[Frak SDK] Failed to fetch merchantId:",
197
+ expect.any(Error)
198
+ );
199
+ });
200
+
201
+ it("should use custom walletUrl to derive backend URL", async () => {
202
+ const mockResponse = {
203
+ merchantId: "merchant-local",
204
+ name: "Test Merchant",
205
+ domain: "shop.example.com",
206
+ };
207
+
208
+ global.fetch = vi.fn().mockResolvedValueOnce({
209
+ ok: true,
210
+ json: async () => mockResponse,
211
+ });
212
+
213
+ const result = await fetchMerchantId(
214
+ "shop.example.com",
215
+ "http://localhost:3000"
216
+ );
217
+
218
+ expect(result).toBe("merchant-local");
219
+ expect(global.fetch).toHaveBeenCalledWith(
220
+ "http://localhost:3030/user/merchant/resolve?domain=shop.example.com"
221
+ );
222
+ });
223
+
224
+ it("should encode domain in URL query parameter", async () => {
225
+ const mockResponse = {
226
+ merchantId: "merchant-encoded",
227
+ name: "Test Merchant",
228
+ domain: "shop.example.com",
229
+ };
230
+
231
+ global.fetch = vi.fn().mockResolvedValueOnce({
232
+ ok: true,
233
+ json: async () => mockResponse,
234
+ });
235
+
236
+ const domainWithSpecialChars = "shop.example.com?test=1&foo=bar";
237
+ await fetchMerchantId(domainWithSpecialChars);
238
+
239
+ expect(global.fetch).toHaveBeenCalledWith(
240
+ "https://backend.frak.id/user/merchant/resolve?domain=shop.example.com%3Ftest%3D1%26foo%3Dbar"
241
+ );
242
+ });
243
+
244
+ it("should cache merchantId from response", async () => {
245
+ const mockResponse = {
246
+ merchantId: "merchant-cached",
247
+ name: "Test Merchant",
248
+ domain: "shop.example.com",
249
+ };
250
+
251
+ global.fetch = vi.fn().mockResolvedValueOnce({
252
+ ok: true,
253
+ json: async () => mockResponse,
254
+ });
255
+
256
+ const result1 = await fetchMerchantId("shop.example.com");
257
+ expect(result1).toBe("merchant-cached");
258
+
259
+ // Clear fetch mock and call again
260
+ global.fetch = vi.fn();
261
+ const result2 = await fetchMerchantId("shop.example.com");
262
+
263
+ // Should return cached value without calling fetch
264
+ expect(result2).toBe("merchant-cached");
265
+ expect(global.fetch).not.toHaveBeenCalled();
266
+ });
267
+
268
+ it("should persist merchantId to sessionStorage after fetch", async () => {
269
+ const mockResponse = {
270
+ merchantId: "merchant-persisted",
271
+ name: "Test Merchant",
272
+ domain: "shop.example.com",
273
+ };
274
+
275
+ global.fetch = vi.fn().mockResolvedValueOnce({
276
+ ok: true,
277
+ json: async () => mockResponse,
278
+ });
279
+
280
+ await fetchMerchantId("shop.example.com");
281
+
282
+ expect(window.sessionStorage.getItem("frak-merchant-id")).toBe(
283
+ "merchant-persisted"
284
+ );
285
+ });
286
+
287
+ it("should restore merchantId from sessionStorage when in-memory cache is cleared", async () => {
288
+ const mockResponse = {
289
+ merchantId: "merchant-storage",
290
+ name: "Test Merchant",
291
+ domain: "shop.example.com",
292
+ };
293
+
294
+ global.fetch = vi.fn().mockResolvedValueOnce({
295
+ ok: true,
296
+ json: async () => mockResponse,
297
+ });
298
+
299
+ // First call populates both caches
300
+ const result1 = await fetchMerchantId("shop.example.com");
301
+ expect(result1).toBe("merchant-storage");
302
+ expect(global.fetch).toHaveBeenCalledTimes(1);
303
+
304
+ // Clear only in-memory cache (not sessionStorage)
305
+ vi.clearAllMocks();
306
+ global.fetch = vi.fn();
307
+
308
+ // Manually clear in-memory but keep sessionStorage
309
+ // We do this by calling the internal clear then restoring sessionStorage
310
+ const stored = window.sessionStorage.getItem("frak-merchant-id");
311
+ clearMerchantIdCache();
312
+ window.sessionStorage.setItem("frak-merchant-id", stored!);
313
+
314
+ // Second call should restore from sessionStorage without fetch
315
+ const result2 = await fetchMerchantId("shop.example.com");
316
+ expect(result2).toBe("merchant-storage");
317
+ expect(global.fetch).not.toHaveBeenCalled();
318
+ });
319
+
320
+ it("should not write to sessionStorage when fetch fails", async () => {
321
+ global.fetch = vi
322
+ .fn()
323
+ .mockRejectedValueOnce(new Error("Network error"));
324
+
325
+ await fetchMerchantId("shop.example.com");
326
+
327
+ expect(
328
+ window.sessionStorage.getItem("frak-merchant-id")
329
+ ).toBeNull();
330
+ });
331
+ });
332
+
333
+ describe("clearMerchantIdCache", () => {
334
+ it("should clear the cached merchantId", async () => {
335
+ const mockResponse = {
336
+ merchantId: "merchant-clear-test",
337
+ name: "Test Merchant",
338
+ domain: "shop.example.com",
339
+ };
340
+
341
+ global.fetch = vi.fn().mockResolvedValue({
342
+ ok: true,
343
+ json: async () => mockResponse,
344
+ });
345
+
346
+ // First fetch
347
+ const result1 = await fetchMerchantId("shop.example.com");
348
+ expect(result1).toBe("merchant-clear-test");
349
+ expect(global.fetch).toHaveBeenCalledTimes(1);
350
+
351
+ // Clear cache
352
+ clearMerchantIdCache();
353
+
354
+ // Second fetch should call API again
355
+ const result2 = await fetchMerchantId("shop.example.com");
356
+ expect(result2).toBe("merchant-clear-test");
357
+ expect(global.fetch).toHaveBeenCalledTimes(2);
358
+ });
359
+
360
+ it("should allow re-fetching after cache clear", async () => {
361
+ const mockResponse1 = {
362
+ merchantId: "merchant-first",
363
+ name: "Test Merchant",
364
+ domain: "shop1.example.com",
365
+ };
366
+
367
+ const mockResponse2 = {
368
+ merchantId: "merchant-second",
369
+ name: "Test Merchant",
370
+ domain: "shop2.example.com",
371
+ };
372
+
373
+ global.fetch = vi
374
+ .fn()
375
+ .mockResolvedValueOnce({
376
+ ok: true,
377
+ json: async () => mockResponse1,
378
+ })
379
+ .mockResolvedValueOnce({
380
+ ok: true,
381
+ json: async () => mockResponse2,
382
+ });
383
+
384
+ const result1 = await fetchMerchantId("shop1.example.com");
385
+ expect(result1).toBe("merchant-first");
386
+
387
+ clearMerchantIdCache();
388
+
389
+ const result2 = await fetchMerchantId("shop2.example.com");
390
+ expect(result2).toBe("merchant-second");
391
+ });
392
+
393
+ it("should clear sessionStorage when clearing cache", async () => {
394
+ const mockResponse = {
395
+ merchantId: "merchant-session-clear",
396
+ name: "Test Merchant",
397
+ domain: "shop.example.com",
398
+ };
399
+
400
+ global.fetch = vi.fn().mockResolvedValueOnce({
401
+ ok: true,
402
+ json: async () => mockResponse,
403
+ });
404
+
405
+ await fetchMerchantId("shop.example.com");
406
+ expect(window.sessionStorage.getItem("frak-merchant-id")).toBe(
407
+ "merchant-session-clear"
408
+ );
409
+
410
+ clearMerchantIdCache();
411
+
412
+ expect(
413
+ window.sessionStorage.getItem("frak-merchant-id")
414
+ ).toBeNull();
415
+ });
416
+ });
417
+
418
+ describe("resolveMerchantId", () => {
419
+ it("should return merchantId from config if available", async () => {
420
+ global.fetch = vi.fn();
421
+
422
+ const config = {
423
+ metadata: {
424
+ merchantId: "config-merchant-123",
425
+ },
426
+ };
427
+
428
+ const result = await resolveMerchantId(config);
429
+
430
+ expect(result).toBe("config-merchant-123");
431
+ expect(global.fetch).not.toHaveBeenCalled();
432
+ });
433
+
434
+ it("should fetch merchantId from backend if not in config", async () => {
435
+ const mockResponse = {
436
+ merchantId: "fetched-merchant-456",
437
+ name: "Test Merchant",
438
+ domain: "shop.example.com",
439
+ };
440
+
441
+ global.fetch = vi.fn().mockResolvedValueOnce({
442
+ ok: true,
443
+ json: async () => mockResponse,
444
+ });
445
+
446
+ Object.defineProperty(window, "location", {
447
+ value: {
448
+ hostname: "shop.example.com",
449
+ },
450
+ writable: true,
451
+ });
452
+
453
+ const config = {
454
+ metadata: {},
455
+ };
456
+
457
+ const result = await resolveMerchantId(config);
458
+
459
+ expect(result).toBe("fetched-merchant-456");
460
+ expect(global.fetch).toHaveBeenCalled();
461
+ });
462
+
463
+ it("should return undefined when config has no merchantId and fetch fails", async () => {
464
+ global.fetch = vi
465
+ .fn()
466
+ .mockRejectedValueOnce(new Error("Network error"));
467
+
468
+ const config = {
469
+ metadata: {},
470
+ };
471
+
472
+ const result = await resolveMerchantId(config);
473
+
474
+ expect(result).toBeUndefined();
475
+ });
476
+
477
+ it("should prioritize config merchantId over fetched value", async () => {
478
+ global.fetch = vi.fn();
479
+
480
+ const config = {
481
+ metadata: {
482
+ merchantId: "config-priority",
483
+ },
484
+ };
485
+
486
+ const result = await resolveMerchantId(config);
487
+
488
+ expect(result).toBe("config-priority");
489
+ expect(global.fetch).not.toHaveBeenCalled();
490
+ });
491
+
492
+ it("should use walletUrl parameter when fetching", async () => {
493
+ const mockResponse = {
494
+ merchantId: "merchant-with-wallet-url",
495
+ name: "Test Merchant",
496
+ domain: "shop.example.com",
497
+ };
498
+
499
+ global.fetch = vi.fn().mockResolvedValueOnce({
500
+ ok: true,
501
+ json: async () => mockResponse,
502
+ });
503
+
504
+ Object.defineProperty(window, "location", {
505
+ value: {
506
+ hostname: "shop.example.com",
507
+ },
508
+ writable: true,
509
+ });
510
+
511
+ const config = {
512
+ metadata: {},
513
+ };
514
+
515
+ const result = await resolveMerchantId(
516
+ config,
517
+ "http://localhost:3000"
518
+ );
519
+
520
+ expect(result).toBe("merchant-with-wallet-url");
521
+ expect(global.fetch).toHaveBeenCalledWith(
522
+ "http://localhost:3030/user/merchant/resolve?domain=shop.example.com"
523
+ );
524
+ });
525
+
526
+ it("should handle config without metadata property", async () => {
527
+ const mockResponse = {
528
+ merchantId: "merchant-no-metadata",
529
+ name: "Test Merchant",
530
+ domain: "shop.example.com",
531
+ };
532
+
533
+ global.fetch = vi.fn().mockResolvedValueOnce({
534
+ ok: true,
535
+ json: async () => mockResponse,
536
+ });
537
+
538
+ Object.defineProperty(window, "location", {
539
+ value: {
540
+ hostname: "shop.example.com",
541
+ },
542
+ writable: true,
543
+ });
544
+
545
+ const config = {};
546
+
547
+ const result = await resolveMerchantId(config);
548
+
549
+ expect(result).toBe("merchant-no-metadata");
550
+ });
551
+
552
+ it("should cache result from fetch in resolveMerchantId", async () => {
553
+ const mockResponse = {
554
+ merchantId: "merchant-cached-resolve",
555
+ name: "Test Merchant",
556
+ domain: "shop.example.com",
557
+ };
558
+
559
+ global.fetch = vi.fn().mockResolvedValueOnce({
560
+ ok: true,
561
+ json: async () => mockResponse,
562
+ });
563
+
564
+ Object.defineProperty(window, "location", {
565
+ value: {
566
+ hostname: "shop.example.com",
567
+ },
568
+ writable: true,
569
+ });
570
+
571
+ const config = {
572
+ metadata: {},
573
+ };
574
+
575
+ // First call
576
+ const result1 = await resolveMerchantId(config);
577
+ expect(result1).toBe("merchant-cached-resolve");
578
+
579
+ // Second call should use cache
580
+ global.fetch = vi.fn();
581
+ const result2 = await resolveMerchantId(config);
582
+ expect(result2).toBe("merchant-cached-resolve");
583
+ expect(global.fetch).not.toHaveBeenCalled();
584
+ });
585
+ });
586
+
587
+ describe("integration scenarios", () => {
588
+ it("should handle multiple different domains", async () => {
589
+ const mockResponse1 = {
590
+ merchantId: "merchant-domain1",
591
+ name: "Merchant 1",
592
+ domain: "shop1.example.com",
593
+ };
594
+
595
+ const mockResponse2 = {
596
+ merchantId: "merchant-domain2",
597
+ name: "Merchant 2",
598
+ domain: "shop2.example.com",
599
+ };
600
+
601
+ global.fetch = vi
602
+ .fn()
603
+ .mockResolvedValueOnce({
604
+ ok: true,
605
+ json: async () => mockResponse1,
606
+ })
607
+ .mockResolvedValueOnce({
608
+ ok: true,
609
+ json: async () => mockResponse2,
610
+ });
611
+
612
+ const result1 = await fetchMerchantId("shop1.example.com");
613
+ expect(result1).toBe("merchant-domain1");
614
+
615
+ clearMerchantIdCache();
616
+
617
+ const result2 = await fetchMerchantId("shop2.example.com");
618
+ expect(result2).toBe("merchant-domain2");
619
+
620
+ expect(global.fetch).toHaveBeenCalledTimes(2);
621
+ });
622
+
623
+ it("should handle rapid successive calls with cache", async () => {
624
+ const mockResponse = {
625
+ merchantId: "merchant-rapid",
626
+ name: "Test Merchant",
627
+ domain: "shop.example.com",
628
+ };
629
+
630
+ global.fetch = vi.fn().mockResolvedValueOnce({
631
+ ok: true,
632
+ json: async () => mockResponse,
633
+ });
634
+
635
+ const results = await Promise.all([
636
+ fetchMerchantId("shop.example.com"),
637
+ fetchMerchantId("shop.example.com"),
638
+ fetchMerchantId("shop.example.com"),
639
+ fetchMerchantId("shop.example.com"),
640
+ fetchMerchantId("shop.example.com"),
641
+ ]);
642
+
643
+ expect(results).toEqual([
644
+ "merchant-rapid",
645
+ "merchant-rapid",
646
+ "merchant-rapid",
647
+ "merchant-rapid",
648
+ "merchant-rapid",
649
+ ]);
650
+ expect(global.fetch).toHaveBeenCalledTimes(1);
651
+ });
652
+ });
653
+ });