@frak-labs/core-sdk 0.1.0 → 0.1.1-beta.1e44255d

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 (131) 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-BkyJ4rEY.d.ts +538 -0
  12. package/dist/computeLegacyProductId-Raks6FXg.d.cts +538 -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-BCJGchIb.d.cts +1022 -0
  18. package/dist/openSso-DG-_9CED.d.ts +1022 -0
  19. package/dist/setupClient-Cfwpu08d.js +13 -0
  20. package/dist/setupClient-Dh8ljuhV.cjs +13 -0
  21. package/dist/siweAuthenticate-BH7Dn7nZ.d.cts +536 -0
  22. package/dist/siweAuthenticate-BJHbtty4.js +1 -0
  23. package/dist/siweAuthenticate-Btem4QHs.d.ts +536 -0
  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 +25 -31
  28. package/src/actions/displayEmbeddedWallet.test.ts +194 -0
  29. package/src/actions/displayEmbeddedWallet.ts +21 -0
  30. package/src/actions/displayModal.test.ts +388 -0
  31. package/src/actions/displayModal.ts +120 -0
  32. package/src/actions/ensureIdentity.ts +68 -0
  33. package/src/actions/getMerchantInformation.test.ts +116 -0
  34. package/src/actions/getMerchantInformation.ts +16 -0
  35. package/src/actions/index.ts +30 -0
  36. package/src/actions/openSso.ts +118 -0
  37. package/src/actions/prepareSso.test.ts +223 -0
  38. package/src/actions/prepareSso.ts +48 -0
  39. package/src/actions/referral/processReferral.test.ts +248 -0
  40. package/src/actions/referral/processReferral.ts +232 -0
  41. package/src/actions/referral/referralInteraction.test.ts +147 -0
  42. package/src/actions/referral/referralInteraction.ts +52 -0
  43. package/src/actions/sendInteraction.ts +56 -0
  44. package/src/actions/trackPurchaseStatus.test.ts +500 -0
  45. package/src/actions/trackPurchaseStatus.ts +90 -0
  46. package/src/actions/watchWalletStatus.test.ts +372 -0
  47. package/src/actions/watchWalletStatus.ts +93 -0
  48. package/src/actions/wrapper/modalBuilder.test.ts +239 -0
  49. package/src/actions/wrapper/modalBuilder.ts +203 -0
  50. package/src/actions/wrapper/sendTransaction.test.ts +164 -0
  51. package/src/actions/wrapper/sendTransaction.ts +62 -0
  52. package/src/actions/wrapper/siweAuthenticate.test.ts +290 -0
  53. package/src/actions/wrapper/siweAuthenticate.ts +94 -0
  54. package/src/bundle.ts +2 -0
  55. package/src/clients/DebugInfo.test.ts +418 -0
  56. package/src/clients/DebugInfo.ts +182 -0
  57. package/src/clients/createIFrameFrakClient.ts +292 -0
  58. package/src/clients/index.ts +3 -0
  59. package/src/clients/setupClient.test.ts +343 -0
  60. package/src/clients/setupClient.ts +73 -0
  61. package/src/clients/transports/iframeLifecycleManager.test.ts +558 -0
  62. package/src/clients/transports/iframeLifecycleManager.ts +229 -0
  63. package/src/constants/interactionTypes.ts +15 -0
  64. package/src/constants/locales.ts +14 -0
  65. package/src/index.ts +109 -0
  66. package/src/types/client.ts +14 -0
  67. package/src/types/compression.ts +22 -0
  68. package/src/types/config.ts +117 -0
  69. package/src/types/context.ts +13 -0
  70. package/src/types/index.ts +74 -0
  71. package/src/types/lifecycle/client.ts +69 -0
  72. package/src/types/lifecycle/iframe.ts +41 -0
  73. package/src/types/lifecycle/index.ts +2 -0
  74. package/src/types/rpc/displayModal.ts +82 -0
  75. package/src/types/rpc/embedded/index.ts +68 -0
  76. package/src/types/rpc/embedded/loggedIn.ts +55 -0
  77. package/src/types/rpc/embedded/loggedOut.ts +28 -0
  78. package/src/types/rpc/interaction.ts +30 -0
  79. package/src/types/rpc/merchantInformation.ts +77 -0
  80. package/src/types/rpc/modal/final.ts +46 -0
  81. package/src/types/rpc/modal/generic.ts +46 -0
  82. package/src/types/rpc/modal/index.ts +16 -0
  83. package/src/types/rpc/modal/login.ts +36 -0
  84. package/src/types/rpc/modal/siweAuthenticate.ts +37 -0
  85. package/src/types/rpc/modal/transaction.ts +33 -0
  86. package/src/types/rpc/sso.ts +80 -0
  87. package/src/types/rpc/walletStatus.ts +29 -0
  88. package/src/types/rpc.ts +150 -0
  89. package/src/types/tracking.ts +60 -0
  90. package/src/types/transport.ts +34 -0
  91. package/src/utils/FrakContext.test.ts +407 -0
  92. package/src/utils/FrakContext.ts +158 -0
  93. package/src/utils/backendUrl.test.ts +83 -0
  94. package/src/utils/backendUrl.ts +62 -0
  95. package/src/utils/clientId.test.ts +41 -0
  96. package/src/utils/clientId.ts +43 -0
  97. package/src/utils/compression/b64.test.ts +181 -0
  98. package/src/utils/compression/b64.ts +29 -0
  99. package/src/utils/compression/compress.test.ts +123 -0
  100. package/src/utils/compression/compress.ts +11 -0
  101. package/src/utils/compression/decompress.test.ts +149 -0
  102. package/src/utils/compression/decompress.ts +11 -0
  103. package/src/utils/compression/index.ts +3 -0
  104. package/src/utils/computeLegacyProductId.ts +11 -0
  105. package/src/utils/constants.test.ts +23 -0
  106. package/src/utils/constants.ts +9 -0
  107. package/src/utils/deepLinkWithFallback.test.ts +243 -0
  108. package/src/utils/deepLinkWithFallback.ts +103 -0
  109. package/src/utils/formatAmount.test.ts +113 -0
  110. package/src/utils/formatAmount.ts +24 -0
  111. package/src/utils/getCurrencyAmountKey.test.ts +44 -0
  112. package/src/utils/getCurrencyAmountKey.ts +15 -0
  113. package/src/utils/getSupportedCurrency.test.ts +51 -0
  114. package/src/utils/getSupportedCurrency.ts +14 -0
  115. package/src/utils/getSupportedLocale.test.ts +64 -0
  116. package/src/utils/getSupportedLocale.ts +16 -0
  117. package/src/utils/iframeHelper.test.ts +463 -0
  118. package/src/utils/iframeHelper.ts +150 -0
  119. package/src/utils/index.ts +36 -0
  120. package/src/utils/merchantId.test.ts +653 -0
  121. package/src/utils/merchantId.ts +143 -0
  122. package/src/utils/sso.ts +126 -0
  123. package/src/utils/ssoUrlListener.test.ts +252 -0
  124. package/src/utils/ssoUrlListener.ts +60 -0
  125. package/src/utils/trackEvent.test.ts +180 -0
  126. package/src/utils/trackEvent.ts +41 -0
  127. package/cdn/bundle.js.LICENSE.txt +0 -10
  128. package/dist/interactions.cjs +0 -1
  129. package/dist/interactions.d.cts +0 -182
  130. package/dist/interactions.d.ts +0 -182
  131. package/dist/interactions.js +0 -1
