@cimplify/sdk 0.104.0 → 0.104.1

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 (71) hide show
  1. package/dist/advanced.d.mts +3 -7
  2. package/dist/advanced.d.ts +3 -7
  3. package/dist/advanced.js +23 -23
  4. package/dist/advanced.mjs +4 -4
  5. package/dist/{client-BuLcOWLv.d.ts → catalogue-BlQ85Uma.d.mts} +415 -763
  6. package/dist/{client-D5AoobFc.d.mts → catalogue-C8TqOj9K.d.ts} +415 -763
  7. package/dist/{payment-g0HFkEDU.d.mts → checkout-BqK9XUz5.d.mts} +3824 -1446
  8. package/dist/{payment-g0HFkEDU.d.ts → checkout-BqK9XUz5.d.ts} +3824 -1446
  9. package/dist/{chunk-GOHSXBKW.mjs → chunk-33VCYWVL.mjs} +25 -1
  10. package/dist/{chunk-2WPVUIHX.mjs → chunk-6G3ANKQA.mjs} +1030 -360
  11. package/dist/{chunk-E4K2VQDD.js → chunk-BWEIMKLF.js} +541 -16
  12. package/dist/{chunk-IQFFSKE2.js → chunk-DQEXY542.js} +1345 -746
  13. package/dist/{chunk-CNN7Z2IP.js → chunk-EV2EVOYS.js} +27 -0
  14. package/dist/{chunk-YVAEY3VC.js → chunk-HYFA67A7.js} +1034 -364
  15. package/dist/{chunk-5RT4HYMP.mjs → chunk-L5SNEU2O.mjs} +524 -15
  16. package/dist/{chunk-4BKOKHE3.js → chunk-RBNBSQI7.mjs} +217 -19
  17. package/dist/{chunk-H3RATH4B.mjs → chunk-SFUJMS5E.js} +261 -2
  18. package/dist/{chunk-DYIAX3WI.mjs → chunk-U54CWUUT.mjs} +11 -2
  19. package/dist/{chunk-OKADBZAI.mjs → chunk-UFOWPN3D.mjs} +808 -574
  20. package/dist/{chunk-OCCNG6MO.mjs → chunk-V4457OKB.mjs} +1310 -722
  21. package/dist/{chunk-5BDYDZ4U.js → chunk-WXREIBFW.js} +892 -657
  22. package/dist/{chunk-7PDEX54K.js → chunk-XAUSDMMZ.js} +13 -3
  23. package/dist/{client-BVNWjk6W.d.ts → client-DhXeiUrf.d.ts} +2 -2
  24. package/dist/{client-fM716k7G.d.mts → client-mXiUrV4X.d.mts} +2 -2
  25. package/dist/{index-BtcoqrY1.d.ts → index-B0AdZvrM.d.mts} +9 -4
  26. package/dist/{index-D7Tf3v7c.d.mts → index-CK8Nk5uQ.d.ts} +9 -4
  27. package/dist/index-DPWI5XcU.d.ts +8 -0
  28. package/dist/index-DVfUh-ci.d.mts +8 -0
  29. package/dist/index.d.mts +31 -14
  30. package/dist/index.d.ts +31 -14
  31. package/dist/index.js +305 -133
  32. package/dist/index.mjs +6 -6
  33. package/dist/mock/cli.mjs +1442 -386
  34. package/dist/mock/library.js +1168 -352
  35. package/dist/mock/library.mjs +1168 -352
  36. package/dist/mock/msw.js +1168 -352
  37. package/dist/mock/msw.mjs +1168 -352
  38. package/dist/{price-BLIeAb8K.d.ts → price-6hQIIac9.d.ts} +1 -1
  39. package/dist/{price-Czq_cTC_.d.mts → price-BUDv1rI7.d.mts} +1 -1
  40. package/dist/react.d.mts +10 -5
  41. package/dist/react.d.ts +10 -5
  42. package/dist/react.js +227 -197
  43. package/dist/react.mjs +90 -60
  44. package/dist/{server-DKK_D-PP.d.ts → server-DJ0mSs9i.d.mts} +31 -57
  45. package/dist/{server-BkVhOIwn.d.mts → server-DSTX76Ki.d.ts} +31 -57
  46. package/dist/server.d.mts +3 -4
  47. package/dist/server.d.ts +3 -4
  48. package/dist/server.js +11 -11
  49. package/dist/server.mjs +5 -5
  50. package/dist/styles.css +2 -2
  51. package/dist/testing/msw.d.mts +2 -2
  52. package/dist/testing/msw.d.ts +2 -2
  53. package/dist/testing/msw.js +4 -4
  54. package/dist/testing/msw.mjs +3 -3
  55. package/dist/testing/suite.d.mts +5 -6
  56. package/dist/testing/suite.d.ts +5 -6
  57. package/dist/testing/suite.js +29 -28
  58. package/dist/testing/suite.mjs +10 -9
  59. package/dist/testing.d.mts +2177 -14
  60. package/dist/testing.d.ts +2177 -14
  61. package/dist/testing.js +167 -89
  62. package/dist/testing.mjs +24 -10
  63. package/dist/utils.d.mts +3 -4
  64. package/dist/utils.d.ts +3 -4
  65. package/dist/utils.js +35 -31
  66. package/dist/utils.mjs +2 -2
  67. package/package.json +9 -9
  68. package/registry/order-history.json +1 -1
  69. package/registry/order-summary.json +1 -1
  70. package/dist/link-D9iSVxt5.d.mts +0 -308
  71. package/dist/link-D9iSVxt5.d.ts +0 -308
@@ -1,6 +1,333 @@
1
1
  import { messagePreview } from './chunk-U4MW6PYU.mjs';
