@frak-labs/core-sdk 0.1.0 → 0.1.1-beta.4dfea079

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 (130) hide show
  1. package/README.md +58 -0
  2. package/cdn/bundle.js +3 -8
  3. package/dist/actions.cjs +1 -1
  4. package/dist/actions.d.cts +3 -1400
  5. package/dist/actions.d.ts +3 -1400
  6. package/dist/actions.js +1 -1
  7. package/dist/bundle.cjs +1 -13
  8. package/dist/bundle.d.cts +4 -1927
  9. package/dist/bundle.d.ts +4 -1927
  10. package/dist/bundle.js +1 -13
  11. package/dist/computeLegacyProductId-CscYhyUi.d.cts +525 -0
  12. package/dist/computeLegacyProductId-WbD1gXV9.d.ts +525 -0
  13. package/dist/index.cjs +1 -13
  14. package/dist/index.d.cts +3 -1269
  15. package/dist/index.d.ts +3 -1269
  16. package/dist/index.js +1 -13
  17. package/dist/openSso-CC1-loUk.d.cts +1019 -0
  18. package/dist/openSso-tkqaDQLV.d.ts +1019 -0
  19. package/dist/setupClient-BjIbK6XJ.cjs +13 -0
  20. package/dist/setupClient-D_HId3e2.js +13 -0
  21. package/dist/siweAuthenticate-B_Z2OZmj.cjs +1 -0
  22. package/dist/siweAuthenticate-CQ4OfPuA.js +1 -0
  23. package/dist/siweAuthenticate-CR4Dpji6.d.cts +467 -0
  24. package/dist/siweAuthenticate-udoruuy9.d.ts +467 -0
  25. package/dist/trackEvent-CGIryq5h.cjs +1 -0
  26. package/dist/trackEvent-YfUh4jrx.js +1 -0
  27. package/package.json +24 -30
  28. package/src/actions/displayEmbeddedWallet.test.ts +194 -0
  29. package/src/actions/displayEmbeddedWallet.ts +20 -0
  30. package/src/actions/displayModal.test.ts +388 -0
  31. package/src/actions/displayModal.ts +120 -0
  32. package/src/actions/getMerchantInformation.test.ts +116 -0
  33. package/src/actions/getMerchantInformation.ts +9 -0
  34. package/src/actions/index.ts +29 -0
  35. package/src/actions/openSso.ts +116 -0
  36. package/src/actions/prepareSso.test.ts +223 -0
  37. package/src/actions/prepareSso.ts +48 -0
  38. package/src/actions/referral/processReferral.test.ts +248 -0
  39. package/src/actions/referral/processReferral.ts +232 -0
  40. package/src/actions/referral/referralInteraction.test.ts +147 -0
  41. package/src/actions/referral/referralInteraction.ts +52 -0
  42. package/src/actions/sendInteraction.ts +24 -0
  43. package/src/actions/trackPurchaseStatus.test.ts +287 -0
  44. package/src/actions/trackPurchaseStatus.ts +56 -0
  45. package/src/actions/watchWalletStatus.test.ts +372 -0
  46. package/src/actions/watchWalletStatus.ts +93 -0
  47. package/src/actions/wrapper/modalBuilder.test.ts +239 -0
  48. package/src/actions/wrapper/modalBuilder.ts +203 -0
  49. package/src/actions/wrapper/sendTransaction.test.ts +164 -0
  50. package/src/actions/wrapper/sendTransaction.ts +62 -0
  51. package/src/actions/wrapper/siweAuthenticate.test.ts +290 -0
  52. package/src/actions/wrapper/siweAuthenticate.ts +94 -0
  53. package/src/bundle.ts +2 -0
  54. package/src/clients/DebugInfo.test.ts +418 -0
  55. package/src/clients/DebugInfo.ts +182 -0
  56. package/src/clients/createIFrameFrakClient.ts +289 -0
  57. package/src/clients/index.ts +3 -0
  58. package/src/clients/setupClient.test.ts +343 -0
  59. package/src/clients/setupClient.ts +73 -0
  60. package/src/clients/transports/iframeLifecycleManager.test.ts +558 -0
  61. package/src/clients/transports/iframeLifecycleManager.ts +174 -0
  62. package/src/constants/interactionTypes.ts +15 -0
  63. package/src/constants/locales.ts +14 -0
  64. package/src/index.ts +110 -0
  65. package/src/types/client.ts +14 -0
  66. package/src/types/compression.ts +22 -0
  67. package/src/types/config.ts +117 -0
  68. package/src/types/context.ts +13 -0
  69. package/src/types/index.ts +75 -0
  70. package/src/types/lifecycle/client.ts +69 -0
  71. package/src/types/lifecycle/iframe.ts +41 -0
  72. package/src/types/lifecycle/index.ts +2 -0
  73. package/src/types/rpc/displayModal.ts +82 -0
  74. package/src/types/rpc/embedded/index.ts +68 -0
  75. package/src/types/rpc/embedded/loggedIn.ts +55 -0
  76. package/src/types/rpc/embedded/loggedOut.ts +28 -0
  77. package/src/types/rpc/interaction.ts +30 -0
  78. package/src/types/rpc/merchantInformation.ts +77 -0
  79. package/src/types/rpc/modal/final.ts +46 -0
  80. package/src/types/rpc/modal/generic.ts +46 -0
  81. package/src/types/rpc/modal/index.ts +16 -0
  82. package/src/types/rpc/modal/login.ts +36 -0
  83. package/src/types/rpc/modal/siweAuthenticate.ts +37 -0
  84. package/src/types/rpc/modal/transaction.ts +33 -0
  85. package/src/types/rpc/sso.ts +80 -0
  86. package/src/types/rpc/walletStatus.ts +29 -0
  87. package/src/types/rpc.ts +146 -0
  88. package/src/types/tracking.ts +60 -0
  89. package/src/types/transport.ts +34 -0
  90. package/src/utils/FrakContext.test.ts +407 -0
  91. package/src/utils/FrakContext.ts +158 -0
  92. package/src/utils/backendUrl.test.ts +83 -0
  93. package/src/utils/backendUrl.ts +62 -0
  94. package/src/utils/clientId.test.ts +41 -0
  95. package/src/utils/clientId.ts +40 -0
  96. package/src/utils/compression/b64.test.ts +181 -0
  97. package/src/utils/compression/b64.ts +29 -0
  98. package/src/utils/compression/compress.test.ts +123 -0
  99. package/src/utils/compression/compress.ts +11 -0
  100. package/src/utils/compression/decompress.test.ts +149 -0
  101. package/src/utils/compression/decompress.ts +11 -0
  102. package/src/utils/compression/index.ts +3 -0
  103. package/src/utils/computeLegacyProductId.ts +11 -0
  104. package/src/utils/constants.test.ts +23 -0
  105. package/src/utils/constants.ts +14 -0
  106. package/src/utils/deepLinkWithFallback.test.ts +243 -0
  107. package/src/utils/deepLinkWithFallback.ts +97 -0
  108. package/src/utils/formatAmount.test.ts +113 -0
  109. package/src/utils/formatAmount.ts +18 -0
  110. package/src/utils/getCurrencyAmountKey.test.ts +44 -0
  111. package/src/utils/getCurrencyAmountKey.ts +15 -0
  112. package/src/utils/getSupportedCurrency.test.ts +51 -0
  113. package/src/utils/getSupportedCurrency.ts +14 -0
  114. package/src/utils/getSupportedLocale.test.ts +64 -0
  115. package/src/utils/getSupportedLocale.ts +16 -0
  116. package/src/utils/iframeHelper.test.ts +450 -0
  117. package/src/utils/iframeHelper.ts +147 -0
  118. package/src/utils/index.ts +36 -0
  119. package/src/utils/merchantId.test.ts +564 -0
  120. package/src/utils/merchantId.ts +122 -0
  121. package/src/utils/sso.ts +126 -0
  122. package/src/utils/ssoUrlListener.test.ts +252 -0
  123. package/src/utils/ssoUrlListener.ts +60 -0
  124. package/src/utils/trackEvent.test.ts +180 -0
  125. package/src/utils/trackEvent.ts +31 -0
  126. package/cdn/bundle.js.LICENSE.txt +0 -10
  127. package/dist/interactions.cjs +0 -1
  128. package/dist/interactions.d.cts +0 -182
  129. package/dist/interactions.d.ts +0 -182
  130. package/dist/interactions.js +0 -1