@@ -0,0 +1,52 @@
1
+ import type { DisplayEmbeddedWalletParamsType, FrakClient } from "../../types";
2
+ import { FrakContextManager } from "../../utils";
3
+ import { watchWalletStatus } from "../index";
4
+ import {
5
+ type ProcessReferralOptions,
6
+ processReferral,
7
+ } from "./processReferral";
8
+
9
+ /**
10
+ * Function used to handle referral interactions
11
+ * @param client - The current Frak Client
12
+ * @param args
13
+ * @param args.modalConfig - The modal configuration to display if the user is not logged in
14
+ * @param args.options - Some options for the referral interaction
15
+ *
16
+ * @returns A promise with the resulting referral state, or undefined in case of an error
17
+ *
18
+ * @description This function will automatically handle the referral interaction process
19
+ *
20
+ * @see {@link processReferral} for more details on the automatic referral handling process
21
+ * @see {@link @frak-labs/core-sdk!ModalStepTypes} for more details on each modal steps types
22
+ */
23
+ export async function referralInteraction(
24
+ client: FrakClient,
25
+ {
26
+ modalConfig,
27
+ options,
28
+ }: {
29
+ modalConfig?: DisplayEmbeddedWalletParamsType;
30
+ options?: ProcessReferralOptions;
31
+ } = {}
32
+ ) {
33
+ // Get the current frak context
34
+ const frakContext = FrakContextManager.parse({
35
+ url: window.location.href,
36
+ });
37
+
38
+ // Get the current wallet status
39
+ const currentWalletStatus = await watchWalletStatus(client);
40
+
41
+ try {
42
+ return await processReferral(client, {
43
+ walletStatus: currentWalletStatus,
44
+ frakContext,
45
+ modalConfig,
46
+ options,
47
+ });
48
+ } catch (error) {
49
+ console.warn("Error processing referral", { error });
50
+ }
51
+ return;
52
+ }
@@ -0,0 +1,56 @@
1
+ import type { FrakClient } from "../types";
2
+ import type { SendInteractionParamsType } from "../types/rpc/interaction";
3
+ import { getClientId } from "../utils/clientId";
4
+
5
+ /**
6
+ * Send an interaction to the backend via the listener RPC.
7
+ * Fire-and-forget: errors are caught and logged, not thrown.
8
+ *
9
+ * @param client - The Frak client instance
10
+ * @param params - The interaction parameters
11
+ *
12
+ * @description Sends a user interaction event through the wallet iframe RPC. Supports three interaction types: arrival tracking, sharing events, and custom interactions.
13
+ *
14
+ * @example
15
+ * Track a user arrival with referral attribution:
16
+ * ```ts
17
+ * await sendInteraction(client, {
18
+ * type: "arrival",
19
+ * referrerWallet: "0x1234...abcd",
20
+ * landingUrl: window.location.href,
21
+ * utmSource: "twitter",
22
+ * utmMedium: "social",
23
+ * utmCampaign: "launch-2026",
24
+ * });
25
+ * ```
26
+ *
27
+ * @example
28
+ * Track a sharing event:
29
+ * ```ts
30
+ * await sendInteraction(client, { type: "sharing" });
31
+ * ```
32
+ *
33
+ * @example
34
+ * Send a custom interaction:
35
+ * ```ts
36
+ * await sendInteraction(client, {
37
+ * type: "custom",
38
+ * customType: "newsletter_signup",
39
+ * data: { email: "user@example.com" },
40
+ * });
41
+ * ```
42
+ */
43
+ export async function sendInteraction(
44
+ client: FrakClient,
45
+ params: SendInteractionParamsType
46
+ ): Promise<void> {
47
+ try {
48
+ await client.request({
49
+ method: "frak_sendInteraction",
50
+ params: [params, { clientId: getClientId() }],
51
+ });
52
+ } catch {
53
+ // Silent failure - fire-and-forget
54
+ console.warn("[Frak SDK] Failed to send interaction:", params.type);
55
+ }
56
+ }
@@ -0,0 +1,500 @@
1
+ import { vi } from "vitest";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ test,
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";
20
+ import { trackPurchaseStatus } from "./trackPurchaseStatus";
21
+
22
+ describe.sequential("trackPurchaseStatus", () => {
23
+ const TRACK_PURCHASE_URL = "https://backend.frak.id/user/track/purchase";
24
+
25
+ let mockSessionStorage: {
26
+ getItem: ReturnType<typeof vi.fn>;
27
+ setItem: ReturnType<typeof vi.fn>;
28
+ removeItem: ReturnType<typeof vi.fn>;
29
+ };
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
+ }
70
+
71
+ beforeEach(() => {
72
+ mockSessionStorage = {
73
+ getItem: vi.fn(),
74
+ setItem: vi.fn(),
75
+ removeItem: vi.fn(),
76
+ };
77
+
78
+ mockLocalStorage = {
79
+ getItem: vi.fn(),
80
+ setItem: vi.fn(),
81
+ removeItem: vi.fn(),
82
+ };
83
+
84
+ Object.defineProperty(window, "sessionStorage", {
85
+ value: mockSessionStorage,
86
+ writable: true,
87
+ configurable: true,
88
+ });
89
+
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
+
105
+ fetchSpy = vi.fn().mockResolvedValue({
106
+ ok: true,
107
+ status: 200,
108
+ });
109
+ global.fetch = fetchSpy as typeof fetch;
110
+
111
+ consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
112
+ });
113
+
114
+ afterEach(() => {
115
+ consoleWarnSpy.mockRestore();
116
+ vi.clearAllMocks();
117
+ });
118
+
119
+ describe("successful tracking", () => {
120
+ test("should send POST request with correct parameters including merchantId", async () => {
121
+ const callCountBefore = getTrackingRequests().length;
122
+
123
+ await trackPurchaseStatus({
124
+ customerId: "cust-456",
125
+ orderId: "order-789",
126
+ token: "purchase-token",
127
+ merchantId: "merchant-explicit",
128
+ });
129
+
130
+ expect(getTrackingRequests().length).toBe(callCountBefore + 1);
131
+ expect(getLastTrackingRequest()).toEqual([
132
+ TRACK_PURCHASE_URL,
133
+ {
134
+ method: "POST",
135
+ headers: {
136
+ Accept: "application/json",
137
+ "Content-Type": "application/json",
138
+ "x-wallet-sdk-auth": "token-123",
139
+ "x-frak-client-id": "test-client-id",
140
+ },
141
+ body: JSON.stringify({
142
+ customerId: "cust-456",
143
+ orderId: "order-789",
144
+ token: "purchase-token",
145
+ merchantId: "merchant-explicit",
146
+ }),
147
+ },
148
+ ]);
149
+ });
150
+
151
+ test("should include x-frak-client-id header", async () => {
152
+ setupStorage({
153
+ interactionToken: null,
154
+ merchantId: null,
155
+ clientId: "test-client-id",
156
+ });
157
+
158
+ await trackPurchaseStatus({
159
+ customerId: "cust-1",
160
+ orderId: "order-1",
161
+ token: "token-1",
162
+ merchantId: "merchant-1",
163
+ });
164
+
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
+ });
173
+ });
174
+
175
+ test("should include x-wallet-sdk-auth header when interaction token exists", async () => {
176
+ await trackPurchaseStatus({
177
+ customerId: "cust-1",
178
+ orderId: "order-1",
179
+ token: "token-1",
180
+ merchantId: "merchant-1",
181
+ });
182
+
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
+ });
192
+ });
193
+
194
+ test("should handle numeric customerId and orderId", async () => {
195
+ await trackPurchaseStatus({
196
+ customerId: 12345,
197
+ orderId: 67890,
198
+ token: "purchase-token",
199
+ merchantId: "merchant-1",
200
+ });
201
+
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",
211
+ })
212
+ );
213
+ });
214
+
215
+ test("should use new endpoint URL /user/track/purchase", async () => {
216
+ await trackPurchaseStatus({
217
+ customerId: "cust-1",
218
+ orderId: "order-1",
219
+ token: "token-1",
220
+ merchantId: "merchant-1",
221
+ });
222
+
223
+ expect(getLastTrackingRequest()?.[0]).toBe(TRACK_PURCHASE_URL);
224
+ });
225
+ });
226
+
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;
237
+
238
+ await trackPurchaseStatus({
239
+ customerId: "cust-1",
240
+ orderId: "order-1",
241
+ token: "token-1",
242
+ merchantId: "explicit-merchant-id",
243
+ });
244
+
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
258
+ );
259
+ });
260
+
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;
269
+
270
+ await trackPurchaseStatus({
271
+ customerId: "cust-1",
272
+ orderId: "order-1",
273
+ token: "token-1",
274
+ });
275
+
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
+ );
290
+ });
291
+
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");
299
+
300
+ await trackPurchaseStatus({
301
+ customerId: "cust-1",
302
+ orderId: "order-1",
303
+ token: "token-1",
304
+ });
305
+
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",
315
+ })
316
+ );
317
+ });
318
+
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",
324
+ });
325
+ vi.mocked(fetchMerchantId).mockResolvedValue(undefined);
326
+ const callCountBefore = getTrackingRequests().length;
327
+
328
+ await trackPurchaseStatus({
329
+ customerId: "cust-1",
330
+ orderId: "order-1",
331
+ token: "token-1",
332
+ });
333
+
334
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
335
+ "[Frak] No merchant id found, skipping purchase check"
336
+ );
337
+ expect(getTrackingRequests().length).toBe(callCountBefore);
338
+ });
339
+ });
340
+
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
+ });
348
+
349
+ await trackPurchaseStatus({
350
+ customerId: "cust-1",
351
+ orderId: "order-1",
352
+ token: "token-1",
353
+ merchantId: "merchant-1",
354
+ });
355
+
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
+ });
364
+ });
365
+
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
+ });
372
+
373
+ await trackPurchaseStatus({
374
+ customerId: "cust-1",
375
+ orderId: "order-1",
376
+ token: "token-1",
377
+ merchantId: "merchant-1",
378
+ });
379
+
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
+ });
389
+ });
390
+
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;
399
+
400
+ await trackPurchaseStatus({
401
+ customerId: "cust-1",
402
+ orderId: "order-1",
403
+ token: "token-1",
404
+ });
405
+
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"
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();
498
+ });
499
+ });
500
+ });
@@ -0,0 +1,90 @@
1
+ import { getBackendUrl } from "../utils/backendUrl";
2
+ import { getClientId } from "../utils/clientId";
3
+ import { fetchMerchantId } from "../utils/merchantId";
4
+
5
+ /**
6
+ * Function used to track the status of a purchase
7
+ * when a purchase is tracked, the `purchaseCompleted` interactions will be automatically send for the user when we receive the purchase confirmation via webhook.
8
+ *
9
+ * @param args.customerId - The customer id that made the purchase (on your side)
10
+ * @param args.orderId - The order id of the purchase (on your side)
11
+ * @param args.token - The token of the purchase
12
+ * @param args.merchantId - Optional explicit merchant id to use for the tracking request
13
+ *
14
+ * @description This function will send a request to the backend to listen for the purchase status.
15
+ *
16
+ * @example
17
+ * async function trackPurchase(checkout) {
18
+ * const payload = {
19
+ * customerId: checkout.order.customer.id,
20
+ * orderId: checkout.order.id,
21
+ * token: checkout.token,
22
+ * merchantId: "your-merchant-id",
23
+ * };
24
+ *
25
+ * await trackPurchaseStatus(payload);
26
+ * }
27
+ *
28
+ * @remarks
29
+ * - Merchant id is resolved in this order: explicit `args.merchantId`, `frak-merchant-id` from session storage, then `fetchMerchantId()`.
30
+ * - This function supports anonymous users and will use the `x-frak-client-id` header when available.
31
+ * - At least one identity source must exist (`frak-wallet-interaction-token` or `x-frak-client-id`), otherwise the tracking request is skipped.
32
+ * - This function will print a warning if used in a non-browser environment or if no identity / merchant id can be resolved.
33
+ */
34
+ export async function trackPurchaseStatus(args: {
35
+ customerId: string | number;
36
+ orderId: string | number;
37
+ token: string;
38
+ merchantId?: string;
39
+ }) {
40
+ if (typeof window === "undefined") {
41
+ console.warn("[Frak] No window found, can't track purchase");
42
+ return;
43
+ }
44
+
45
+ const interactionToken = window.sessionStorage.getItem(
46
+ "frak-wallet-interaction-token"
47
+ );
48
+
49
+ const clientId = getClientId();
50
+ if (!interactionToken && !clientId) {
51
+ console.warn("[Frak] No identity found, skipping purchase check");
52
+ return;
53
+ }
54
+
55
+ const merchantIdFromStorage =
56
+ window.sessionStorage.getItem("frak-merchant-id");
57
+ const merchantId =
58
+ args.merchantId ?? merchantIdFromStorage ?? (await fetchMerchantId());
59
+
60
+ if (!merchantId) {
61
+ console.warn("[Frak] No merchant id found, skipping purchase check");
62
+ return;
63
+ }
64
+
65
+ const headers: Record<string, string> = {
66
+ Accept: "application/json",
67
+ "Content-Type": "application/json",
68
+ };
69
+
70
+ if (interactionToken) {
71
+ headers["x-wallet-sdk-auth"] = interactionToken;
72
+ }
73
+
74
+ if (clientId) {
75
+ headers["x-frak-client-id"] = clientId;
76
+ }
77
+
78
+ // Submit the listening request
79
+ const backendUrl = getBackendUrl();
80
+ await fetch(`${backendUrl}/user/track/purchase`, {
81
+ method: "POST",
82
+ headers,
83
+ body: JSON.stringify({
84
+ customerId: args.customerId,
85
+ orderId: args.orderId,
86
+ token: args.token,
87
+ merchantId,
88
+ }),
89
+ });
90
+ }