2
- import { safe, generateIdempotencyKey, err, ok, CatalogueQueries, CartOperations, CheckoutService, OrderQueries, LinkService, AuthService, BusinessService, InventoryService, SchedulingService, LiteService, FxService, createElements } from './chunk-OCCNG6MO.mjs';
3
- import { CimplifyError, ErrorCode, IdempotencyMismatchError, enrichError } from './chunk-H3RATH4B.mjs';
2
+ import { ok, err, safe, generateIdempotencyKey, createDefaultPaymentAuthorizationAdapterRegistry, CatalogueQueries, CartOperations, CheckoutService, OrderQueries, LinkService, AuthService, BusinessService, InventoryService, SchedulingService, LiteService, FxService, createElements } from './chunk-V4457OKB.mjs';
3
+ import { PURCHASE_INTENT_RESOLVE_STATUS, PURCHASE_INTENT_CONFIRM_STATUS, PURCHASE_INTENT_RESOLUTION_KIND, PURCHASE_INTENT_ACTION_DECISION, CimplifyError, PURCHASE_INTENT_ERROR_CODE, ErrorCode, IdempotencyMismatchError, enrichError } from './chunk-RBNBSQI7.mjs';
4
+
5
+ // src/types/observability.ts
6
+ var SESSION_CHANGE_SOURCE = {
7
+ RESPONSE: "response",
8
+ MANUAL: "manual",
9
+ CLEAR: "clear"
10
+ };
11
+
12
+ // src/utils/purchase-intent-resolver.ts
13
+ var DEFAULT_POLL_INTERVAL_MS = 3e3;
14
+ var DEFAULT_MAX_POLL_ATTEMPTS = 60;
15
+ var DEFAULT_MAX_ACTION_STEPS = 4;
16
+ var DEFAULT_MAX_TERMS_REVIEW_STEPS = 4;
17
+ function nonnegativeInteger(value, fallback) {
18
+ if (value === void 0 || !Number.isFinite(value)) return fallback;
19
+ return Math.max(0, Math.floor(value));
20
+ }
21
+ function abortError() {
22
+ const error = new Error("Purchase-intent resolution was cancelled.");
23
+ error.name = "AbortError";
24
+ return error;
25
+ }
26
+ async function abortableWait(milliseconds, signal) {
27
+ if (signal?.aborted) throw abortError();
28
+ if (milliseconds <= 0) return;
29
+ await new Promise((resolve, reject) => {
30
+ const timer = setTimeout(() => {
31
+ signal?.removeEventListener("abort", onAbort);
32
+ resolve();
33
+ }, milliseconds);
34
+ const onAbort = () => {
35
+ clearTimeout(timer);
36
+ reject(abortError());
37
+ };
38
+ signal?.addEventListener("abort", onAbort, { once: true });
39
+ });
40
+ }
41
+ var PurchaseIntentResolver = class {
42
+ constructor(dependencies) {
43
+ this.confirmation = dependencies.confirmation;
44
+ this.wait = dependencies.wait ?? abortableWait;
45
+ }
46
+ async resolve(intentId, options) {
47
+ const pollIntervalMs = nonnegativeInteger(
48
+ options.pollIntervalMs,
49
+ DEFAULT_POLL_INTERVAL_MS
50
+ );
51
+ const maxPollAttempts = nonnegativeInteger(
52
+ options.maxPollAttempts,
53
+ DEFAULT_MAX_POLL_ATTEMPTS
54
+ );
55
+ const maxActionSteps = nonnegativeInteger(options.maxActionSteps, DEFAULT_MAX_ACTION_STEPS);
56
+ const maxTermsReviewSteps = nonnegativeInteger(
57
+ options.maxTermsReviewSteps,
58
+ DEFAULT_MAX_TERMS_REVIEW_STEPS
59
+ );
60
+ let request = options.confirmation;
61
+ let pollAttempts = 0;
62
+ let actionSteps = 0;
63
+ let termsReviewSteps = 0;
64
+ this.ensureActive(options.signal);
65
+ this.notify(options, PURCHASE_INTENT_RESOLVE_STATUS.CONFIRMING);
66
+ let responseResult = options.initialResponse ? ok(options.initialResponse) : await this.confirmation.confirm(intentId, request, { signal: options.signal });
67
+ while (responseResult.ok) {
68
+ this.ensureActive(options.signal);
69
+ const response = responseResult.value;
70
+ switch (response.status) {
71
+ case PURCHASE_INTENT_CONFIRM_STATUS.ORDER_CREATED:
72
+ this.notify(options, PURCHASE_INTENT_RESOLVE_STATUS.COMPLETED, response);
73
+ return ok({ kind: PURCHASE_INTENT_RESOLUTION_KIND.COMPLETED, response });
74
+ case PURCHASE_INTENT_CONFIRM_STATUS.PAYMENT_FAILED:
75
+ this.notify(options, PURCHASE_INTENT_RESOLVE_STATUS.FAILED, response);
76
+ return ok({ kind: PURCHASE_INTENT_RESOLUTION_KIND.FAILED, response });
77
+ case PURCHASE_INTENT_CONFIRM_STATUS.PAYMENT_OUTCOME_UNKNOWN:
78
+ this.notify(options, PURCHASE_INTENT_RESOLVE_STATUS.OUTCOME_UNKNOWN, response);
79
+ return ok({ kind: PURCHASE_INTENT_RESOLUTION_KIND.OUTCOME_UNKNOWN, response });
80
+ case PURCHASE_INTENT_CONFIRM_STATUS.COLLECTION_PROCESSING:
81
+ if (pollAttempts >= maxPollAttempts) {
82
+ return ok({ kind: PURCHASE_INTENT_RESOLUTION_KIND.TIMED_OUT, response });
83
+ }
84
+ pollAttempts += 1;
85
+ this.notify(options, PURCHASE_INTENT_RESOLVE_STATUS.POLLING, response);
86
+ await this.wait(pollIntervalMs, options.signal);
87
+ responseResult = await this.confirmation.confirm(intentId, request, {
88
+ signal: options.signal
89
+ });
90
+ continue;
91
+ case PURCHASE_INTENT_CONFIRM_STATUS.REDIRECT_REQUIRED:
92
+ case PURCHASE_INTENT_CONFIRM_STATUS.CARD_POPUP_REQUIRED:
93
+ case PURCHASE_INTENT_CONFIRM_STATUS.AUTHORIZATION_REQUIRED: {
94
+ this.notify(options, PURCHASE_INTENT_RESOLVE_STATUS.AWAITING_ACTION, response);
95
+ if (!options.onActionRequired || actionSteps >= maxActionSteps) {
96
+ return ok({ kind: PURCHASE_INTENT_RESOLUTION_KIND.ACTION_REQUIRED, response });
97
+ }
98
+ actionSteps += 1;
99
+ const decision = await options.onActionRequired(response);
100
+ if (decision === PURCHASE_INTENT_ACTION_DECISION.STOP) {
101
+ return ok({ kind: PURCHASE_INTENT_RESOLUTION_KIND.ACTION_REQUIRED, response });
102
+ }
103
+ if (typeof decision === "object" && decision.kind === PURCHASE_INTENT_ACTION_DECISION.SUBMIT) {
104
+ if (!this.confirmation.submitAction) {
105
+ return err(
106
+ new CimplifyError(
107
+ PURCHASE_INTENT_ERROR_CODE.ACTION_UNSUPPORTED,
108
+ "This confirmation transport cannot submit purchase-intent actions.",
109
+ false
110
+ )
111
+ );
112
+ }
113
+ responseResult = await this.confirmation.submitAction(
114
+ intentId,
115
+ response.attempt_id,
116
+ decision.request,
117
+ { signal: options.signal }
118
+ );
119
+ continue;
120
+ }
121
+ if (decision !== PURCHASE_INTENT_ACTION_DECISION.CONTINUE) {
122
+ return err(
123
+ new CimplifyError(
124
+ PURCHASE_INTENT_ERROR_CODE.ACTION_UNSUPPORTED,
125
+ "The action callback returned an unsupported decision.",
126
+ false
127
+ )
128
+ );
129
+ }
130
+ await this.wait(pollIntervalMs, options.signal);
131
+ responseResult = await this.confirmation.confirm(intentId, request, {
132
+ signal: options.signal
133
+ });
134
+ continue;
135
+ }
136
+ case PURCHASE_INTENT_CONFIRM_STATUS.TERMS_CHANGED: {
137
+ this.notify(options, PURCHASE_INTENT_RESOLVE_STATUS.REVIEWING_TERMS, response);
138
+ if (!options.onTermsChanged || termsReviewSteps >= maxTermsReviewSteps) {
139
+ return ok({ kind: PURCHASE_INTENT_RESOLUTION_KIND.REVIEW_REQUIRED, response });
140
+ }
141
+ termsReviewSteps += 1;
142
+ const replacementRequest = await options.onTermsChanged(response);
143
+ if (!replacementRequest) {
144
+ return ok({ kind: PURCHASE_INTENT_RESOLUTION_KIND.REVIEW_REQUIRED, response });
145
+ }
146
+ request = replacementRequest;
147
+ responseResult = await this.confirmation.confirm(intentId, request, {
148
+ signal: options.signal
149
+ });
150
+ continue;
151
+ }
152
+ case PURCHASE_INTENT_CONFIRM_STATUS.SELECTION_UNAVAILABLE:
153
+ case PURCHASE_INTENT_CONFIRM_STATUS.OFFER_ENDED:
154
+ case PURCHASE_INTENT_CONFIRM_STATUS.SOURCE_UNAVAILABLE:
155
+ return ok({ kind: PURCHASE_INTENT_RESOLUTION_KIND.REVIEW_REQUIRED, response });
156
+ }
157
+ }
158
+ return responseResult;
159
+ }
160
+ ensureActive(signal) {
161
+ if (signal?.aborted) throw abortError();
162
+ }
163
+ notify(options, status, response) {
164
+ try {
165
+ options.onStatusChange?.(status, response);
166
+ } catch {
167
+ }
168
+ }
169
+ };
170
+
171
+ // src/purchase-intent.ts
172
+ var PURCHASE_INTENT_ROUTE = {
173
+ DIRECT_COLLECTION: "/api/v1/purchase-intents",
174
+ HOSTED_SESSION_COLLECTION: "/v1/checkout/sessions",
175
+ PREPARE: "prepare",
176
+ REFRESH: "refresh",
177
+ CONFIRM: "confirm",
178
+ REPLAY: "replay",
179
+ ATTEMPTS: "attempts",
180
+ ACTIONS: "actions"
181
+ };
182
+ function directIntentPath(intentId) {
183
+ const normalized = intentId.trim();
184
+ if (!normalized) throw new TypeError("intentId must not be empty");
185
+ return `${PURCHASE_INTENT_ROUTE.DIRECT_COLLECTION}/${encodeURIComponent(normalized)}`;
186
+ }
187
+ function hostedSessionPath(sessionId) {
188
+ const normalized = sessionId.trim();
189
+ if (!normalized) throw new TypeError("sessionId must not be empty");
190
+ return `${PURCHASE_INTENT_ROUTE.HOSTED_SESSION_COLLECTION}/${encodeURIComponent(normalized)}`;
191
+ }
192
+ var PurchaseIntentService = class {
193
+ constructor(transport) {
194
+ this.transport = transport;
195
+ this.resolver = new PurchaseIntentResolver({ confirmation: this });
196
+ }
197
+ mint(request, options) {
198
+ return safe(
199
+ this.transport.post(PURCHASE_INTENT_ROUTE.DIRECT_COLLECTION, request, {
200
+ idempotencyKey: request.client_intent_key,
201
+ signal: options?.signal,
202
+ timeoutMs: options?.timeoutMs
203
+ })
204
+ );
205
+ }
206
+ get(intentId, options) {
207
+ return safe(
208
+ this.transport.get(this.intentPath(intentId), {
209
+ signal: options?.signal,
210
+ timeoutMs: options?.timeoutMs
211
+ })
212
+ );
213
+ }
214
+ refresh(intentId, expectedRevision, options) {
215
+ const request = {
216
+ expected_intent_revision: expectedRevision
217
+ };
218
+ return safe(
219
+ this.transport.post(
220
+ `${this.intentPath(intentId)}/${PURCHASE_INTENT_ROUTE.REFRESH}`,
221
+ request,
222
+ {
223
+ signal: options?.signal,
224
+ timeoutMs: options?.timeoutMs
225
+ }
226
+ )
227
+ );
228
+ }
229
+ confirm(intentId, request, options) {
230
+ return safe(
231
+ this.transport.post(
232
+ `${this.intentPath(intentId)}/${PURCHASE_INTENT_ROUTE.CONFIRM}`,
233
+ request,
234
+ {
235
+ idempotencyKey: request.idempotency_key,
236
+ signal: options?.signal,
237
+ timeoutMs: options?.timeoutMs
238
+ }
239
+ )
240
+ );
241
+ }
242
+ submitAction(intentId, attemptId, request, options) {
243
+ const normalizedAttemptId = attemptId.trim();
244
+ if (!normalizedAttemptId) throw new TypeError("attemptId must not be empty");
245
+ return safe(
246
+ this.transport.post(
247
+ `${this.intentPath(intentId)}/${PURCHASE_INTENT_ROUTE.ATTEMPTS}/${encodeURIComponent(normalizedAttemptId)}/${PURCHASE_INTENT_ROUTE.ACTIONS}`,
248
+ request,
249
+ {
250
+ signal: options?.signal,
251
+ timeoutMs: options?.timeoutMs
252
+ }
253
+ )
254
+ );
255
+ }
256
+ resolve(intentId, options) {
257
+ return this.resolver.resolve(intentId, options);
258
+ }
259
+ /**
260
+ * Bind subsequent preparation/confirmation to one hosted checkout-session
261
+ * owner. The session ID is a route authority, not an intent-owner rewrite.
262
+ */
263
+ forHostedSession(sessionId) {
264
+ return new HostedPurchaseIntentService(this.transport, sessionId);
265
+ }
266
+ intentPath(intentId) {
267
+ return directIntentPath(intentId);
268
+ }
269
+ };
270
+ var HostedPurchaseIntentService = class {
271
+ constructor(transport, sessionId) {
272
+ this.transport = transport;
273
+ this.sessionPath = hostedSessionPath(sessionId);
274
+ this.resolver = new PurchaseIntentResolver({ confirmation: this });
275
+ }
276
+ prepare(request, options) {
277
+ return safe(
278
+ this.transport.post(
279
+ `${this.sessionPath}/${PURCHASE_INTENT_ROUTE.PREPARE}`,
280
+ request,
281
+ {
282
+ idempotencyKey: request.idempotency_key,
283
+ signal: options?.signal,
284
+ timeoutMs: options?.timeoutMs
285
+ }
286
+ )
287
+ );
288
+ }
289
+ confirm(_intentId, request, options) {
290
+ return safe(
291
+ this.transport.post(
292
+ `${this.sessionPath}/${PURCHASE_INTENT_ROUTE.CONFIRM}`,
293
+ request,
294
+ {
295
+ idempotencyKey: request.idempotency_key,
296
+ signal: options?.signal,
297
+ timeoutMs: options?.timeoutMs
298
+ }
299
+ )
300
+ );
301
+ }
302
+ replay(options) {
303
+ return safe(
304
+ this.transport.get(
305
+ `${this.sessionPath}/${PURCHASE_INTENT_ROUTE.REPLAY}`,
306
+ {
307
+ signal: options?.signal,
308
+ timeoutMs: options?.timeoutMs
309
+ }
310
+ )
311
+ );
312
+ }
313
+ submitAction(_intentId, attemptId, request, options) {
314
+ const normalized = attemptId.trim();
315
+ if (!normalized) throw new TypeError("attemptId must not be empty");
316
+ return safe(
317
+ this.transport.post(
318
+ `${this.sessionPath}/${PURCHASE_INTENT_ROUTE.ATTEMPTS}/${encodeURIComponent(normalized)}/${PURCHASE_INTENT_ROUTE.ACTIONS}`,
319
+ request,
320
+ {
321
+ signal: options?.signal,
322
+ timeoutMs: options?.timeoutMs
323
+ }
324
+ )
325
+ );
326
+ }
327
+ resolve(intentId, options) {
328
+ return this.resolver.resolve(intentId, options);
329
+ }
330
+ };
4
331
 
