@frak-labs/core-sdk 0.1.1 → 0.2.0-beta.7898df5b

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 +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/computeLegacyProductId-CCAZvLa5.d.cts +537 -0
  12. package/dist/computeLegacyProductId-b5cUWdAm.d.ts +537 -0
  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-B0g7-807.d.cts} +173 -136
  18. package/dist/{openSso-DsKJ4y0j.d.ts → openSso-CMzwvaCa.d.ts} +173 -136
  19. package/dist/setupClient-BICl5fdX.js +13 -0
  20. package/dist/setupClient-nl8Dhh4V.cjs +13 -0
  21. package/dist/siweAuthenticate-BWmI2_TN.cjs +1 -0
  22. package/dist/{index-d8xS4ryI.d.ts → siweAuthenticate-CVigMOxz.d.cts} +113 -92
  23. package/dist/{index-C6FxkWPC.d.cts → siweAuthenticate-CnCZ7mok.d.ts} +113 -92
  24. package/dist/siweAuthenticate-zczqxm0a.js +1 -0
  25. package/dist/trackEvent-CeLFVzZn.js +1 -0
  26. package/dist/trackEvent-Ew5r5zfI.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 +117 -242
  37. package/src/actions/referral/processReferral.ts +134 -204
  38. package/src/actions/referral/referralInteraction.test.ts +4 -12
  39. package/src/actions/referral/referralInteraction.ts +3 -13
  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 +27 -16
  52. package/src/types/config.ts +6 -0
  53. package/src/types/context.ts +48 -6
  54. package/src/types/index.ts +15 -11
  55. package/src/types/lifecycle/client.ts +24 -1
  56. package/src/types/lifecycle/iframe.ts +6 -0
  57. package/src/types/rpc/displayModal.ts +2 -4
  58. package/src/types/rpc/embedded/index.ts +2 -2
  59. package/src/types/rpc/interaction.ts +31 -39
  60. package/src/types/rpc/merchantInformation.ts +77 -0
  61. package/src/types/rpc/modal/index.ts +0 -4
  62. package/src/types/rpc/modal/login.ts +5 -1
  63. package/src/types/rpc/walletStatus.ts +1 -7
  64. package/src/types/rpc.ts +22 -30
  65. package/src/types/tracking.ts +31 -0
  66. package/src/utils/FrakContext.test.ts +270 -186
  67. package/src/utils/FrakContext.ts +78 -56
  68. package/src/utils/backendUrl.test.ts +83 -0
  69. package/src/utils/backendUrl.ts +62 -0
  70. package/src/utils/clientId.test.ts +41 -0
  71. package/src/utils/clientId.ts +43 -0
  72. package/src/utils/compression/compress.test.ts +1 -1
  73. package/src/utils/compression/compress.ts +2 -2
  74. package/src/utils/compression/decompress.test.ts +8 -4
  75. package/src/utils/compression/decompress.ts +2 -2
  76. package/src/utils/{computeProductId.ts → computeLegacyProductId.ts} +2 -2
  77. package/src/utils/constants.ts +5 -0
  78. package/src/utils/deepLinkWithFallback.test.ts +243 -0
  79. package/src/utils/deepLinkWithFallback.ts +103 -0
  80. package/src/utils/formatAmount.ts +6 -0
  81. package/src/utils/iframeHelper.test.ts +18 -5
  82. package/src/utils/iframeHelper.ts +10 -3
  83. package/src/utils/index.ts +16 -1
  84. package/src/utils/merchantId.test.ts +653 -0
  85. package/src/utils/merchantId.ts +143 -0
  86. package/src/utils/sso.ts +18 -11
  87. package/src/utils/trackEvent.test.ts +23 -5
  88. package/src/utils/trackEvent.ts +13 -0
  89. package/cdn/bundle.iife.js +0 -14
  90. package/dist/actions-B5j-i1p0.cjs +0 -1
  91. package/dist/actions-q090Z0oR.js +0 -1
  92. package/dist/index-7OZ39x1U.d.ts +0 -195
  93. package/dist/index-CRsQWnTs.d.cts +0 -351
  94. package/dist/index-Ck1hudEi.d.ts +0 -351
  95. package/dist/index-zDq-VlKx.d.cts +0 -195
  96. package/dist/interaction-DMJ3ZfaF.d.cts +0 -45
  97. package/dist/interaction-KX1h9a7V.d.ts +0 -45
  98. package/dist/interactions-DnfM3oe0.js +0 -1
  99. package/dist/interactions-EIXhNLf6.cjs +0 -1
  100. package/dist/interactions.cjs +0 -1
  101. package/dist/interactions.d.cts +0 -2
  102. package/dist/interactions.d.ts +0 -2
  103. package/dist/interactions.js +0 -1
  104. package/dist/productTypes-BUkXJKZ7.cjs +0 -1
  105. package/dist/productTypes-CGb1MmBF.js +0 -1
  106. package/dist/src-1LQ4eLq5.js +0 -13
  107. package/dist/src-hW71KjPN.cjs +0 -13
  108. package/dist/trackEvent-CHnYa85W.js +0 -1
  109. package/dist/trackEvent-GuQm_1Nm.cjs +0 -1
  110. package/src/actions/getProductInformation.ts +0 -14
  111. package/src/actions/openSso.test.ts +0 -407
  112. package/src/actions/sendInteraction.test.ts +0 -219
  113. package/src/constants/interactionTypes.test.ts +0 -128
  114. package/src/constants/productTypes.test.ts +0 -130
  115. package/src/constants/productTypes.ts +0 -33
  116. package/src/interactions/index.ts +0 -5
  117. package/src/interactions/pressEncoder.test.ts +0 -215
  118. package/src/interactions/pressEncoder.ts +0 -53
  119. package/src/interactions/purchaseEncoder.test.ts +0 -291
  120. package/src/interactions/purchaseEncoder.ts +0 -99
  121. package/src/interactions/referralEncoder.test.ts +0 -170
  122. package/src/interactions/referralEncoder.ts +0 -47
  123. package/src/interactions/retailEncoder.test.ts +0 -107
  124. package/src/interactions/retailEncoder.ts +0 -37
  125. package/src/interactions/webshopEncoder.test.ts +0 -56
  126. package/src/interactions/webshopEncoder.ts +0 -30
  127. package/src/types/rpc/modal/openSession.ts +0 -25
  128. package/src/types/rpc/productInformation.ts +0 -59
  129. package/src/utils/computeProductId.test.ts +0 -80
  130. package/src/utils/sso.test.ts +0 -361