@@ -0,0 +1,564 @@
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
21
+ clearMerchantIdCache();
22
+ // Clear all mocks
23
+ vi.clearAllMocks();
24
+ // Mock console methods to avoid noise in test output
25
+ vi.spyOn(console, "warn").mockImplementation(() => {});
26
+ });
27
+
28
+ afterEach(() => {
29
+ vi.restoreAllMocks();
30
+ });
31
+
32
+ describe("fetchMerchantId", () => {
33
+ it("should fetch merchantId from backend when not cached", async () => {
34
+ const mockResponse = {
35
+ merchantId: "merchant-123",
36
+ name: "Test Merchant",
37
+ domain: "shop.example.com",
38
+ };
39
+
40
+ global.fetch = vi.fn().mockResolvedValueOnce({
41
+ ok: true,
42
+ json: async () => mockResponse,
43
+ });
44
+
45
+ const result = await fetchMerchantId("shop.example.com");
46
+
47
+ expect(result).toBe("merchant-123");
48
+ expect(global.fetch).toHaveBeenCalledWith(
49
+ "https://backend.frak.id/user/merchant/resolve?domain=shop.example.com"
50
+ );
51
+ });
52
+
53
+ it("should return cached merchantId on subsequent calls", async () => {
54
+ const mockResponse = {
55
+ merchantId: "merchant-456",
56
+ name: "Test Merchant",
57
+ domain: "shop.example.com",
58
+ };
59
+
60
+ global.fetch = vi.fn().mockResolvedValueOnce({
61
+ ok: true,
62
+ json: async () => mockResponse,
63
+ });
64
+
65
+ // First call
66
+ const result1 = await fetchMerchantId("shop.example.com");
67
+ expect(result1).toBe("merchant-456");
68
+
69
+ // Second call should use cache
70
+ const result2 = await fetchMerchantId("shop.example.com");
71
+ expect(result2).toBe("merchant-456");
72
+
73
+ // Fetch should only be called once
74
+ expect(global.fetch).toHaveBeenCalledTimes(1);
75
+ });
76
+
77
+ it("should deduplicate concurrent requests", async () => {
78
+ const mockResponse = {
79
+ merchantId: "merchant-789",
80
+ name: "Test Merchant",
81
+ domain: "shop.example.com",
82
+ };
83
+
84
+ global.fetch = vi.fn().mockResolvedValueOnce({
85
+ ok: true,
86
+ json: async () => mockResponse,
87
+ });
88
+
89
+ // Start multiple concurrent requests
90
+ const [result1, result2, result3] = await Promise.all([
91
+ fetchMerchantId("shop.example.com"),
92
+ fetchMerchantId("shop.example.com"),
93
+ fetchMerchantId("shop.example.com"),
94
+ ]);
95
+
96
+ expect(result1).toBe("merchant-789");
97
+ expect(result2).toBe("merchant-789");
98
+ expect(result3).toBe("merchant-789");
99
+
100
+ // Fetch should only be called once despite concurrent requests
101
+ expect(global.fetch).toHaveBeenCalledTimes(1);
102
+ });
103
+
104
+ it("should use window.location.hostname as fallback when domain not provided", async () => {
105
+ const mockResponse = {
106
+ merchantId: "merchant-default",
107
+ name: "Test Merchant",
108
+ domain: "example.com",
109
+ };
110
+
111
+ global.fetch = vi.fn().mockResolvedValueOnce({
112
+ ok: true,
113
+ json: async () => mockResponse,
114
+ });
115
+
116
+ // Mock window.location.hostname
117
+ Object.defineProperty(window, "location", {
118
+ value: {
119
+ hostname: "example.com",
120
+ },
121
+ writable: true,
122
+ });
123
+
124
+ const result = await fetchMerchantId();
125
+
126
+ expect(result).toBe("merchant-default");
127
+ expect(global.fetch).toHaveBeenCalledWith(
128
+ "https://backend.frak.id/user/merchant/resolve?domain=example.com"
129
+ );
130
+ });
131
+
132
+ it("should return undefined when domain is empty and no hostname available", async () => {
133
+ global.fetch = vi.fn();
134
+
135
+ const result = await fetchMerchantId("");
136
+
137
+ expect(result).toBeUndefined();
138
+ expect(global.fetch).not.toHaveBeenCalled();
139
+ });
140
+
141
+ it("should handle fetch errors gracefully", async () => {
142
+ global.fetch = vi
143
+ .fn()
144
+ .mockRejectedValueOnce(new Error("Network error"));
145
+
146
+ const result = await fetchMerchantId("shop.example.com");
147
+
148
+ expect(result).toBeUndefined();
149
+ expect(console.warn).toHaveBeenCalledWith(
150
+ "[Frak SDK] Failed to fetch merchantId:",
151
+ expect.any(Error)
152
+ );
153
+ });
154
+
155
+ it("should handle non-ok response status", async () => {
156
+ global.fetch = vi.fn().mockResolvedValueOnce({
157
+ ok: false,
158
+ status: 404,
159
+ });
160
+
161
+ const result = await fetchMerchantId("nonexistent.com");
162
+
163
+ expect(result).toBeUndefined();
164
+ expect(console.warn).toHaveBeenCalledWith(
165
+ "[Frak SDK] Merchant lookup failed for domain nonexistent.com: 404"
166
+ );
167
+ });
168
+
169
+ it("should handle 500 server error", async () => {
170
+ global.fetch = vi.fn().mockResolvedValueOnce({
171
+ ok: false,
172
+ status: 500,
173
+ });
174
+
175
+ const result = await fetchMerchantId("shop.example.com");
176
+
177
+ expect(result).toBeUndefined();
178
+ expect(console.warn).toHaveBeenCalledWith(
179
+ "[Frak SDK] Merchant lookup failed for domain shop.example.com: 500"
180
+ );
181
+ });
182
+
183
+ it("should handle invalid JSON response", async () => {
184
+ global.fetch = vi.fn().mockResolvedValueOnce({
185
+ ok: true,
186
+ json: async () => {
187
+ throw new Error("Invalid JSON");
188
+ },
189
+ });
190
+
191
+ const result = await fetchMerchantId("shop.example.com");
192
+
193
+ expect(result).toBeUndefined();
194
+ expect(console.warn).toHaveBeenCalledWith(
195
+ "[Frak SDK] Failed to fetch merchantId:",
196
+ expect.any(Error)
197
+ );
198
+ });
199
+
200
+ it("should use custom walletUrl to derive backend URL", async () => {
201
+ const mockResponse = {
202
+ merchantId: "merchant-local",
203
+ name: "Test Merchant",
204
+ domain: "shop.example.com",
205
+ };
206
+
207
+ global.fetch = vi.fn().mockResolvedValueOnce({
208
+ ok: true,
209
+ json: async () => mockResponse,
210
+ });
211
+
212
+ const result = await fetchMerchantId(
213
+ "shop.example.com",
214
+ "http://localhost:3000"
215
+ );
216
+
217
+ expect(result).toBe("merchant-local");
218
+ expect(global.fetch).toHaveBeenCalledWith(
219
+ "http://localhost:3030/user/merchant/resolve?domain=shop.example.com"
220
+ );
221
+ });
222
+
223
+ it("should encode domain in URL query parameter", async () => {
224
+ const mockResponse = {
225
+ merchantId: "merchant-encoded",
226
+ name: "Test Merchant",
227
+ domain: "shop.example.com",
228
+ };
229
+
230
+ global.fetch = vi.fn().mockResolvedValueOnce({
231
+ ok: true,
232
+ json: async () => mockResponse,
233
+ });
234
+
235
+ const domainWithSpecialChars = "shop.example.com?test=1&foo=bar";
236
+ await fetchMerchantId(domainWithSpecialChars);
237
+
238
+ expect(global.fetch).toHaveBeenCalledWith(
239
+ "https://backend.frak.id/user/merchant/resolve?domain=shop.example.com%3Ftest%3D1%26foo%3Dbar"
240
+ );
241
+ });
242
+
243
+ it("should cache merchantId from response", async () => {
244
+ const mockResponse = {
245
+ merchantId: "merchant-cached",
246
+ name: "Test Merchant",
247
+ domain: "shop.example.com",
248
+ };
249
+
250
+ global.fetch = vi.fn().mockResolvedValueOnce({
251
+ ok: true,
252
+ json: async () => mockResponse,
253
+ });
254
+
255
+ const result1 = await fetchMerchantId("shop.example.com");
256
+ expect(result1).toBe("merchant-cached");
257
+
258
+ // Clear fetch mock and call again
259
+ global.fetch = vi.fn();
260
+ const result2 = await fetchMerchantId("shop.example.com");
261
+
262
+ // Should return cached value without calling fetch
263
+ expect(result2).toBe("merchant-cached");
264
+ expect(global.fetch).not.toHaveBeenCalled();
265
+ });
266
+ });
267
+
268
+ describe("clearMerchantIdCache", () => {
269
+ it("should clear the cached merchantId", async () => {
270
+ const mockResponse = {
271
+ merchantId: "merchant-clear-test",
272
+ name: "Test Merchant",
273
+ domain: "shop.example.com",
274
+ };
275
+
276
+ global.fetch = vi.fn().mockResolvedValue({
277
+ ok: true,
278
+ json: async () => mockResponse,
279
+ });
280
+
281
+ // First fetch
282
+ const result1 = await fetchMerchantId("shop.example.com");
283
+ expect(result1).toBe("merchant-clear-test");
284
+ expect(global.fetch).toHaveBeenCalledTimes(1);
285
+
286
+ // Clear cache
287
+ clearMerchantIdCache();
288
+
289
+ // Second fetch should call API again
290
+ const result2 = await fetchMerchantId("shop.example.com");
291
+ expect(result2).toBe("merchant-clear-test");
292
+ expect(global.fetch).toHaveBeenCalledTimes(2);
293
+ });
294
+
295
+ it("should allow re-fetching after cache clear", async () => {
296
+ const mockResponse1 = {
297
+ merchantId: "merchant-first",
298
+ name: "Test Merchant",
299
+ domain: "shop1.example.com",
300
+ };
301
+
302
+ const mockResponse2 = {
303
+ merchantId: "merchant-second",
304
+ name: "Test Merchant",
305
+ domain: "shop2.example.com",
306
+ };
307
+
308
+ global.fetch = vi
309
+ .fn()
310
+ .mockResolvedValueOnce({
311
+ ok: true,
312
+ json: async () => mockResponse1,
313
+ })
314
+ .mockResolvedValueOnce({
315
+ ok: true,
316
+ json: async () => mockResponse2,
317
+ });
318
+
319
+ const result1 = await fetchMerchantId("shop1.example.com");
320
+ expect(result1).toBe("merchant-first");
321
+
322
+ clearMerchantIdCache();
323
+
324
+ const result2 = await fetchMerchantId("shop2.example.com");
325
+ expect(result2).toBe("merchant-second");
326
+ });
327
+ });
328
+
329
+ describe("resolveMerchantId", () => {
330
+ it("should return merchantId from config if available", async () => {
331
+ global.fetch = vi.fn();
332
+
333
+ const config = {
334
+ metadata: {
335
+ merchantId: "config-merchant-123",
336
+ },
337
+ };
338
+
339
+ const result = await resolveMerchantId(config);
340
+
341
+ expect(result).toBe("config-merchant-123");
342
+ expect(global.fetch).not.toHaveBeenCalled();
343
+ });
344
+
345
+ it("should fetch merchantId from backend if not in config", async () => {
346
+ const mockResponse = {
347
+ merchantId: "fetched-merchant-456",
348
+ name: "Test Merchant",
349
+ domain: "shop.example.com",
350
+ };
351
+
352
+ global.fetch = vi.fn().mockResolvedValueOnce({
353
+ ok: true,
354
+ json: async () => mockResponse,
355
+ });
356
+
357
+ Object.defineProperty(window, "location", {
358
+ value: {
359
+ hostname: "shop.example.com",
360
+ },
361
+ writable: true,
362
+ });
363
+
364
+ const config = {
365
+ metadata: {},
366
+ };
367
+
368
+ const result = await resolveMerchantId(config);
369
+
370
+ expect(result).toBe("fetched-merchant-456");
371
+ expect(global.fetch).toHaveBeenCalled();
372
+ });
373
+
374
+ it("should return undefined when config has no merchantId and fetch fails", async () => {
375
+ global.fetch = vi
376
+ .fn()
377
+ .mockRejectedValueOnce(new Error("Network error"));
378
+
379
+ const config = {
380
+ metadata: {},
381
+ };
382
+
383
+ const result = await resolveMerchantId(config);
384
+
385
+ expect(result).toBeUndefined();
386
+ });
387
+
388
+ it("should prioritize config merchantId over fetched value", async () => {
389
+ global.fetch = vi.fn();
390
+
391
+ const config = {
392
+ metadata: {
393
+ merchantId: "config-priority",
394
+ },
395
+ };
396
+
397
+ const result = await resolveMerchantId(config);
398
+
399
+ expect(result).toBe("config-priority");
400
+ expect(global.fetch).not.toHaveBeenCalled();
401
+ });
402
+
403
+ it("should use walletUrl parameter when fetching", async () => {
404
+ const mockResponse = {
405
+ merchantId: "merchant-with-wallet-url",
406
+ name: "Test Merchant",
407
+ domain: "shop.example.com",
408
+ };
409
+
410
+ global.fetch = vi.fn().mockResolvedValueOnce({
411
+ ok: true,
412
+ json: async () => mockResponse,
413
+ });
414
+
415
+ Object.defineProperty(window, "location", {
416
+ value: {
417
+ hostname: "shop.example.com",
418
+ },
419
+ writable: true,
420
+ });
421
+
422
+ const config = {
423
+ metadata: {},
424
+ };
425
+
426
+ const result = await resolveMerchantId(
427
+ config,
428
+ "http://localhost:3000"
429
+ );
430
+
431
+ expect(result).toBe("merchant-with-wallet-url");
432
+ expect(global.fetch).toHaveBeenCalledWith(
433
+ "http://localhost:3030/user/merchant/resolve?domain=shop.example.com"
434
+ );
435
+ });
436
+
437
+ it("should handle config without metadata property", async () => {
438
+ const mockResponse = {
439
+ merchantId: "merchant-no-metadata",
440
+ name: "Test Merchant",
441
+ domain: "shop.example.com",
442
+ };
443
+
444
+ global.fetch = vi.fn().mockResolvedValueOnce({
445
+ ok: true,
446
+ json: async () => mockResponse,
447
+ });
448
+
449
+ Object.defineProperty(window, "location", {
450
+ value: {
451
+ hostname: "shop.example.com",
452
+ },
453
+ writable: true,
454
+ });
455
+
456
+ const config = {};
457
+
458
+ const result = await resolveMerchantId(config);
459
+
460
+ expect(result).toBe("merchant-no-metadata");
461
+ });
462
+
463
+ it("should cache result from fetch in resolveMerchantId", async () => {
464
+ const mockResponse = {
465
+ merchantId: "merchant-cached-resolve",
466
+ name: "Test Merchant",
467
+ domain: "shop.example.com",
468
+ };
469
+
470
+ global.fetch = vi.fn().mockResolvedValueOnce({
471
+ ok: true,
472
+ json: async () => mockResponse,
473
+ });
474
+
475
+ Object.defineProperty(window, "location", {
476
+ value: {
477
+ hostname: "shop.example.com",
478
+ },
479
+ writable: true,
480
+ });
481
+
482
+ const config = {
483
+ metadata: {},
484
+ };
485
+
486
+ // First call
487
+ const result1 = await resolveMerchantId(config);
488
+ expect(result1).toBe("merchant-cached-resolve");
489
+
490
+ // Second call should use cache
491
+ global.fetch = vi.fn();
492
+ const result2 = await resolveMerchantId(config);
493
+ expect(result2).toBe("merchant-cached-resolve");
494
+ expect(global.fetch).not.toHaveBeenCalled();
495
+ });
496
+ });
497
+
498
+ describe("integration scenarios", () => {
499
+ it("should handle multiple different domains", async () => {
500
+ const mockResponse1 = {
501
+ merchantId: "merchant-domain1",
502
+ name: "Merchant 1",
503
+ domain: "shop1.example.com",
504
+ };
505
+
506
+ const mockResponse2 = {
507
+ merchantId: "merchant-domain2",
508
+ name: "Merchant 2",
509
+ domain: "shop2.example.com",
510
+ };
511
+
512
+ global.fetch = vi
513
+ .fn()
514
+ .mockResolvedValueOnce({
515
+ ok: true,
516
+ json: async () => mockResponse1,
517
+ })
518
+ .mockResolvedValueOnce({
519
+ ok: true,
520
+ json: async () => mockResponse2,
521
+ });
522
+
523
+ const result1 = await fetchMerchantId("shop1.example.com");
524
+ expect(result1).toBe("merchant-domain1");
525
+
526
+ clearMerchantIdCache();
527
+
528
+ const result2 = await fetchMerchantId("shop2.example.com");
529
+ expect(result2).toBe("merchant-domain2");
530
+
531
+ expect(global.fetch).toHaveBeenCalledTimes(2);
532
+ });
533
+
534
+ it("should handle rapid successive calls with cache", async () => {
535
+ const mockResponse = {
536
+ merchantId: "merchant-rapid",
537
+ name: "Test Merchant",
538
+ domain: "shop.example.com",
539
+ };
540
+
541
+ global.fetch = vi.fn().mockResolvedValueOnce({
542
+ ok: true,
543
+ json: async () => mockResponse,
544
+ });
545
+
546
+ const results = await Promise.all([
547
+ fetchMerchantId("shop.example.com"),
548
+ fetchMerchantId("shop.example.com"),
549
+ fetchMerchantId("shop.example.com"),
550
+ fetchMerchantId("shop.example.com"),
551
+ fetchMerchantId("shop.example.com"),
552
+ ]);
553
+
554
+ expect(results).toEqual([
555
+ "merchant-rapid",
556
+ "merchant-rapid",
557
+ "merchant-rapid",
558
+ "merchant-rapid",
559
+ "merchant-rapid",
560
+ ]);
561
+ expect(global.fetch).toHaveBeenCalledTimes(1);
562
+ });
563
+ });
564
+ });
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Merchant ID utilities for auto-fetching from backend
3
+ */
4
+
5
+ import { getBackendUrl } from "./backendUrl";
6
+
7
+ /**
8
+ * Response from the merchant lookup endpoint
9
+ */
10
+ type MerchantLookupResponse = {
11
+ merchantId: string;
12
+ name: string;
13
+ domain: string;
14
+ };
15
+
16
+ /**
17
+ * In-memory cache for merchantId lookups
18
+ * Persists for the session to avoid repeated API calls
19
+ */
20
+ let cachedMerchantId: string | undefined;
21
+ let cachePromise: Promise<string | undefined> | undefined;
22
+
23
+ /**
24
+ * Fetch merchantId from backend by domain
25
+ *
26
+ * @param domain - The domain to lookup (defaults to current hostname)
27
+ * @param walletUrl - Optional wallet URL to derive backend URL
28
+ * @returns The merchantId if found, undefined otherwise
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const merchantId = await fetchMerchantId("shop.example.com");
33
+ * if (merchantId) {
34
+ * // Use merchantId for tracking
35
+ * }
36
+ * ```
37
+ */
38
+ export async function fetchMerchantId(
39
+ domain?: string,
40
+ walletUrl?: string
41
+ ): Promise<string | undefined> {
42
+ // Use cached value if available
43
+ if (cachedMerchantId) {
44
+ return cachedMerchantId;
45
+ }
46
+
47
+ // If a fetch is already in progress, wait for it
48
+ if (cachePromise) {
49
+ return cachePromise;
50
+ }
51
+
52
+ // Start the fetch and cache the promise
53
+ cachePromise = fetchMerchantIdInternal(domain, walletUrl);
54
+ const result = await cachePromise;
55
+ cachePromise = undefined;
56
+ return result;
57
+ }
58
+
59
+ /**
60
+ * Internal fetch logic
61
+ */
62
+ async function fetchMerchantIdInternal(
63
+ domain?: string,
64
+ walletUrl?: string
65
+ ): Promise<string | undefined> {
66
+ const targetDomain =
67
+ domain ??
68
+ (typeof window !== "undefined" ? window.location.hostname : "");
69
+ if (!targetDomain) {
70
+ return undefined;
71
+ }
72
+
73
+ try {
74
+ const backendUrl = getBackendUrl(walletUrl);
75
+ const response = await fetch(
76
+ `${backendUrl}/user/merchant/resolve?domain=${encodeURIComponent(targetDomain)}`
77
+ );
78
+
79
+ if (!response.ok) {
80
+ console.warn(
81
+ `[Frak SDK] Merchant lookup failed for domain ${targetDomain}: ${response.status}`
82
+ );
83
+ return undefined;
84
+ }
85
+
86
+ const data = (await response.json()) as MerchantLookupResponse;
87
+ cachedMerchantId = data.merchantId;
88
+ return cachedMerchantId;
89
+ } catch (error) {
90
+ console.warn("[Frak SDK] Failed to fetch merchantId:", error);
91
+ return undefined;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Clear the cached merchantId
97
+ * Useful for testing or when switching domains
98
+ */
99
+ export function clearMerchantIdCache(): void {
100
+ cachedMerchantId = undefined;
101
+ cachePromise = undefined;
102
+ }
103
+
104
+ /**
105
+ * Get merchantId from config or auto-fetch from backend
106
+ *
107
+ * @param config - The SDK config that may contain merchantId
108
+ * @param walletUrl - Optional wallet URL to derive backend URL
109
+ * @returns The merchantId if available (from config or fetch), undefined otherwise
110
+ */
111
+ export async function resolveMerchantId(
112
+ config: { metadata?: { merchantId?: string } },
113
+ walletUrl?: string
114
+ ): Promise<string | undefined> {
115
+ // First, check config
116
+ if (config.metadata?.merchantId) {
117
+ return config.metadata.merchantId;
118
+ }
119
+
120
+ // Otherwise, try to fetch from backend
121
+ return fetchMerchantId(undefined, walletUrl);
122
+ }