5
332
  // src/store-credit.ts
6
333
  var StoreCreditQueries = class {
@@ -18,12 +345,13 @@ var StoreCreditQueries = class {
18
345
 
19
346
  // src/internal/analytics-session.ts
20
347
  var ANALYTICS_SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
21
- var ANALYTICS_SESSION_STORAGE_KEY = "cimplify_analytics_session";
22
- var ANALYTICS_SESSION_STATE_VERSION = 1;
348
+ var ANALYTICS_SESSION_STORAGE_KEY_PREFIX = "cimplify_analytics_session:v2:";
349
+ var ANALYTICS_SESSION_STATE_VERSION = 2;
23
350
  var MICROSECONDS_PER_MILLISECOND = 1e3;
24
351
  var MICROSECONDS_PER_SECOND = 1e6;
25
352
  var AnalyticsSessionCoordinator = class {
26
- constructor() {
353
+ constructor(identity) {
354
+ this.identity = identity;
27
355
  this.fallbackState = null;
28
356
  }
29
357
  beginEvent(nowMs = Date.now()) {
@@ -61,7 +389,7 @@ var AnalyticsSessionCoordinator = class {
61
389
  reset() {
62
390
  this.fallbackState = null;
63
391
  try {
64
- sessionStorage.removeItem(ANALYTICS_SESSION_STORAGE_KEY);
392
+ sessionStorage.removeItem(this.storageKey());
65
393
  } catch {
66
394
  }
67
395
  }
@@ -71,11 +399,11 @@ var AnalyticsSessionCoordinator = class {
71
399
  }
72
400
  read() {
73
401
  try {
74
- const serialized = sessionStorage.getItem(ANALYTICS_SESSION_STORAGE_KEY);
402
+ const serialized = sessionStorage.getItem(this.storageKey());
75
403
  if (!serialized) return this.fallbackState;
76
404
  const state = JSON.parse(serialized);
77
405
  if (!isPersistedSession(state)) {
78
- sessionStorage.removeItem(ANALYTICS_SESSION_STORAGE_KEY);
406
+ sessionStorage.removeItem(this.storageKey());
79
407
  return this.fallbackState;
80
408
  }
81
409
  this.fallbackState = state;
@@ -87,10 +415,13 @@ var AnalyticsSessionCoordinator = class {
87
415
  write(state) {
88
416
  this.fallbackState = state;
89
417
  try {
90
- sessionStorage.setItem(ANALYTICS_SESSION_STORAGE_KEY, JSON.stringify(state));
418
+ sessionStorage.setItem(this.storageKey(), JSON.stringify(state));
91
419
  } catch {
92
420
  }
93
421
  }
422
+ storageKey() {
423
+ return `${ANALYTICS_SESSION_STORAGE_KEY_PREFIX}${encodeURIComponent(this.identity)}`;
424
+ }
94
425
  };
95
426
  function newSession(nowMs) {
96
427
  return {
@@ -157,11 +488,127 @@ function generateSessionId() {
157
488
 
158
489
  // src/activity.ts
159
490
  var VISITOR_ID_STORAGE_KEY = "cimplify_visitor_id";
491
+ var ACTIVITY_RETRY_POLICY = {
492
+ maxRetries: 1,
493
+ jitterRatio: 0.2
494
+ };
495
+ var PAGE_VIEW_TRACKER_KEY = /* @__PURE__ */ Symbol.for("@cimplify/sdk.page-view-tracker-hub");
496
+ var PAGE_VIEW_TRACKER_VERSION = 2;
497
+ function trackerHost() {
498
+ return window;
499
+ }
500
+ function currentWindowTrackerHub() {
501
+ const candidate = trackerHost()[PAGE_VIEW_TRACKER_KEY];
502
+ return candidate?.version === PAGE_VIEW_TRACKER_VERSION && candidate.registrations instanceof Map && typeof candidate.recordAll === "function" ? candidate : null;
503
+ }
504
+ function weakReference(target) {
505
+ const WeakReference = globalThis.WeakRef;
506
+ if (WeakReference) return new WeakReference(target);
507
+ return { deref: () => target };
508
+ }
509
+ function latestLiveOwner(registration) {
510
+ let latest = null;
511
+ for (const [token, reference] of registration.owners) {
512
+ const owner = reference.deref();
513
+ if (!owner || owner.released) {
514
+ registration.owners.delete(token);
515
+ } else {
516
+ latest = owner;
517
+ }
518
+ }
519
+ return latest;
520
+ }
521
+ function recordRegistration(registration, path) {
522
+ if (path === registration.lastPath) return;
523
+ const owner = registration.activeOwner && !registration.activeOwner.released ? registration.activeOwner : latestLiveOwner(registration);
524
+ if (!owner) return;
525
+ registration.activeOwner = owner;
526
+ registration.lastPath = path;
527
+ owner.recorder(path);
528
+ }
529
+ function createWindowTrackerHub() {
530
+ const originalPushState = window.history.pushState;
531
+ const originalReplaceState = window.history.replaceState;
532
+ const hub = {
533
+ version: PAGE_VIEW_TRACKER_VERSION,
534
+ registrations: /* @__PURE__ */ new Map(),
535
+ originalPushState,
536
+ originalReplaceState
537
+ };
538
+ hub.recordAll = () => {
539
+ const path = window.location.pathname;
540
+ for (const registration of hub.registrations.values()) {
541
+ recordRegistration(registration, path);
542
+ }
543
+ };
544
+ hub.patchedPushState = function(...args) {
545
+ originalPushState.apply(this, args);
546
+ hub.recordAll();
547
+ };
548
+ hub.patchedReplaceState = function(...args) {
549
+ originalReplaceState.apply(this, args);
550
+ hub.recordAll();
551
+ };
552
+ hub.popstateListener = () => hub.recordAll();
553
+ window.history.pushState = hub.patchedPushState;
554
+ window.history.replaceState = hub.patchedReplaceState;
555
+ window.addEventListener("popstate", hub.popstateListener);
556
+ trackerHost()[PAGE_VIEW_TRACKER_KEY] = hub;
557
+ return hub;
558
+ }
559
+ function pageViewTrackerIdentity(client) {
560
+ return JSON.stringify([client.getBaseUrl(), client.getPublicKey().trim()]);
561
+ }
562
+ function acquireWindowPageViewTracker(identity, recorder) {
563
+ const hub = currentWindowTrackerHub() ?? createWindowTrackerHub();
564
+ let registration = hub.registrations.get(identity);
565
+ const isNewIdentity = !registration;
566
+ if (!registration) {
567
+ registration = { owners: /* @__PURE__ */ new Map(), activeOwner: null, lastPath: null };
568
+ hub.registrations.set(identity, registration);
569
+ }
570
+ const owner = {
571
+ token: Symbol(identity),
572
+ recorder,
573
+ released: false
574
+ };
575
+ registration.owners.set(owner.token, weakReference(owner));
576
+ registration.activeOwner = owner;
577
+ if (isNewIdentity) {
578
+ recordRegistration(registration, window.location.pathname);
579
+ }
580
+ let released = false;
581
+ return () => {
582
+ if (released) return;
583
+ released = true;
584
+ owner.released = true;
585
+ registration.owners.delete(owner.token);
586
+ if (registration.activeOwner === owner) {
587
+ registration.activeOwner = latestLiveOwner(registration);
588
+ }
589
+ if (registration.activeOwner) return;
590
+ if (hub.registrations.get(identity) === registration) {
591
+ hub.registrations.delete(identity);
592
+ }
593
+ if (hub.registrations.size !== 0) return;
594
+ window.removeEventListener("popstate", hub.popstateListener);
595
+ if (window.history.pushState === hub.patchedPushState) {
596
+ window.history.pushState = hub.originalPushState;
597
+ }
598
+ if (window.history.replaceState === hub.patchedReplaceState) {
599
+ window.history.replaceState = hub.originalReplaceState;
600
+ }
601
+ if (currentWindowTrackerHub() === hub) {
602
+ delete trackerHost()[PAGE_VIEW_TRACKER_KEY];
603
+ }
604
+ };
605
+ }
160
606
  var ActivityService = class {
161
607
  constructor(client) {
162
608
  this.client = client;
163
- this.sessions = new AnalyticsSessionCoordinator();
609
+ this.localGeneration = 0;
164
610
  this.bootstrapRequest = null;
611
+ this.sessions = this.createSessionCoordinator();
165
612
  }
166
613
  /**
167
614
  * Record session start (landing, referrer, UTM, device). Fire-and-forget,
@@ -171,7 +618,7 @@ var ActivityService = class {
171
618
  startSession() {
172
619
  if (typeof window === "undefined") return;
173
620
  const session = this.sessions.beginEvent();
174
- void this.bootstrapSession(session).catch(() => {
621
+ void this.bootstrapSession(session, false, this.captureGeneration()).catch(() => {
175
622
  });
176
623
  }
177
624
  /** Rebind an existing analytics session after the server authenticates a customer. */
@@ -179,13 +626,19 @@ var ActivityService = class {
179
626
  if (typeof window === "undefined") return;
180
627
  const session = this.sessions.current();
181
628
  if (!session) return;
182
- void this.bootstrapSession(session, true).catch(() => {
629
+ void this.bootstrapSession(session, true, this.captureGeneration()).catch(() => {
183
630
  });
184
631
  }
185
632
  /** A signed-out browser starts a fresh anonymous analytics session. */
186
633
  resetSession() {
634
+ this.invalidatePendingActivity();
187
635
  this.sessions.reset();
188
- this.bootstrapRequest = null;
636
+ }
637
+ /** Drop the old storefront's analytics state before binding this service to the new one. */
638
+ resetForStorefrontIdentityChange() {
639
+ this.invalidatePendingActivity();
640
+ this.sessions.reset();
641
+ this.sessions = this.createSessionCoordinator();
189
642
  }
190
643
  buildSessionPayload(session) {
191
644
  const params = new URLSearchParams(window.location.search);
@@ -281,32 +734,12 @@ var ActivityService = class {
281
734
  autoTrackPageViews() {
282
735
  if (typeof window === "undefined") return () => {
283
736
  };
284
- let lastPath = null;
285
- const record = () => {
286
- const path = window.location.pathname;
287
- if (path === lastPath) return;
288
- lastPath = path;
737
+ return acquireWindowPageViewTracker(pageViewTrackerIdentity(this.client), (path) => {
289
738
  try {
290
739
  this.trackPageView({ pagePath: path });
291
740
  } catch {
292
741
  }
293
- };
294
- const { pushState, replaceState } = window.history;
295
- window.history.pushState = function(...args) {
296
- pushState.apply(this, args);
297
- record();
298
- };
299
- window.history.replaceState = function(...args) {
300
- replaceState.apply(this, args);
301
- record();
302
- };
303
- window.addEventListener("popstate", record);
304
- record();
305
- return () => {
306
- window.history.pushState = pushState;
307
- window.history.replaceState = replaceState;
308
- window.removeEventListener("popstate", record);
309
- };
742
+ });
310
743
  }
311
744
  sendEvents(events, session) {
312
745
  if (typeof window === "undefined") return;
@@ -314,29 +747,59 @@ var ActivityService = class {
314
747
  });
315
748
  }
316
749
  async postEvents(events, session) {
317
- await this.bootstrapSession(session);
318
- await this.client.post("/api/v1/activity/events", {
319
- analytics_session_id: session.id,
320
- occurred_at: session.occurredAt,
321
- events
322
- });
750
+ const generation = this.captureGeneration();
751
+ await this.bootstrapSession(session, false, generation);
752
+ if (!this.isCurrentGeneration(generation)) return;
753
+ await this.client.post(
754
+ "/api/v1/activity/events",
755
+ {
756
+ analytics_session_id: session.id,
757
+ occurred_at: session.occurredAt,
758
+ events
759
+ },
760
+ { retryPolicy: ACTIVITY_RETRY_POLICY }
761
+ );
323
762
  }
324
- bootstrapSession(session, force = false) {
763
+ bootstrapSession(session, force = false, generation = this.captureGeneration()) {
325
764
  if (!force && !session.requiresBootstrap) return Promise.resolve();
326
- if (!force && this.bootstrapRequest?.sessionId === session.id) {
765
+ if (!force && this.bootstrapRequest?.sessionId === session.id && sameActivityGeneration(this.bootstrapRequest.generation, generation)) {
327
766
  return this.bootstrapRequest.promise;
328
767
  }
329
- const promise = this.client.post("/api/v1/activity/session", this.buildSessionPayload(session)).then(() => {
330
- this.sessions.markBootstrapped(session.id);
768
+ const promise = this.client.post("/api/v1/activity/session", this.buildSessionPayload(session), {
769
+ retryPolicy: ACTIVITY_RETRY_POLICY
770
+ }).then(() => {
771
+ if (this.isCurrentGeneration(generation)) {
772
+ this.sessions.markBootstrapped(session.id);
773
+ }
331
774
  }).finally(() => {
332
775
  if (this.bootstrapRequest?.promise === promise) {
333
776
  this.bootstrapRequest = null;
334
777
  }
335
778
  });
336
- this.bootstrapRequest = { sessionId: session.id, promise };
779
+ this.bootstrapRequest = { sessionId: session.id, generation, promise };
337
780
  return promise;
338
781
  }
782
+ captureGeneration() {
783
+ return {
784
+ local: this.localGeneration,
785
+ publicKey: this.client.getPublicKeyEpoch(),
786
+ session: this.client.getSessionEpoch()
787
+ };
788
+ }
789
+ isCurrentGeneration(generation) {
790
+ return sameActivityGeneration(generation, this.captureGeneration());
791
+ }
792
+ invalidatePendingActivity() {
793
+ this.localGeneration += 1;
794
+ this.bootstrapRequest = null;
795
+ }
796
+ createSessionCoordinator() {
797
+ return new AnalyticsSessionCoordinator(pageViewTrackerIdentity(this.client));
798
+ }
339
799
  };
800
+ function sameActivityGeneration(left, right) {
801
+ return left.local === right.local && left.publicKey === right.publicKey && left.session === right.session;
802
+ }
340
803
  function currentPath() {
341
804
  if (typeof window !== "undefined") {
342
805
  return window.location.pathname;
@@ -849,459 +1312,108 @@ var SupportService = class {
849
1312
  }
850
1313
  };
851
1314
 
852
- // src/payment-authorization/providers/paystack.ts
853
- async function openPaystackPopup(options, signal) {
854
- if (typeof window === "undefined") {
855
- return { success: false, error: "PROVIDER_UNAVAILABLE" };
856
- }
857
- let PaystackPop;
858
- try {
859
- const imported = await import('@paystack/inline-js');
860
- PaystackPop = imported.default;
861
- } catch {
862
- return { success: false, error: "PROVIDER_UNAVAILABLE" };
863
- }
864
- return new Promise((resolve) => {
865
- let settled = false;
866
- let providerStarted = false;
867
- const resolveOnce = (result) => {
868
- if (settled) {
869
- return;
870
- }
871
- settled = true;
872
- if (signal) {
873
- signal.removeEventListener("abort", onAbort);
874
- }
875
- resolve(result);
876
- };
877
- const onAbort = () => {
878
- resolveOnce({
879
- success: false,
880
- // Once Paystack owns the transaction, cancellation is ambiguous. The
881
- // adapter converts this into submitted so Cimplify polling decides.
882
- error: providerStarted ? "SUBMISSION_UNKNOWN" : "CANCELLED"
883
- });
884
- };
885
- if (signal?.aborted) {
886
- resolveOnce({ success: false, error: "CANCELLED" });
887
- return;
888
- }
889
- if (signal) {
890
- signal.addEventListener("abort", onAbort, { once: true });
891
- }
892
- try {
893
- const popup = new PaystackPop();
894
- popup.newTransaction({
895
- key: options.key,
896
- email: options.email,
897
- amount: options.amount,
898
- currency: options.currency,
899
- reference: options.reference,
900
- accessCode: options.accessCode,
901
- onSuccess: (transaction) => {
902
- resolveOnce({
903
- success: true,
904
- reference: transaction.reference ?? options.reference
905
- });
906
- },
907
- onCancel: () => {
908
- resolveOnce({ success: false, error: "PAYMENT_CANCELLED" });
909
- },
910
- onError: (error) => {
911
- resolveOnce({
912
- success: false,
913
- error: error?.message || "PAYMENT_FAILED"
914
- });
915
- }
916
- });
917
- providerStarted = true;
918
- } catch {
919
- resolveOnce({ success: false, error: "POPUP_BLOCKED" });
920
- }
921
- });
1315
+ // src/internal/cross-tab-session.ts
1316
+ var CROSS_TAB_SESSION_HUB_KEY = /* @__PURE__ */ Symbol.for("@cimplify/sdk.cross-tab-session-hub");
1317
+ var SESSION_SYNC_STORAGE_KEY = "cimplify_session_sync";
1318
+ var MAX_RECENT_ENVELOPES = 64;
1319
+ function hubHost() {
1320
+ return window;
922
1321
  }
923
- var PaystackAuthorizationAdapter = class {
924
- constructor(openPopup = openPaystackPopup) {
925
- this.openPopup = openPopup;
926
- this.provider = "paystack";
927
- }
928
- async authorize(context) {
929
- const result = await this.openPopup(
930
- {
931
- key: context.action.public_key,
932
- email: context.email,
933
- accessCode: context.action.client_secret,
934
- reference: context.reference || context.action.client_secret,
935
- currency: context.currency
936
- },
937
- context.signal
938
- );
939
- if (result.success) {
940
- return { status: "submitted", reference: result.reference || context.reference };
941
- }
942
- if (result.error === "SUBMISSION_UNKNOWN") {
943
- return { status: "submitted", reference: context.reference };
944
- }
945
- if (!result.error || result.error === "CANCELLED" || result.error === "PAYMENT_CANCELLED") {
946
- return { status: "cancelled" };
947
- }
948
- if (result.error === "POPUP_BLOCKED") {
949
- return {
950
- status: "failed",
951
- code: "POPUP_BLOCKED",
952
- message: "Unable to open the Paystack payment popup. Please allow popups and try again.",
953
- recoverable: true
954
- };
955
- }
956
- if (result.error === "PROVIDER_UNAVAILABLE") {
957
- return {
958
- status: "failed",
959
- code: "PROVIDER_UNAVAILABLE",
960
- message: "Paystack is unavailable in this environment.",
961
- recoverable: false
962
- };
963
- }
964
- return {
965
- status: "failed",
966
- code: "PAYMENT_FAILED",
967
- message: result.error || "Paystack payment could not be submitted.",
968
- recoverable: true
969
- };
1322
+ function currentHub() {
1323
+ const candidate = hubHost()[CROSS_TAB_SESSION_HUB_KEY];
1324
+ return candidate?.subscribers instanceof Map && candidate.recentEnvelopes instanceof Set && typeof candidate.storageListener === "function" && typeof candidate.dispatch === "function" ? candidate : null;
1325
+ }
1326
+ function subscriberReference(target) {
1327
+ const WeakReference = globalThis.WeakRef;
1328
+ if (WeakReference) return new WeakReference(target);
1329
+ return { deref: () => target };
1330
+ }
1331
+ function pruneSubscribers(hub) {
1332
+ for (const [token, reference] of hub.subscribers) {
1333
+ if (!reference.deref()) hub.subscribers.delete(token);
970
1334
  }
971
- };
972
-
973
- // src/payment-authorization/providers/stripe.ts
974
- async function loadStripeFromPeer(publicKey, options) {
1335
+ }
1336
+ function parseSessionSyncPayload(serialized) {
1337
+ if (!serialized) return null;
975
1338
  try {
976
- const imported = await import('@stripe/stripe-js');
977
- return imported.loadStripe ? imported.loadStripe(publicKey, options) : null;
1339
+ const payload = JSON.parse(serialized);
1340
+ return (payload.type === "claimed" || payload.type === "cleared") && typeof payload.token === "string" && payload.token.length > 0 && typeof payload.at === "number" && Number.isFinite(payload.at) && (payload.name === void 0 || payload.name === null || typeof payload.name === "string") ? payload : null;
978
1341
  } catch {
979
1342
  return null;
980
1343
  }
981
1344
  }
982
- function createStripeModal() {
983
- if (typeof document === "undefined" || !document.body) return null;
984
- const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
985
- const root = document.createElement("div");
986
- root.setAttribute("role", "dialog");
987
- root.setAttribute("aria-modal", "true");
988
- root.setAttribute("aria-label", "Secure card payment");
989
- Object.assign(root.style, {
990
- position: "fixed",
991
- inset: "0",
992
- zIndex: "2147483647",
993
- display: "grid",
994
- placeItems: "center",
995
- padding: "16px",
996
- background: "rgba(15, 23, 42, 0.58)"
997
- });
998
- const form = document.createElement("form");
999
- Object.assign(form.style, {
1000
- width: "min(100%, 480px)",
1001
- maxHeight: "calc(100vh - 32px)",
1002
- overflow: "auto",
1003
- borderRadius: "12px",
1004
- padding: "24px",
1005
- background: "#fff",
1006
- boxShadow: "0 24px 80px rgba(15, 23, 42, 0.32)"
1007
- });
1008
- const title = document.createElement("h2");
1009
- title.textContent = "Complete card payment";
1010
- Object.assign(title.style, { margin: "0 0 18px", font: "600 18px/1.4 system-ui" });
1011
- const paymentMount = document.createElement("div");
1012
- paymentMount.setAttribute("data-cimplify-stripe-payment-element", "");
1013
- const error = document.createElement("p");
1014
- error.setAttribute("role", "alert");
1015
- Object.assign(error.style, {
1016
- minHeight: "20px",
1017
- margin: "14px 0 0",
1018
- color: "#b91c1c",
1019
- font: "14px/1.4 system-ui"
1020
- });
1021
- const actions = document.createElement("div");
1022
- Object.assign(actions.style, {
1023
- display: "flex",
1024
- justifyContent: "flex-end",
1025
- gap: "10px",
1026
- marginTop: "18px"
1027
- });
1028
- const cancelButton = document.createElement("button");
1029
- cancelButton.type = "button";
1030
- cancelButton.textContent = "Cancel";
1031
- Object.assign(cancelButton.style, {
1032
- border: "1px solid #cbd5e1",
1033
- borderRadius: "8px",
1034
- padding: "10px 16px",
1035
- background: "#fff",
1036
- color: "#0f172a",
1037
- cursor: "pointer"
1038
- });
1039
- const submitButton = document.createElement("button");
1040
- submitButton.type = "submit";
1041
- submitButton.textContent = "Pay securely";
1042
- Object.assign(submitButton.style, {
1043
- border: "0",
1044
- borderRadius: "8px",
1045
- padding: "10px 16px",
1046
- background: "#0f172a",
1047
- color: "#fff",
1048
- cursor: "pointer"
1049
- });
1050
- actions.append(cancelButton, submitButton);
1051
- form.append(title, paymentMount, error, actions);
1052
- root.append(form);
1053
- document.body.append(root);
1054
- cancelButton.focus();
1055
- return { root, form, paymentMount, submitButton, cancelButton, error, previousFocus };
1345
+ function sessionSyncEnvelopeKey(payload) {
1346
+ const name = payload.name === void 0 ? ["missing"] : ["present", payload.name];
1347
+ return JSON.stringify([payload.type, payload.token, name, payload.at]);
1056
1348
  }
1057
- function stripeFailure(error) {
1058
- const message = error?.message || "Stripe could not authorize this payment.";
1059
- if (error?.code === "payment_intent_authentication_failure" || error?.type === "invalid_request_error") {
1060
- return {
1061
- status: "failed",
1062
- code: "AUTHORIZATION_FAILED",
1063
- message,
1064
- recoverable: true
1065
- };
1066
- }
1067
- return { status: "failed", code: "PAYMENT_FAILED", message, recoverable: true };
1349
+ function rememberEnvelope(hub, envelope) {
1350
+ if (hub.recentEnvelopes.has(envelope)) return false;
1351
+ hub.recentEnvelopes.add(envelope);
1352
+ if (hub.recentEnvelopes.size > MAX_RECENT_ENVELOPES) {
1353
+ const oldest = hub.recentEnvelopes.values().next().value;
1354
+ if (oldest) hub.recentEnvelopes.delete(oldest);
1355
+ }
1356
+ return true;
1068
1357
  }
1069
- var StripeAuthorizationAdapter = class {
1070
- constructor(loadStripe = loadStripeFromPeer) {
1071
- this.loadStripe = loadStripe;
1072
- this.provider = "stripe";
1073
- }
1074
- async authorize(context) {
1075
- if (typeof window === "undefined" || typeof document === "undefined" || context.signal?.aborted) {
1076
- return context.signal?.aborted ? { status: "cancelled" } : {
1077
- status: "failed",
1078
- code: "PROVIDER_UNAVAILABLE",
1079
- message: "Stripe Payment Element is unavailable in this environment.",
1080
- recoverable: false
1081
- };
1082
- }
1083
- let stripe;
1084
- try {
1085
- stripe = await this.loadStripe(
1086
- context.action.public_key,
1087
- context.action.provider_account_id ? { stripeAccount: context.action.provider_account_id } : void 0
1088
- );
1089
- } catch (error) {
1090
- return {
1091
- status: "failed",
1092
- code: "PROVIDER_UNAVAILABLE",
1093
- message: error instanceof Error ? error.message : "Stripe.js could not be loaded.",
1094
- recoverable: true
1095
- };
1096
- }
1097
- if (context.signal?.aborted) {
1098
- return { status: "cancelled" };
1099
- }
1100
- if (!stripe) {
1101
- return {
1102
- status: "failed",
1103
- code: "PROVIDER_UNAVAILABLE",
1104
- message: "Stripe.js could not be loaded.",
1105
- recoverable: false
1106
- };
1107
- }
1108
- const modal = createStripeModal();
1109
- if (!modal) {
1110
- return {
1111
- status: "failed",
1112
- code: "PROVIDER_UNAVAILABLE",
1113
- message: "Stripe Payment Element could not be mounted.",
1114
- recoverable: false
1115
- };
1116
- }
1117
- let paymentElement;
1118
- let elements;
1119
- try {
1120
- elements = stripe.elements({ clientSecret: context.action.client_secret });
1121
- paymentElement = elements.create("payment");
1122
- paymentElement.mount(modal.paymentMount);
1123
- } catch {
1124
- modal.root.remove();
1125
- modal.previousFocus?.focus();
1126
- return {
1127
- status: "failed",
1128
- code: "PROVIDER_UNAVAILABLE",
1129
- message: "Stripe Payment Element could not be initialized.",
1130
- recoverable: false
1131
- };
1358
+ function createHub() {
1359
+ const hub = {
1360
+ subscribers: /* @__PURE__ */ new Map(),
1361
+ recentEnvelopes: /* @__PURE__ */ new Set()
1362
+ };
1363
+ hub.dispatch = (payload) => {
1364
+ if (!rememberEnvelope(hub, sessionSyncEnvelopeKey(payload))) return;
1365
+ for (const [token, reference] of hub.subscribers) {
1366
+ const subscriber = reference.deref();
1367
+ if (!subscriber) {
1368
+ hub.subscribers.delete(token);
1369
+ continue;
1370
+ }
1371
+ try {
1372
+ subscriber.notify(payload);
1373
+ } catch {
1374
+ }
1132
1375
  }
1133
- if (context.signal?.aborted) {
1134
- if (paymentElement.destroy) paymentElement.destroy();
1135
- else paymentElement.unmount?.();
1136
- modal.root.remove();
1137
- modal.previousFocus?.focus();
1138
- return { status: "cancelled" };
1139
- }
1140
- return new Promise((resolve) => {
1141
- let settled = false;
1142
- let submitting = false;
1143
- let confirmationStarted = false;
1144
- const cleanup = () => {
1145
- context.signal?.removeEventListener("abort", onAbort);
1146
- modal.form.removeEventListener("submit", onSubmit);
1147
- modal.cancelButton.removeEventListener("click", onCancel);
1148
- if (paymentElement.destroy) paymentElement.destroy();
1149
- else paymentElement.unmount?.();
1150
- modal.root.remove();
1151
- modal.previousFocus?.focus();
1152
- };
1153
- const settle = (outcome) => {
1154
- if (settled) return;
1155
- settled = true;
1156
- cleanup();
1157
- resolve(outcome);
1158
- };
1159
- const onAbort = () => {
1160
- if (confirmationStarted) {
1161
- settle({ status: "submitted", reference: context.reference });
1162
- return;
1163
- }
1164
- settle({ status: "cancelled" });
1165
- };
1166
- const onCancel = () => {
1167
- if (!confirmationStarted) settle({ status: "cancelled" });
1168
- };
1169
- const onSubmit = async (event) => {
1170
- event.preventDefault();
1171
- if (submitting || settled) return;
1172
- submitting = true;
1173
- modal.submitButton.disabled = true;
1174
- modal.error.textContent = "";
1175
- try {
1176
- const validation = elements.submit ? await elements.submit() : {};
1177
- if (validation.error) {
1178
- modal.error.textContent = validation.error.message || "Check your payment details.";
1179
- submitting = false;
1180
- modal.submitButton.disabled = false;
1181
- return;
1182
- }
1183
- if (settled || context.signal?.aborted) {
1184
- settle({ status: "cancelled" });
1185
- return;
1186
- }
1187
- confirmationStarted = true;
1188
- modal.cancelButton.disabled = true;
1189
- const confirmation = await stripe.confirmPayment({
1190
- elements,
1191
- confirmParams: { return_url: context.returnUrl || window.location.href },
1192
- redirect: "if_required"
1193
- });
1194
- if (settled) return;
1195
- if (confirmation.error) {
1196
- settle(stripeFailure(confirmation.error));
1197
- return;
1198
- }
1199
- settle({
1200
- status: "submitted",
1201
- reference: confirmation.paymentIntent?.id || context.reference
1202
- });
1203
- } catch (error) {
1204
- if (confirmationStarted) {
1205
- settle({ status: "submitted", reference: context.reference });
1206
- return;
1207
- }
1208
- settle(
1209
- stripeFailure({
1210
- message: error instanceof Error ? error.message : "Stripe confirmation failed."
1211
- })
1212
- );
1213
- }
1214
- };
1215
- modal.form.addEventListener("submit", onSubmit);
1216
- modal.cancelButton.addEventListener("click", onCancel);
1217
- context.signal?.addEventListener("abort", onAbort, { once: true });
1218
- paymentElement.on?.("escape", onCancel);
1219
- paymentElement.on?.("loaderror", (event) => {
1220
- if (confirmationStarted) return;
1221
- settle({
1222
- status: "failed",
1223
- code: "PROVIDER_UNAVAILABLE",
1224
- message: event?.error?.message || "Stripe Payment Element failed to load.",
1225
- recoverable: true
1226
- });
1227
- });
1228
- });
1229
- }
1230
- };
1231
-
1232
- // src/payment-authorization/registry.ts
1233
- function normalizeProvider(provider) {
1234
- return typeof provider === "string" ? provider.trim().toLowerCase() : "";
1376
+ };
1377
+ hub.storageListener = (event) => {
1378
+ const storageEvent = event;
1379
+ if (storageEvent.key !== SESSION_SYNC_STORAGE_KEY) return;
1380
+ const payload = parseSessionSyncPayload(storageEvent.newValue);
1381
+ if (payload) hub.dispatch(payload);
1382
+ };
1383
+ window.addEventListener("storage", hub.storageListener);
1384
+ hubHost()[CROSS_TAB_SESSION_HUB_KEY] = hub;
1385
+ return hub;
1235
1386
  }
1236
- var PaymentAuthorizationAdapterRegistry = class {
1237
- constructor(adapters = []) {
1238
- this.adapters = /* @__PURE__ */ new Map();
1239
- for (const adapter of adapters) {
1240
- this.register(adapter);
1241
- }
1242
- }
1243
- register(adapter) {
1244
- const provider = normalizeProvider(adapter.provider);
1245
- if (!provider) {
1246
- throw new TypeError("Payment authorization adapter provider cannot be empty.");
1247
- }
1248
- this.adapters.set(provider, adapter);
1249
- return this;
1250
- }
1251
- unregister(provider) {
1252
- return this.adapters.delete(normalizeProvider(provider));
1253
- }
1254
- get(provider) {
1255
- return this.adapters.get(normalizeProvider(provider));
1256
- }
1257
- supports(provider) {
1258
- return this.adapters.has(normalizeProvider(provider));
1259
- }
1260
- providers() {
1261
- return [...this.adapters.keys()];
1262
- }
1263
- async authorize(action, options) {
1264
- const provider = normalizeProvider(action.provider);
1265
- if (!provider) {
1266
- return {
1267
- status: "failed",
1268
- code: "PROVIDER_UNAVAILABLE",
1269
- message: "Card authorization response did not identify a payment provider.",
1270
- recoverable: false
1271
- };
1272
- }
1273
- const adapter = this.adapters.get(provider);
1274
- if (!adapter) {
1275
- return {
1276
- status: "failed",
1277
- code: "PROVIDER_UNAVAILABLE",
1278
- message: `Card authorization provider '${provider}' is not supported by this SDK client.`,
1279
- recoverable: false
1280
- };
1281
- }
1282
- try {
1283
- const normalizedAction = action.provider === provider ? action : { ...action, provider };
1284
- return await adapter.authorize({ ...options, action: normalizedAction });
1285
- } catch (error) {
1286
- return {
1287
- status: "failed",
1288
- code: "PAYMENT_FAILED",
1289
- message: error instanceof Error ? error.message : `Card authorization provider '${provider}' failed unexpectedly.`,
1290
- recoverable: true
1291
- };
1292
- }
1387
+ function publishLocalSessionSync(payload) {
1388
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
1389
+ (currentHub() ?? createHub()).dispatch(payload);
1390
+ }
1391
+ function subscribeCrossTabSession(notify) {
1392
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") {
1393
+ return () => {
1394
+ };
1293
1395
  }
1294
- };
1295
- function createDefaultPaymentAuthorizationAdapterRegistry() {
1296
- return new PaymentAuthorizationAdapterRegistry([
1297
- new PaystackAuthorizationAdapter(),
1298
- new StripeAuthorizationAdapter()
1299
- ]);
1396
+ const hub = currentHub() ?? createHub();
1397
+ pruneSubscribers(hub);
1398
+ const token = /* @__PURE__ */ Symbol("cross-tab-session-subscriber");
1399
+ const subscriber = { notify };
1400
+ hub.subscribers.set(token, subscriberReference(subscriber));
1401
+ let unsubscribed = false;
1402
+ return () => {
1403
+ if (unsubscribed) return;
1404
+ unsubscribed = true;
1405
+ subscriber.notify = () => {
1406
+ };
1407
+ hub.subscribers.delete(token);
1408
+ pruneSubscribers(hub);
1409
+ if (hub.subscribers.size !== 0) return;
1410
+ window.removeEventListener("storage", hub.storageListener);
1411
+ if (currentHub() === hub) delete hubHost()[CROSS_TAB_SESSION_HUB_KEY];
1412
+ };
1300
1413
  }
1301
1414
 
1302
1415
  // src/client.ts
1303
1416
  var SESSION_TOKEN_STORAGE_KEY = "cimplify_session_token";
1304
- var SESSION_SYNC_STORAGE_KEY = "cimplify_session_sync";
1305
1417
  var ORDER_TOKEN_PREFIX = "cimplify_ot_";
1306
1418
  var SESSION_TOKEN_HEADER = "x-session-token";
1307
1419
  var LOCATION_ID_QUERY_PARAMETER = "location_id";
@@ -1309,8 +1421,37 @@ var ORDER_TOKEN_TTL_MS = 24 * 60 * 60 * 1e3;
1309
1421
  var DEFAULT_TIMEOUT_MS = 5e3;
1310
1422
  var DEFAULT_MAX_RETRIES = 3;
1311
1423
  var DEFAULT_RETRY_DELAY_MS = 1e3;
1312
- function sleep(ms) {
1313
- return new Promise((resolve) => setTimeout(resolve, ms));
1424
+ var MAX_RETRY_AFTER_MS = 3e4;
1425
+ function sleep(ms, signal) {
1426
+ if (signal?.aborted) {
1427
+ return Promise.reject(new CimplifyError(ErrorCode.CANCELLED, "Request was cancelled.", true));
1428
+ }
1429
+ return new Promise((resolve, reject) => {
1430
+ const timer = setTimeout(() => {
1431
+ signal?.removeEventListener("abort", onAbort);
1432
+ resolve();
1433
+ }, ms);
1434
+ const onAbort = () => {
1435
+ clearTimeout(timer);
1436
+ reject(new CimplifyError(ErrorCode.CANCELLED, "Request was cancelled.", true));
1437
+ };
1438
+ signal?.addEventListener("abort", onAbort, { once: true });
1439
+ });
1440
+ }
1441
+ function parseRetryAfterMs(value) {
1442
+ if (!value) return null;
1443
+ const seconds = Number(value);
1444
+ if (Number.isFinite(seconds) && seconds >= 0) {
1445
+ return Math.min(seconds * 1e3, MAX_RETRY_AFTER_MS);
1446
+ }
1447
+ const retryAt = Date.parse(value);
1448
+ if (!Number.isFinite(retryAt)) return null;
1449
+ return Math.min(Math.max(0, retryAt - Date.now()), MAX_RETRY_AFTER_MS);
1450
+ }
1451
+ function withPositiveJitter(delayMs, jitterRatio = 0) {
1452
+ const boundedRatio = Math.min(Math.max(jitterRatio, 0), 1);
1453
+ if (boundedRatio === 0 || delayMs === 0) return delayMs;
1454
+ return Math.round(delayMs + delayMs * boundedRatio * Math.random());
1314
1455
  }
1315
1456
  function nextInit(cacheOptions) {
1316
1457
  if (!cacheOptions) return {};
@@ -1424,6 +1565,9 @@ var CimplifyClient = class {
1424
1565
  this.businessId = null;
1425
1566
  this.businessIdResolvePromise = null;
1426
1567
  this.publicKeyEpoch = 0;
1568
+ this.stopCrossTabSessionWatch = null;
1569
+ this.latestSessionSyncAt = Number.NEGATIVE_INFINITY;
1570
+ this.latestSessionSyncOrderKey = null;
1427
1571
  this.inflightRequests = /* @__PURE__ */ new Map();
1428
1572
  /**
1429
1573
  * App-level event channel. Subscribe to surface mutation rollbacks, outbox
@@ -1445,7 +1589,11 @@ var CimplifyClient = class {
1445
1589
  this.authorizationAdapters = config.authorizationAdapterRegistry ?? createDefaultPaymentAuthorizationAdapterRegistry();
1446
1590
  this.fetchImpl = (config.fetch ?? globalThis.fetch).bind(globalThis);
1447
1591
  this.sessionToken = this.loadSessionToken() ?? this.mintSessionToken();
1448
- this.watchCrossTabSession();
1592
+ const persistedSessionSync = this.loadSessionSyncPayload();
1593
+ if (persistedSessionSync) this.observeSessionSync(persistedSessionSync);
1594
+ this.stopCrossTabSessionWatch = subscribeCrossTabSession((payload) => {
1595
+ this.applyCrossTabSession(payload);
1596
+ });
1449
1597
  if (!this.publicKey && !config.suppressPublicKeyWarning) {
1450
1598
  console.warn(
1451
1599
  '[Cimplify] No public key found. Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in your environment, or pass { publicKey: "cpk_..." } to createCimplifyClient().'
@@ -1472,6 +1620,12 @@ var CimplifyClient = class {
1472
1620
  this.stopPageViewTracking?.();
1473
1621
  this.stopPageViewTracking = null;
1474
1622
  }
1623
+ /** Release window-level subscriptions and automatic tracking owned by this client. */
1624
+ destroy() {
1625
+ this.stopTrackingPageViews();
1626
+ this.stopCrossTabSessionWatch?.();
1627
+ this.stopCrossTabSessionWatch = null;
1628
+ }
1475
1629
  getAccessToken() {
1476
1630
  return this.accessToken;
1477
1631
  }
@@ -1499,7 +1653,7 @@ var CimplifyClient = class {
1499
1653
  this.hooks.onSessionChange?.({
1500
1654
  previousToken: previous,
1501
1655
  newToken: token,
1502
- source: "manual"
1656
+ source: SESSION_CHANGE_SOURCE.MANUAL
1503
1657
  });
1504
1658
  this.events.emit(AUTH_TOKEN_CHANGED, { token });
1505
1659
  }
@@ -1543,7 +1697,7 @@ var CimplifyClient = class {
1543
1697
  this.hooks.onSessionChange?.({
1544
1698
  previousToken: previous,
1545
1699
  newToken: null,
1546
- source: "clear"
1700
+ source: SESSION_CHANGE_SOURCE.CLEAR
1547
1701
  });
1548
1702
  this.events.emit(AUTH_TOKEN_CHANGED, { token: null });
1549
1703
  this.events.emit(SESSION_CLEARED, {});
@@ -1565,9 +1719,10 @@ var CimplifyClient = class {
1565
1719
  hasClaimedSessionAuthority() {
1566
1720
  if (!this.sessionToken) return false;
1567
1721
  try {
1568
- const raw = this.safeStorage()?.getItem(SESSION_SYNC_STORAGE_KEY);
1569
- if (!raw) return false;
1570
- const payload = JSON.parse(raw);
1722
+ const payload = parseSessionSyncPayload(
1723
+ this.safeStorage()?.getItem(SESSION_SYNC_STORAGE_KEY) ?? null
1724
+ );
1725
+ if (!payload) return false;
1571
1726
  return payload.type === "claimed" && payload.token === this.sessionToken;
1572
1727
  } catch {
1573
1728
  return false;
@@ -1582,41 +1737,49 @@ var CimplifyClient = class {
1582
1737
  });
1583
1738
  }
1584
1739
  broadcastSessionSync(payload) {
1740
+ const envelope = {
1741
+ ...payload,
1742
+ at: this.nextSessionSyncTimestamp()
1743
+ };
1585
1744
  try {
1586
- this.safeStorage()?.setItem(
1587
- SESSION_SYNC_STORAGE_KEY,
1588
- JSON.stringify({ ...payload, at: Date.now() })
1589
- );
1745
+ this.safeStorage()?.setItem(SESSION_SYNC_STORAGE_KEY, JSON.stringify(envelope));
1590
1746
  } catch {
1591
1747
  }
1748
+ publishLocalSessionSync(envelope);
1592
1749
  }
1593
- watchCrossTabSession() {
1594
- if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
1595
- window.addEventListener("storage", (event) => {
1596
- if (event.key !== SESSION_SYNC_STORAGE_KEY || !event.newValue) return;
1597
- let payload;
1598
- try {
1599
- payload = JSON.parse(event.newValue);
1600
- } catch {
1601
- return;
1602
- }
1603
- if (!payload?.token || payload.token === this.sessionToken) return;
1604
- if (payload.type === "claimed") {
1605
- this.sessionToken = payload.token;
1606
- this.sessionEpoch++;
1607
- this.events.emit(SESSION_CLAIMED, {
1608
- sessionToken: payload.token,
1609
- threadId: null,
1610
- customerName: payload.name ?? ""
1611
- });
1612
- } else if (payload.type === "cleared") {
1613
- this.accessToken = null;
1614
- this.sessionToken = payload.token;
1615
- this.sessionEpoch++;
1616
- this.events.emit(AUTH_TOKEN_CHANGED, { token: null });
1617
- this.events.emit(SESSION_CLEARED, {});
1618
- }
1619
- });
1750
+ applyCrossTabSession(payload) {
1751
+ if (!this.observeSessionSync(payload)) return;
1752
+ if (payload.token === this.sessionToken) return;
1753
+ if (payload.type === "claimed") {
1754
+ this.sessionToken = payload.token;
1755
+ this.sessionEpoch++;
1756
+ this.events.emit(SESSION_CLAIMED, {
1757
+ sessionToken: payload.token,
1758
+ threadId: null,
1759
+ customerName: payload.name ?? ""
1760
+ });
1761
+ } else {
1762
+ this.accessToken = null;
1763
+ this._activity?.resetSession();
1764
+ this.sessionToken = payload.token;
1765
+ this.sessionEpoch++;
1766
+ this.events.emit(AUTH_TOKEN_CHANGED, { token: null });
1767
+ this.events.emit(SESSION_CLEARED, {});
1768
+ }
1769
+ }
1770
+ observeSessionSync(payload) {
1771
+ if (payload.at < this.latestSessionSyncAt) return false;
1772
+ const envelope = sessionSyncEnvelopeKey(payload);
1773
+ if (payload.at === this.latestSessionSyncAt && this.latestSessionSyncOrderKey !== null && envelope <= this.latestSessionSyncOrderKey) {
1774
+ return false;
1775
+ }
1776
+ this.latestSessionSyncAt = payload.at;
1777
+ this.latestSessionSyncOrderKey = envelope;
1778
+ return true;
1779
+ }
1780
+ nextSessionSyncTimestamp() {
1781
+ const now = Date.now();
1782
+ return Number.isFinite(this.latestSessionSyncAt) ? Math.max(now, this.latestSessionSyncAt + 1) : now;
1620
1783
  }
1621
1784
  setOrderToken(orderId, token) {
1622
1785
  try {
@@ -1680,17 +1843,24 @@ var CimplifyClient = class {
1680
1843
  setPublicKey(publicKey) {
1681
1844
  const normalized = publicKey.trim();
1682
1845
  if (normalized === this.publicKey) return;
1846
+ const restartPageViewTracking = this.stopPageViewTracking !== null;
1847
+ if (restartPageViewTracking) {
1848
+ this.stopTrackingPageViews();
1849
+ }
1683
1850
  this.publicKey = normalized;
1684
1851
  this.publicKeyEpoch += 1;
1685
1852
  this.businessId = null;
1686
1853
  this.businessIdResolvePromise = null;
1687
1854
  this.inflightRequests.clear();
1688
- this._activity?.resetSession();
1855
+ this._activity?.resetForStorefrontIdentityChange();
1689
1856
  delete this.context.location_id;
1690
1857
  if (this.accessToken !== null) {
1691
1858
  this.setAccessToken(null);
1692
1859
  }
1693
1860
  this.events.emit(SESSION_CLEARED, {});
1861
+ if (restartPageViewTracking) {
1862
+ this.stopPageViewTracking = this.activity.autoTrackPageViews();
1863
+ }
1694
1864
  }
1695
1865
  getBusinessId() {
1696
1866
  return this.businessId;
@@ -1731,8 +1901,8 @@ var CimplifyClient = class {
1731
1901
  }
1732
1902
  }
1733
1903
  }
1734
- async authedFetch(url, options, timeoutMs) {
1735
- return this.resilientFetch(url, options, timeoutMs);
1904
+ async authedFetch(url, options, timeoutMs, retryPolicy, signal) {
1905
+ return this.resilientFetch(url, options, timeoutMs, retryPolicy, signal);
1736
1906
  }
1737
1907
  // Storage guards: even the `window.localStorage` property access throws in
1738
1908
  // some sandboxed iframes and strict private-browsing modes. These run in the
@@ -1754,6 +1924,13 @@ var CimplifyClient = class {
1754
1924
  return null;
1755
1925
  }
1756
1926
  }
1927
+ loadSessionSyncPayload() {
1928
+ try {
1929
+ return parseSessionSyncPayload(this.safeStorage()?.getItem(SESSION_SYNC_STORAGE_KEY) ?? null);
1930
+ } catch {
1931
+ return null;
1932
+ }
1933
+ }
1757
1934
  /**
1758
1935
  * Mint the session token client-side so the very first request already
1759
1936
  * carries it. Without this, parallel first-load requests each get a
@@ -1809,7 +1986,7 @@ var CimplifyClient = class {
1809
1986
  }
1810
1987
  return headers;
1811
1988
  }
1812
- async resilientFetch(url, options, timeoutMs) {
1989
+ async resilientFetch(url, options, timeoutMs, retryPolicy, signal) {
1813
1990
  const method = options.method || "GET";
1814
1991
  const path = url.replace(this.baseUrl, "").replace(this.linkApiUrl, "");
1815
1992
  const startTime = Date.now();
@@ -1823,15 +2000,31 @@ var CimplifyClient = class {
1823
2000
  };
1824
2001
  this.hooks.onRequestStart?.(context);
1825
2002
  let lastError;
1826
- for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
2003
+ const configuredMaxRetries = Math.max(0, Math.floor(this.maxRetries));
2004
+ const maxRetries = Math.min(
2005
+ configuredMaxRetries,
2006
+ Math.max(0, Math.floor(retryPolicy?.maxRetries ?? configuredMaxRetries))
2007
+ );
2008
+ const jitterRatio = retryPolicy?.jitterRatio ?? 0;
2009
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1827
2010
  try {
2011
+ if (signal?.aborted) {
2012
+ throw new CimplifyError(ErrorCode.CANCELLED, "Request was cancelled.", true);
2013
+ }
1828
2014
  const controller = new AbortController();
1829
2015
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs ?? this.timeout);
1830
- const response = await this.fetchImpl(url, {
1831
- ...options,
1832
- signal: controller.signal
1833
- });
1834
- clearTimeout(timeoutId);
2016
+ const onAbort = () => controller.abort();
2017
+ signal?.addEventListener("abort", onAbort, { once: true });
2018
+ let response;
2019
+ try {
2020
+ response = await this.fetchImpl(url, {
2021
+ ...options,
2022
+ signal: controller.signal
2023
+ });
2024
+ } finally {
2025
+ clearTimeout(timeoutId);
2026
+ signal?.removeEventListener("abort", onAbort);
2027
+ }
1835
2028
  this.captureSessionToken(response);
1836
2029
  if (response.ok) {
1837
2030
  this.hooks.onRequestSuccess?.({
@@ -1841,17 +2034,19 @@ var CimplifyClient = class {
1841
2034
  });
1842
2035
  return response;
1843
2036
  }
1844
- if ((response.status === 429 || response.status === 503) && attempt < this.maxRetries) {
2037
+ if ((response.status === 429 || response.status === 503) && attempt < maxRetries) {
1845
2038
  retryCount++;
1846
- const retryAfter = response.headers.get("Retry-After");
1847
- const delay = retryAfter ? Math.min(parseInt(retryAfter, 10) * 1e3, 3e4) || this.retryDelay * Math.pow(2, attempt) : this.retryDelay * Math.pow(2, attempt);
2039
+ const retryAfterMs = parseRetryAfterMs(response.headers.get("Retry-After"));
2040
+ const retryBaseMs = retryAfterMs ?? this.retryDelay * Math.pow(2, attempt);
2041
+ const jitteredDelayMs = withPositiveJitter(retryBaseMs, jitterRatio);
2042
+ const delay = retryAfterMs === null ? jitteredDelayMs : Math.min(jitteredDelayMs, MAX_RETRY_AFTER_MS);
1848
2043
  this.hooks.onRetry?.({
1849
2044
  ...context,
1850
2045
  attempt: retryCount,
1851
2046
  delayMs: delay,
1852
2047
  error: new Error(`Retryable: ${response.status}`)
1853
2048
  });
1854
- await sleep(delay);
2049
+ await sleep(delay, signal);
1855
2050
  continue;
1856
2051
  }
1857
2052
  if (response.status >= 400 && response.status < 500) {
@@ -1869,22 +2064,22 @@ var CimplifyClient = class {
1869
2064
  });
1870
2065
  return response;
1871
2066
  }
1872
- if (response.status >= 500 && attempt < this.maxRetries) {
2067
+ if (response.status >= 500 && attempt < maxRetries) {
1873
2068
  retryCount++;
1874
- const delay = this.retryDelay * Math.pow(2, attempt);
2069
+ const delay = withPositiveJitter(this.retryDelay * Math.pow(2, attempt), jitterRatio);
1875
2070
  this.hooks.onRetry?.({
1876
2071
  ...context,
1877
2072
  attempt: retryCount,
1878
2073
  delayMs: delay,
1879
2074
  error: new Error(`Server error: ${response.status}`)
1880
2075
  });
1881
- await sleep(delay);
2076
+ await sleep(delay, signal);
1882
2077
  continue;
1883
2078
  }
1884
2079
  this.hooks.onRequestError?.({
1885
2080
  ...context,
1886
2081
  error: new CimplifyError(
1887
- "SERVER_ERROR",
2082
+ ErrorCode.SERVER_ERROR,
1888
2083
  `Server error ${response.status} after ${retryCount} retries`,
1889
2084
  false
1890
2085
  ),
@@ -1896,9 +2091,12 @@ var CimplifyClient = class {
1896
2091
  return response;
1897
2092
  } catch (error) {
1898
2093
  lastError = error;
2094
+ if (signal?.aborted) {
2095
+ throw new CimplifyError(ErrorCode.CANCELLED, "Request was cancelled.", true);
2096
+ }
1899
2097
  const networkError = toNetworkError(error, this.isTestMode());
1900
2098
  const errorRetryable = isRetryable(error);
1901
- if (!errorRetryable || attempt >= this.maxRetries) {
2099
+ if (!errorRetryable || attempt >= maxRetries) {
1902
2100
  this.hooks.onRequestError?.({
1903
2101
  ...context,
1904
2102
  error: networkError,
@@ -1909,14 +2107,14 @@ var CimplifyClient = class {
1909
2107
  throw networkError;
1910
2108
  }
1911
2109
  retryCount++;
1912
- const delay = this.retryDelay * Math.pow(2, attempt);
2110
+ const delay = withPositiveJitter(this.retryDelay * Math.pow(2, attempt), jitterRatio);
1913
2111
  this.hooks.onRetry?.({
1914
2112
  ...context,
1915
2113
  attempt: retryCount,
1916
2114
  delayMs: delay,
1917
2115
  error: networkError
1918
2116
  });
1919
- await sleep(delay);
2117
+ await sleep(delay, signal);
1920
2118
  }
1921
2119
  }
1922
2120
  const finalError = toNetworkError(lastError, this.isTestMode());
@@ -1953,16 +2151,24 @@ var CimplifyClient = class {
1953
2151
  return request;
1954
2152
  }
1955
2153
  async get(path, opts) {
1956
- const key = this.getDedupeKey("get", path, opts?.cacheOptions);
1957
- return this.deduplicatedRequest(key, async () => {
1958
- const response = await this.authedFetch(this.buildUrl(this.baseUrl, path), {
1959
- method: "GET",
1960
- credentials: this.credentials,
1961
- headers: this.getHeaders(),
1962
- ...nextInit(opts?.cacheOptions)
1963
- });
2154
+ const request = async () => {
2155
+ const response = await this.authedFetch(
2156
+ this.buildUrl(this.baseUrl, path),
2157
+ {
2158
+ method: "GET",
2159
+ credentials: this.credentials,
2160
+ headers: this.getHeaders(),
2161
+ ...nextInit(opts?.cacheOptions)
2162
+ },
2163
+ opts?.timeoutMs,
2164
+ opts?.retryPolicy,
2165
+ opts?.signal
2166
+ );
1964
2167
  return this.handleRestResponse(response);
1965
- });
2168
+ };
2169
+ if (opts?.signal) return request();
2170
+ const key = this.getDedupeKey("get", path, opts?.cacheOptions);
2171
+ return this.deduplicatedRequest(key, request);
1966
2172
  }
1967
2173
  async post(path, body, opts) {
1968
2174
  const response = await this.authedFetch(
@@ -1973,7 +2179,9 @@ var CimplifyClient = class {
1973
2179
  headers: mergeOptionHeaders(this.getHeaders(), opts),
1974
2180
  body: body ? JSON.stringify(body) : void 0
1975
2181
  },
1976
- opts?.timeoutMs
2182
+ opts?.timeoutMs,
2183
+ opts?.retryPolicy,
2184
+ opts?.signal
1977
2185
  );
1978
2186
  return this.handleRestResponse(response);
1979
2187
  }
@@ -1986,7 +2194,9 @@ var CimplifyClient = class {
1986
2194
  headers: mergeOptionHeaders(this.getHeaders(), opts),
1987
2195
  body: body ? JSON.stringify(body) : void 0
1988
2196
  },
1989
- opts?.timeoutMs
2197
+ opts?.timeoutMs,
2198
+ opts?.retryPolicy,
2199
+ opts?.signal
1990
2200
  );
1991
2201
  return this.handleRestResponse(response);
1992
2202
  }
@@ -1998,21 +2208,31 @@ var CimplifyClient = class {
1998
2208
  credentials: this.credentials,
1999
2209
  headers: mergeOptionHeaders(this.getHeaders(), opts)
2000
2210
  },
2001
- opts?.timeoutMs
2211
+ opts?.timeoutMs,
2212
+ opts?.retryPolicy,
2213
+ opts?.signal
2002
2214
  );
2003
2215
  return this.handleRestResponse(response);
2004
2216
  }
2005
2217
  async linkGet(path, opts) {
2006
- const key = this.getDedupeKey("linkGet", path, opts?.cacheOptions);
2007
- return this.deduplicatedRequest(key, async () => {
2008
- const response = await this.authedFetch(`${this.linkApiUrl}${path}`, {
2009
- method: "GET",
2010
- credentials: this.credentials,
2011
- headers: this.getHeaders(),
2012
- ...nextInit(opts?.cacheOptions)
2013
- });
2218
+ const request = async () => {
2219
+ const response = await this.authedFetch(
2220
+ `${this.linkApiUrl}${path}`,
2221
+ {
2222
+ method: "GET",
2223
+ credentials: this.credentials,
2224
+ headers: this.getHeaders(),
2225
+ ...nextInit(opts?.cacheOptions)
2226
+ },
2227
+ opts?.timeoutMs,
2228
+ opts?.retryPolicy,
2229
+ opts?.signal
2230
+ );
2014
2231
  return this.handleRestResponse(response);
2015
- });
2232
+ };
2233
+ if (opts?.signal) return request();
2234
+ const key = this.getDedupeKey("linkGet", path, opts?.cacheOptions);
2235
+ return this.deduplicatedRequest(key, request);
2016
2236
  }
2017
2237
  async linkPost(path, body, opts) {
2018
2238
  const response = await this.authedFetch(
@@ -2023,25 +2243,33 @@ var CimplifyClient = class {
2023
2243
  headers: mergeOptionHeaders(this.getHeaders(), opts),
2024
2244
  body: body ? JSON.stringify(body) : void 0
2025
2245
  },
2026
- opts?.timeoutMs
2246
+ opts?.timeoutMs,
2247
+ opts?.retryPolicy,
2248
+ opts?.signal
2027
2249
  );
2028
2250
  return this.handleRestResponse(response);
2029
2251
  }
2030
2252
  async linkDelete(path, opts) {
2031
- const response = await this.authedFetch(`${this.linkApiUrl}${path}`, {
2032
- method: "DELETE",
2033
- credentials: this.credentials,
2034
- headers: mergeOptionHeaders(this.getHeaders(), opts)
2035
- });
2253
+ const response = await this.authedFetch(
2254
+ `${this.linkApiUrl}${path}`,
2255
+ {
2256
+ method: "DELETE",
2257
+ credentials: this.credentials,
2258
+ headers: mergeOptionHeaders(this.getHeaders(), opts)
2259
+ },
2260
+ opts?.timeoutMs,
2261
+ opts?.retryPolicy,
2262
+ opts?.signal
2263
+ );
2036
2264
  return this.handleRestResponse(response);
2037
2265
  }
2038
2266
  async handleRestResponse(response) {
2039
2267
  const json = await response.json();
2040
2268
  if (!response.ok) {
2041
- const errorCode = typeof json.error === "object" && json.error?.error_code || typeof json.error === "object" && json.error?.code || "API_ERROR";
2269
+ const errorCode = typeof json.error === "object" && json.error?.error_code || typeof json.error === "object" && json.error?.code || ErrorCode.API_ERROR;
2042
2270
  const errorMessage = typeof json.error === "object" && json.error?.error_message || typeof json.error === "object" && json.error?.message || typeof json.error === "string" && json.error || typeof json.message === "string" && json.message || "An error occurred";
2043
2271
  const retryable = typeof json.error === "object" && typeof json.error?.retryable === "boolean" ? json.error.retryable : false;
2044
- if (errorCode === "IDEMPOTENCY_MISMATCH") {
2272
+ if (errorCode === ErrorCode.IDEMPOTENCY_MISMATCH) {
2045
2273
  const originalCreatedAt = typeof json.error === "object" && json.error?.original_created_at || "";
2046
2274
  throw new IdempotencyMismatchError(errorMessage, originalCreatedAt);
2047
2275
  }
@@ -2058,7 +2286,7 @@ var CimplifyClient = class {
2058
2286
  if (json?.success === false || json?.error?.code || json?.error?.message) {
2059
2287
  const error = enrichError(
2060
2288
  new CimplifyError(
2061
- json.error?.code || "API_ERROR",
2289
+ json.error?.code || ErrorCode.API_ERROR,
2062
2290
  json.error?.message || "An error occurred",
2063
2291
  json.error?.retryable || false
2064
2292
  ),
@@ -2089,6 +2317,12 @@ var CimplifyClient = class {
2089
2317
  }
2090
2318
  return this._checkout;
2091
2319
  }
2320
+ get purchaseIntents() {
2321
+ if (!this._purchaseIntents) {
2322
+ this._purchaseIntents = new PurchaseIntentService(this);
2323
+ }
2324
+ return this._purchaseIntents;
2325
+ }
2092
2326
  get orders() {
2093
2327
  if (!this._orders) {
2094
2328
  this._orders = new OrderQueries(this);
@@ -2199,4 +2433,4 @@ function createCimplifyClient(config = {}) {
2199
2433
  return new CimplifyClient(config);
2200
2434
  }
2201
2435
 
2202
- export { AUTH_TOKEN_CHANGED, ActivityService, CimplifyClient, DeliveryService, MUTATION_ERROR, PaymentAuthorizationAdapterRegistry, PaystackAuthorizationAdapter, PlacesService, SESSION_CLAIMED, SESSION_CLEARED, StoreCreditQueries, StripeAuthorizationAdapter, SubscriptionService, SupportService, UploadService, chatRealtimeUpdateFromWire, createCimplifyClient, createDefaultPaymentAuthorizationAdapterRegistry };
2436
+ export { AUTH_TOKEN_CHANGED, ActivityService, CimplifyClient, DeliveryService, HostedPurchaseIntentService, MUTATION_ERROR, PURCHASE_INTENT_ROUTE, PlacesService, PurchaseIntentResolver, PurchaseIntentService, SESSION_CHANGE_SOURCE, SESSION_CLAIMED, SESSION_CLEARED, StoreCreditQueries, SubscriptionService, SupportService, UploadService, chatRealtimeUpdateFromWire, createCimplifyClient };