@@ -1,276 +1,401 @@
1
- /**
2
- * Tests for trackPurchaseStatus action
3
- * Tests webhook registration for purchase tracking
4
- */
5
-
1
+ import { vi } from "vitest";
6
2
  import {
7
3
  afterEach,
8
4
  beforeEach,
9
5
  describe,
10
6
  expect,
11
- it,
12
- vi,
7
+ test,
13
8
  } from "../../tests/vitest-fixtures";
9
+
10
+ vi.mock("../utils/clientId", () => ({
11
+ getClientId: vi.fn().mockReturnValue("test-client-id"),
12
+ }));
13
+
14
+ vi.mock("../utils/merchantId", () => ({
15
+ fetchMerchantId: vi.fn().mockResolvedValue(undefined),
16
+ }));
17
+
18
+ import { getClientId } from "../utils/clientId";
19
+ import { fetchMerchantId } from "../utils/merchantId";
14
20
  import { trackPurchaseStatus } from "./trackPurchaseStatus";
15
21
 
16
- describe("trackPurchaseStatus", () => {
22
+ describe.sequential("trackPurchaseStatus", () => {
23
+ const TRACK_PURCHASE_URL = "https://backend.frak.id/user/track/purchase";
24
+
17
25
  let mockSessionStorage: {
18
26
  getItem: ReturnType<typeof vi.fn>;
19
27
  setItem: ReturnType<typeof vi.fn>;
20
28
  removeItem: ReturnType<typeof vi.fn>;
21
29
  };
22
- let fetchSpy: any;
23
- let consoleWarnSpy: any;
30
+ let mockLocalStorage: {
31
+ getItem: ReturnType<typeof vi.fn>;
32
+ setItem: ReturnType<typeof vi.fn>;
33
+ removeItem: ReturnType<typeof vi.fn>;
34
+ };
35
+ let fetchSpy: ReturnType<typeof vi.fn>;
36
+ let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
37
+
38
+ function setupStorage(values: {
39
+ interactionToken?: string | null;
40
+ merchantId?: string | null;
41
+ clientId?: string | null;
42
+ }) {
43
+ mockSessionStorage.getItem.mockImplementation((key: string) => {
44
+ if (key === "frak-wallet-interaction-token") {
45
+ return values.interactionToken ?? null;
46
+ }
47
+ if (key === "frak-merchant-id") {
48
+ return values.merchantId ?? null;
49
+ }
50
+ return null;
51
+ });
52
+
53
+ mockLocalStorage.getItem.mockImplementation((key: string) => {
54
+ if (key === "frak-client-id") {
55
+ return values.clientId ?? null;
56
+ }
57
+ return null;
58
+ });
59
+ }
60
+
61
+ function getTrackingRequests() {
62
+ return fetchSpy.mock.calls.filter(
63
+ ([url]) => url === TRACK_PURCHASE_URL
64
+ );
65
+ }
66
+
67
+ function getLastTrackingRequest() {
68
+ return getTrackingRequests().at(-1);
69
+ }
24
70
 
25
71
  beforeEach(() => {
26
- // Mock sessionStorage
27
72
  mockSessionStorage = {
28
73
  getItem: vi.fn(),
29
74
  setItem: vi.fn(),
30
75
  removeItem: vi.fn(),
31
76
  };
77
+
78
+ mockLocalStorage = {
79
+ getItem: vi.fn(),
80
+ setItem: vi.fn(),
81
+ removeItem: vi.fn(),
82
+ };
83
+
32
84
  Object.defineProperty(window, "sessionStorage", {
33
85
  value: mockSessionStorage,
34
86
  writable: true,
35
87
  configurable: true,
36
88
  });
37
89
 
38
- // Mock fetch
90
+ Object.defineProperty(window, "localStorage", {
91
+ value: mockLocalStorage,
92
+ writable: true,
93
+ configurable: true,
94
+ });
95
+
96
+ setupStorage({
97
+ interactionToken: "token-123",
98
+ merchantId: null,
99
+ clientId: "test-client-id",
100
+ });
101
+
102
+ vi.mocked(getClientId).mockReturnValue("test-client-id");
103
+ vi.mocked(fetchMerchantId).mockResolvedValue(undefined);
104
+
39
105
  fetchSpy = vi.fn().mockResolvedValue({
40
106
  ok: true,
41
107
  status: 200,
42
108
  });
43
- global.fetch = fetchSpy;
109
+ global.fetch = fetchSpy as typeof fetch;
44
110
 
45
- // Mock console.warn
46
111
  consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
47
112
  });
48
113
 
49
114
  afterEach(() => {
50
- vi.clearAllMocks();
51
115
  consoleWarnSpy.mockRestore();
116
+ vi.clearAllMocks();
52
117
  });
53
118
 
54
119
  describe("successful tracking", () => {
55
- it("should send POST request with correct parameters", async () => {
56
- mockSessionStorage.getItem.mockReturnValue("token-123");
120
+ test("should send POST request with correct parameters including merchantId", async () => {
121
+ const callCountBefore = getTrackingRequests().length;
57
122
 
58
123
  await trackPurchaseStatus({
59
124
  customerId: "cust-456",
60
125
  orderId: "order-789",
61
126
  token: "purchase-token",
127
+ merchantId: "merchant-explicit",
62
128
  });
63
129
 
64
- expect(fetchSpy).toHaveBeenCalledWith(
65
- "https://backend.frak.id/interactions/listenForPurchase",
130
+ expect(getTrackingRequests().length).toBe(callCountBefore + 1);
131
+ expect(getLastTrackingRequest()).toEqual([
132
+ TRACK_PURCHASE_URL,
66
133
  {
67
134
  method: "POST",
68
135
  headers: {
69
136
  Accept: "application/json",
70
137
  "Content-Type": "application/json",
71
138
  "x-wallet-sdk-auth": "token-123",
139
+ "x-frak-client-id": "test-client-id",
72
140
  },
73
141
  body: JSON.stringify({
74
142
  customerId: "cust-456",
75
143
  orderId: "order-789",
76
144
  token: "purchase-token",
145
+ merchantId: "merchant-explicit",
77
146
  }),
78
- }
79
- );
147
+ },
148
+ ]);
80
149
  });
81
150
 
82
- it("should read interaction token from sessionStorage", async () => {
83
- mockSessionStorage.getItem.mockReturnValue("my-token");
151
+ test("should include x-frak-client-id header", async () => {
152
+ setupStorage({
153
+ interactionToken: null,
154
+ merchantId: null,
155
+ clientId: "test-client-id",
156
+ });
84
157
 
85
158
  await trackPurchaseStatus({
86
159
  customerId: "cust-1",
87
160
  orderId: "order-1",
88
161
  token: "token-1",
162
+ merchantId: "merchant-1",
89
163
  });
90
164
 
91
- expect(mockSessionStorage.getItem).toHaveBeenCalledWith(
92
- "frak-wallet-interaction-token"
93
- );
94
- expect(fetchSpy).toHaveBeenCalled();
165
+ const requestInit = getLastTrackingRequest()?.[1] as {
166
+ headers: Record<string, string>;
167
+ };
168
+ expect(requestInit.headers).toEqual({
169
+ Accept: "application/json",
170
+ "Content-Type": "application/json",
171
+ "x-frak-client-id": "test-client-id",
172
+ });
95
173
  });
96
174
 
97
- it("should handle numeric customerId", async () => {
98
- mockSessionStorage.getItem.mockReturnValue("token-123");
99
-
175
+ test("should include x-wallet-sdk-auth header when interaction token exists", async () => {
100
176
  await trackPurchaseStatus({
101
- customerId: 12345,
102
- orderId: "order-789",
103
- token: "purchase-token",
177
+ customerId: "cust-1",
178
+ orderId: "order-1",
179
+ token: "token-1",
180
+ merchantId: "merchant-1",
104
181
  });
105
182
 
106
- expect(fetchSpy).toHaveBeenCalledWith(
107
- expect.any(String),
108
- expect.objectContaining({
109
- body: JSON.stringify({
110
- customerId: 12345,
111
- orderId: "order-789",
112
- token: "purchase-token",
113
- }),
114
- })
115
- );
183
+ const requestInit = getLastTrackingRequest()?.[1] as {
184
+ headers: Record<string, string>;
185
+ };
186
+ expect(requestInit.headers).toEqual({
187
+ Accept: "application/json",
188
+ "Content-Type": "application/json",
189
+ "x-wallet-sdk-auth": "token-123",
190
+ "x-frak-client-id": "test-client-id",
191
+ });
116
192
  });
117
193
 
118
- it("should handle numeric orderId", async () => {
119
- mockSessionStorage.getItem.mockReturnValue("token-123");
120
-
194
+ test("should handle numeric customerId and orderId", async () => {
121
195
  await trackPurchaseStatus({
122
- customerId: "cust-456",
196
+ customerId: 12345,
123
197
  orderId: 67890,
124
198
  token: "purchase-token",
199
+ merchantId: "merchant-1",
125
200
  });
126
201
 
127
- expect(fetchSpy).toHaveBeenCalledWith(
128
- expect.any(String),
129
- expect.objectContaining({
130
- body: JSON.stringify({
131
- customerId: "cust-456",
132
- orderId: 67890,
133
- token: "purchase-token",
134
- }),
202
+ const requestInit = getLastTrackingRequest()?.[1] as {
203
+ body: string;
204
+ };
205
+ expect(requestInit.body).toBe(
206
+ JSON.stringify({
207
+ customerId: 12345,
208
+ orderId: 67890,
209
+ token: "purchase-token",
210
+ merchantId: "merchant-1",
135
211
  })
136
212
  );
137
213
  });
138
214
 
139
- it("should handle both numeric customerId and orderId", async () => {
140
- mockSessionStorage.getItem.mockReturnValue("token-123");
141
-
215
+ test("should use new endpoint URL /user/track/purchase", async () => {
142
216
  await trackPurchaseStatus({
143
- customerId: 12345,
144
- orderId: 67890,
145
- token: "purchase-token",
217
+ customerId: "cust-1",
218
+ orderId: "order-1",
219
+ token: "token-1",
220
+ merchantId: "merchant-1",
146
221
  });
147
222
 
148
- expect(fetchSpy).toHaveBeenCalledWith(
149
- expect.any(String),
150
- expect.objectContaining({
151
- body: JSON.stringify({
152
- customerId: 12345,
153
- orderId: 67890,
154
- token: "purchase-token",
155
- }),
156
- })
157
- );
223
+ expect(getLastTrackingRequest()?.[0]).toBe(TRACK_PURCHASE_URL);
158
224
  });
159
225
  });
160
226
 
161
- describe("missing interaction token", () => {
162
- it("should warn when no interaction token found", async () => {
163
- mockSessionStorage.getItem.mockReturnValue(null);
227
+ describe("merchantId resolution", () => {
228
+ test("should resolve merchantId from explicit param first", async () => {
229
+ setupStorage({
230
+ interactionToken: "token-123",
231
+ merchantId: "session-merchant-id",
232
+ clientId: "test-client-id",
233
+ });
234
+ vi.mocked(fetchMerchantId).mockResolvedValue("fetched-merchant-id");
235
+ const merchantLookupCallsBefore =
236
+ vi.mocked(fetchMerchantId).mock.calls.length;
164
237
 
165
238
  await trackPurchaseStatus({
166
- customerId: "cust-456",
167
- orderId: "order-789",
168
- token: "purchase-token",
239
+ customerId: "cust-1",
240
+ orderId: "order-1",
241
+ token: "token-1",
242
+ merchantId: "explicit-merchant-id",
169
243
  });
170
244
 
171
- expect(consoleWarnSpy).toHaveBeenCalledWith(
172
- "[Frak] No frak session found, skipping purchase check"
245
+ const requestInit = getLastTrackingRequest()?.[1] as {
246
+ body: string;
247
+ };
248
+ expect(requestInit.body).toBe(
249
+ JSON.stringify({
250
+ customerId: "cust-1",
251
+ orderId: "order-1",
252
+ token: "token-1",
253
+ merchantId: "explicit-merchant-id",
254
+ })
255
+ );
256
+ expect(vi.mocked(fetchMerchantId).mock.calls.length).toBe(
257
+ merchantLookupCallsBefore
173
258
  );
174
259
  });
175
260
 
176
- it("should not send request when no interaction token", async () => {
177
- mockSessionStorage.getItem.mockReturnValue(null);
261
+ test("should fall back to sessionStorage for merchantId", async () => {
262
+ setupStorage({
263
+ interactionToken: "token-123",
264
+ merchantId: "session-merchant-id",
265
+ clientId: "test-client-id",
266
+ });
267
+ const merchantLookupCallsBefore =
268
+ vi.mocked(fetchMerchantId).mock.calls.length;
178
269
 
179
270
  await trackPurchaseStatus({
180
- customerId: "cust-456",
181
- orderId: "order-789",
182
- token: "purchase-token",
271
+ customerId: "cust-1",
272
+ orderId: "order-1",
273
+ token: "token-1",
183
274
  });
184
275
 
185
- expect(fetchSpy).not.toHaveBeenCalled();
276
+ const requestInit = getLastTrackingRequest()?.[1] as {
277
+ body: string;
278
+ };
279
+ expect(requestInit.body).toBe(
280
+ JSON.stringify({
281
+ customerId: "cust-1",
282
+ orderId: "order-1",
283
+ token: "token-1",
284
+ merchantId: "session-merchant-id",
285
+ })
286
+ );
287
+ expect(vi.mocked(fetchMerchantId).mock.calls.length).toBe(
288
+ merchantLookupCallsBefore
289
+ );
186
290
  });
187
291
 
188
- it("should not send request when interaction token is empty string", async () => {
189
- mockSessionStorage.getItem.mockReturnValue("");
292
+ test("should fall back to fetchMerchantId when no explicit or sessionStorage", async () => {
293
+ setupStorage({
294
+ interactionToken: "token-123",
295
+ merchantId: null,
296
+ clientId: "test-client-id",
297
+ });
298
+ vi.mocked(fetchMerchantId).mockResolvedValue("fetched-merchant-id");
190
299
 
191
300
  await trackPurchaseStatus({
192
- customerId: "cust-456",
193
- orderId: "order-789",
194
- token: "purchase-token",
301
+ customerId: "cust-1",
302
+ orderId: "order-1",
303
+ token: "token-1",
195
304
  });
196
305
 
197
- expect(fetchSpy).not.toHaveBeenCalled();
198
- });
199
- });
200
-
201
- describe("network errors", () => {
202
- it("should handle fetch rejection", async () => {
203
- mockSessionStorage.getItem.mockReturnValue("token-123");
204
- fetchSpy.mockRejectedValue(new Error("Network error"));
205
-
206
- await expect(
207
- trackPurchaseStatus({
208
- customerId: "cust-456",
209
- orderId: "order-789",
210
- token: "purchase-token",
306
+ const requestInit = getLastTrackingRequest()?.[1] as {
307
+ body: string;
308
+ };
309
+ expect(requestInit.body).toBe(
310
+ JSON.stringify({
311
+ customerId: "cust-1",
312
+ orderId: "order-1",
313
+ token: "token-1",
314
+ merchantId: "fetched-merchant-id",
211
315
  })
212
- ).rejects.toThrow("Network error");
316
+ );
213
317
  });
214
318
 
215
- it("should handle fetch with error response", async () => {
216
- mockSessionStorage.getItem.mockReturnValue("token-123");
217
- fetchSpy.mockResolvedValue({
218
- ok: false,
219
- status: 500,
319
+ test("should warn and skip when no merchantId available", async () => {
320
+ setupStorage({
321
+ interactionToken: "token-123",
322
+ merchantId: null,
323
+ clientId: "test-client-id",
220
324
  });
325
+ vi.mocked(fetchMerchantId).mockResolvedValue(undefined);
326
+ const callCountBefore = getTrackingRequests().length;
221
327
 
222
- // Function doesn't check response, so it should complete
223
328
  await trackPurchaseStatus({
224
- customerId: "cust-456",
225
- orderId: "order-789",
226
- token: "purchase-token",
329
+ customerId: "cust-1",
330
+ orderId: "order-1",
331
+ token: "token-1",
227
332
  });
228
333
 
229
- expect(fetchSpy).toHaveBeenCalled();
334
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
335
+ "[Frak] No merchant id found, skipping purchase check"
336
+ );
337
+ expect(getTrackingRequests().length).toBe(callCountBefore);
230
338
  });
231
339
  });
232
340
 
233
- describe("request format", () => {
234
- it("should include correct headers", async () => {
235
- mockSessionStorage.getItem.mockReturnValue("my-auth-token");
341
+ describe("anonymous user support", () => {
342
+ test("should send request with only x-frak-client-id when no interaction token", async () => {
343
+ setupStorage({
344
+ interactionToken: null,
345
+ merchantId: null,
346
+ clientId: "test-client-id",
347
+ });
236
348
 
237
349
  await trackPurchaseStatus({
238
350
  customerId: "cust-1",
239
351
  orderId: "order-1",
240
352
  token: "token-1",
353
+ merchantId: "merchant-1",
241
354
  });
242
355
 
243
- expect(fetchSpy).toHaveBeenCalledWith(
244
- expect.any(String),
245
- expect.objectContaining({
246
- headers: {
247
- Accept: "application/json",
248
- "Content-Type": "application/json",
249
- "x-wallet-sdk-auth": "my-auth-token",
250
- },
251
- })
252
- );
356
+ const requestInit = getLastTrackingRequest()?.[1] as {
357
+ headers: Record<string, string>;
358
+ };
359
+ expect(requestInit.headers).toEqual({
360
+ Accept: "application/json",
361
+ "Content-Type": "application/json",
362
+ "x-frak-client-id": "test-client-id",
363
+ });
253
364
  });
254
365
 
255
- it("should use POST method", async () => {
256
- mockSessionStorage.getItem.mockReturnValue("token-123");
366
+ test("should send request with both headers when both available", async () => {
367
+ setupStorage({
368
+ interactionToken: "token-123",
369
+ merchantId: null,
370
+ clientId: "test-client-id",
371
+ });
257
372
 
258
373
  await trackPurchaseStatus({
259
374
  customerId: "cust-1",
260
375
  orderId: "order-1",
261
376
  token: "token-1",
377
+ merchantId: "merchant-1",
262
378
  });
263
379
 
264
- expect(fetchSpy).toHaveBeenCalledWith(
265
- expect.any(String),
266
- expect.objectContaining({
267
- method: "POST",
268
- })
269
- );
380
+ const requestInit = getLastTrackingRequest()?.[1] as {
381
+ headers: Record<string, string>;
382
+ };
383
+ expect(requestInit.headers).toEqual({
384
+ Accept: "application/json",
385
+ "Content-Type": "application/json",
386
+ "x-wallet-sdk-auth": "token-123",
387
+ "x-frak-client-id": "test-client-id",
388
+ });
270
389
  });
271
390
 
272
- it("should target correct backend URL", async () => {
273
- mockSessionStorage.getItem.mockReturnValue("token-123");
391
+ test("should skip when no identity available", async () => {
392
+ setupStorage({
393
+ interactionToken: null,
394
+ merchantId: "merchant-1",
395
+ clientId: null,
396
+ });
397
+ vi.mocked(getClientId).mockReturnValue("");
398
+ const callCountBefore = getTrackingRequests().length;
274
399
 
275
400
  await trackPurchaseStatus({
276
401
  customerId: "cust-1",
@@ -278,10 +403,98 @@ describe("trackPurchaseStatus", () => {
278
403
  token: "token-1",
279
404
  });
280
405
 
281
- expect(fetchSpy).toHaveBeenCalledWith(
282
- "https://backend.frak.id/interactions/listenForPurchase",
283
- expect.any(Object)
406
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
407
+ "[Frak] No identity found, skipping purchase check"
408
+ );
409
+ expect(getTrackingRequests().length).toBe(callCountBefore);
410
+ });
411
+ });
412
+
413
+ describe("missing identity", () => {
414
+ test("should warn when no identity sources available", async () => {
415
+ setupStorage({
416
+ interactionToken: null,
417
+ merchantId: "merchant-1",
418
+ clientId: null,
419
+ });
420
+ vi.mocked(getClientId).mockReturnValue("");
421
+ const callCountBefore = getTrackingRequests().length;
422
+
423
+ await trackPurchaseStatus({
424
+ customerId: "cust-456",
425
+ orderId: "order-789",
426
+ token: "purchase-token",
427
+ });
428
+
429
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
430
+ "[Frak] No identity found, skipping purchase check"
284
431
  );
432
+ expect(getTrackingRequests().length).toBe(callCountBefore);
433
+ });
434
+ });
435
+
436
+ describe("non-browser environment", () => {
437
+ test("should warn and skip when window is undefined", async () => {
438
+ const savedWindow = globalThis.window;
439
+ Reflect.deleteProperty(globalThis, "window");
440
+ const callCountBefore = getTrackingRequests().length;
441
+
442
+ try {
443
+ await trackPurchaseStatus({
444
+ customerId: "cust-1",
445
+ orderId: "order-1",
446
+ token: "token-1",
447
+ merchantId: "merchant-1",
448
+ });
449
+ } finally {
450
+ Object.defineProperty(globalThis, "window", {
451
+ value: savedWindow,
452
+ writable: true,
453
+ configurable: true,
454
+ });
455
+ }
456
+
457
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
458
+ "[Frak] No window found, can't track purchase"
459
+ );
460
+ expect(getTrackingRequests().length).toBe(callCountBefore);
461
+ });
462
+ });
463
+
464
+ describe("network errors", () => {
465
+ test("should handle fetch rejection", async () => {
466
+ vi.mocked(getClientId).mockReturnValue("test-client-id");
467
+ setupStorage({
468
+ interactionToken: "token-123",
469
+ merchantId: null,
470
+ clientId: "test-client-id",
471
+ });
472
+ fetchSpy.mockRejectedValueOnce(new Error("Network error"));
473
+
474
+ await expect(
475
+ trackPurchaseStatus({
476
+ customerId: "cust-456",
477
+ orderId: "order-789",
478
+ token: "purchase-token",
479
+ merchantId: "merchant-1",
480
+ })
481
+ ).rejects.toThrow("Network error");
482
+ });
483
+
484
+ test("should handle fetch with error response", async () => {
485
+ fetchSpy.mockResolvedValue({
486
+ ok: false,
487
+ status: 500,
488
+ });
489
+
490
+ await trackPurchaseStatus({
491
+ customerId: "cust-456",
492
+ orderId: "order-789",
493
+ token: "purchase-token",
494
+ merchantId: "merchant-1",
495
+ });
496
+
497
+ expect(fetchSpy).toHaveBeenCalled();
285
498
  });
286
499
  });
287
500
  });