@cimplify/sdk 0.103.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 (72) 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-DHBwvh7U.d.ts → catalogue-BlQ85Uma.d.mts} +492 -748
  6. package/dist/{client-BfpNjJ_a.d.mts → catalogue-C8TqOj9K.d.ts} +492 -748
  7. package/dist/{payment-H5LoIvXU.d.mts → checkout-BqK9XUz5.d.mts} +4338 -1495
  8. package/dist/{payment-H5LoIvXU.d.ts → checkout-BqK9XUz5.d.ts} +4338 -1495
  9. package/dist/{chunk-GOHSXBKW.mjs → chunk-33VCYWVL.mjs} +25 -1
  10. package/dist/{chunk-C2JSNJUC.mjs → chunk-6G3ANKQA.mjs} +1080 -369
  11. package/dist/{chunk-QOWPGS7Z.js → chunk-BWEIMKLF.js} +572 -17
  12. package/dist/{chunk-JSWJ4XDE.js → chunk-DQEXY542.js} +1630 -824
  13. package/dist/{chunk-CNN7Z2IP.js → chunk-EV2EVOYS.js} +27 -0
  14. package/dist/{chunk-VIL6IQCG.js → chunk-HYFA67A7.js} +1084 -373
  15. package/dist/{chunk-H7ACMHM3.mjs → chunk-L5SNEU2O.mjs} +553 -16
  16. package/dist/{chunk-JG5CQNCB.js → chunk-RBNBSQI7.mjs} +221 -19
  17. package/dist/{chunk-VNPEYLB2.mjs → chunk-SFUJMS5E.js} +265 -2
  18. package/dist/{chunk-M4URVKHE.mjs → chunk-U54CWUUT.mjs} +11 -2
  19. package/dist/{chunk-GERXZQSX.mjs → chunk-UFOWPN3D.mjs} +967 -582
  20. package/dist/{chunk-J6PNHDHL.mjs → chunk-V4457OKB.mjs} +1580 -785
  21. package/dist/{chunk-R6OLQ33I.js → chunk-WXREIBFW.js} +1041 -654
  22. package/dist/{chunk-NNG6IFFX.js → chunk-XAUSDMMZ.js} +13 -3
  23. package/dist/{client-DpqRobY9.d.mts → client-DhXeiUrf.d.ts} +2 -2
  24. package/dist/{client-Dqg2mc-7.d.ts → client-mXiUrV4X.d.mts} +2 -2
  25. package/dist/{index-C8uDqLwJ.d.ts → index-B0AdZvrM.d.mts} +9 -4
  26. package/dist/{index-e2y6AK70.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 +34 -15
  30. package/dist/index.d.ts +34 -15
  31. package/dist/index.js +306 -133
  32. package/dist/index.mjs +7 -6
  33. package/dist/mock/cli.mjs +1494 -395
  34. package/dist/mock/library.js +1220 -361
  35. package/dist/mock/library.mjs +1220 -361
  36. package/dist/mock/msw.js +1220 -361
  37. package/dist/mock/msw.mjs +1220 -361
  38. package/dist/{price-C2RqscMY.d.ts → price-6hQIIac9.d.ts} +1 -1
  39. package/dist/{price-BvePgDXe.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 +279 -225
  43. package/dist/react.mjs +140 -86
  44. package/dist/{server-DoXIpSd7.d.ts → server-DJ0mSs9i.d.mts} +33 -57
  45. package/dist/{server-C7FyLdrz.d.mts → server-DSTX76Ki.d.ts} +33 -57
  46. package/dist/server.d.mts +3 -4
  47. package/dist/server.d.ts +3 -4
  48. package/dist/server.js +12 -11
  49. package/dist/server.mjs +6 -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 +2217 -14
  60. package/dist/testing.d.ts +2217 -14
  61. package/dist/testing.js +173 -83
  62. package/dist/testing.mjs +28 -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/chat-widget.json +1 -1
  69. package/registry/order-history.json +1 -1
  70. package/registry/order-summary.json +1 -1
  71. package/dist/link-cuNVYc2s.d.mts +0 -297
  72. package/dist/link-cuNVYc2s.d.ts +0 -297
@@ -1,5 +1,333 @@
1
- import { safe, generateIdempotencyKey, err, ok, CatalogueQueries, CartOperations, CheckoutService, OrderQueries, LinkService, AuthService, BusinessService, InventoryService, SchedulingService, LiteService, FxService, createElements } from './chunk-J6PNHDHL.mjs';
2
- import { CimplifyError, ErrorCode, IdempotencyMismatchError, enrichError } from './chunk-VNPEYLB2.mjs';
1
+ import { messagePreview } from './chunk-U4MW6PYU.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
+ };
3
331
 
4
332
  // src/store-credit.ts
5
333
  var StoreCreditQueries = class {
@@ -17,12 +345,13 @@ var StoreCreditQueries = class {
17
345
 
18
346
  // src/internal/analytics-session.ts
19
347
  var ANALYTICS_SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
20
- var ANALYTICS_SESSION_STORAGE_KEY = "cimplify_analytics_session";
21
- var ANALYTICS_SESSION_STATE_VERSION = 1;
348
+ var ANALYTICS_SESSION_STORAGE_KEY_PREFIX = "cimplify_analytics_session:v2:";
349
+ var ANALYTICS_SESSION_STATE_VERSION = 2;
22
350
  var MICROSECONDS_PER_MILLISECOND = 1e3;
23
351
  var MICROSECONDS_PER_SECOND = 1e6;
24
352
  var AnalyticsSessionCoordinator = class {
25
- constructor() {
353
+ constructor(identity) {
354
+ this.identity = identity;
26
355
  this.fallbackState = null;
27
356
  }
28
357
  beginEvent(nowMs = Date.now()) {
@@ -60,7 +389,7 @@ var AnalyticsSessionCoordinator = class {
60
389
  reset() {
61
390
  this.fallbackState = null;
62
391
  try {
63
- sessionStorage.removeItem(ANALYTICS_SESSION_STORAGE_KEY);
392
+ sessionStorage.removeItem(this.storageKey());
64
393
  } catch {
65
394
  }
66
395
  }
@@ -70,11 +399,11 @@ var AnalyticsSessionCoordinator = class {
70
399
  }
71
400
  read() {
72
401
  try {
73
- const serialized = sessionStorage.getItem(ANALYTICS_SESSION_STORAGE_KEY);
402
+ const serialized = sessionStorage.getItem(this.storageKey());
74
403
  if (!serialized) return this.fallbackState;
75
404
  const state = JSON.parse(serialized);
76
405
  if (!isPersistedSession(state)) {
77
- sessionStorage.removeItem(ANALYTICS_SESSION_STORAGE_KEY);
406
+ sessionStorage.removeItem(this.storageKey());
78
407
  return this.fallbackState;
79
408
  }
80
409
  this.fallbackState = state;
@@ -86,10 +415,13 @@ var AnalyticsSessionCoordinator = class {
86
415
  write(state) {
87
416
  this.fallbackState = state;
88
417
  try {
89
- sessionStorage.setItem(ANALYTICS_SESSION_STORAGE_KEY, JSON.stringify(state));
418
+ sessionStorage.setItem(this.storageKey(), JSON.stringify(state));
90
419
  } catch {
91
420
  }
92
421
  }
422
+ storageKey() {
423
+ return `${ANALYTICS_SESSION_STORAGE_KEY_PREFIX}${encodeURIComponent(this.identity)}`;
424
+ }
93
425
  };
94
426
  function newSession(nowMs) {
95
427
  return {
@@ -156,11 +488,127 @@ function generateSessionId() {
156
488
 
157
489
  // src/activity.ts
158
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
+ }
159
606
  var ActivityService = class {
160
607
  constructor(client) {
161
608
  this.client = client;
162
- this.sessions = new AnalyticsSessionCoordinator();
609
+ this.localGeneration = 0;
163
610
  this.bootstrapRequest = null;
611
+ this.sessions = this.createSessionCoordinator();
164
612
  }
165
613
  /**
166
614
  * Record session start (landing, referrer, UTM, device). Fire-and-forget,
@@ -170,7 +618,7 @@ var ActivityService = class {
170
618
  startSession() {
171
619
  if (typeof window === "undefined") return;
172
620
  const session = this.sessions.beginEvent();
173
- void this.bootstrapSession(session).catch(() => {
621
+ void this.bootstrapSession(session, false, this.captureGeneration()).catch(() => {
174
622
  });
175
623
  }
176
624
  /** Rebind an existing analytics session after the server authenticates a customer. */
@@ -178,13 +626,19 @@ var ActivityService = class {
178
626
  if (typeof window === "undefined") return;
179
627
  const session = this.sessions.current();
180
628
  if (!session) return;
181
- void this.bootstrapSession(session, true).catch(() => {
629
+ void this.bootstrapSession(session, true, this.captureGeneration()).catch(() => {
182
630
  });
183
631
  }
184
632
  /** A signed-out browser starts a fresh anonymous analytics session. */
185
633
  resetSession() {
634
+ this.invalidatePendingActivity();
186
635
  this.sessions.reset();
187
- 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();
188
642
  }
189
643
  buildSessionPayload(session) {
190
644
  const params = new URLSearchParams(window.location.search);
@@ -206,6 +660,7 @@ var ActivityService = class {
206
660
  utm_campaign: utm("campaign"),
207
661
  utm_term: utm("term"),
208
662
  utm_content: utm("content"),
663
+ social_commerce_ref: params.get("social_commerce_ref") || void 0,
209
664
  screen_width: window.screen?.width,
210
665
  screen_height: window.screen?.height,
211
666
  viewport_width: window.innerWidth,
@@ -279,32 +734,12 @@ var ActivityService = class {
279
734
  autoTrackPageViews() {
280
735
  if (typeof window === "undefined") return () => {
281
736
  };
282
- let lastPath = null;
283
- const record = () => {
284
- const path = window.location.pathname;
285
- if (path === lastPath) return;
286
- lastPath = path;
737
+ return acquireWindowPageViewTracker(pageViewTrackerIdentity(this.client), (path) => {
287
738
  try {
288
739
  this.trackPageView({ pagePath: path });
289
740
  } catch {
290
741
  }
291
- };
292
- const { pushState, replaceState } = window.history;
293
- window.history.pushState = function(...args) {
294
- pushState.apply(this, args);
295
- record();
296
- };
297
- window.history.replaceState = function(...args) {
298
- replaceState.apply(this, args);
299
- record();
300
- };
301
- window.addEventListener("popstate", record);
302
- record();
303
- return () => {
304
- window.history.pushState = pushState;
305
- window.history.replaceState = replaceState;
306
- window.removeEventListener("popstate", record);
307
- };
742
+ });
308
743
  }
309
744
  sendEvents(events, session) {
310
745
  if (typeof window === "undefined") return;
@@ -312,29 +747,59 @@ var ActivityService = class {
312
747
  });
313
748
  }
314
749
  async postEvents(events, session) {
315
- await this.bootstrapSession(session);
316
- await this.client.post("/api/v1/activity/events", {
317
- analytics_session_id: session.id,
318
- occurred_at: session.occurredAt,
319
- events
320
- });
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
+ );
321
762
  }
322
- bootstrapSession(session, force = false) {
763
+ bootstrapSession(session, force = false, generation = this.captureGeneration()) {
323
764
  if (!force && !session.requiresBootstrap) return Promise.resolve();
324
- if (!force && this.bootstrapRequest?.sessionId === session.id) {
765
+ if (!force && this.bootstrapRequest?.sessionId === session.id && sameActivityGeneration(this.bootstrapRequest.generation, generation)) {
325
766
  return this.bootstrapRequest.promise;
326
767
  }
327
- const promise = this.client.post("/api/v1/activity/session", this.buildSessionPayload(session)).then(() => {
328
- 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
+ }
329
774
  }).finally(() => {
330
775
  if (this.bootstrapRequest?.promise === promise) {
331
776
  this.bootstrapRequest = null;
332
777
  }
333
778
  });
334
- this.bootstrapRequest = { sessionId: session.id, promise };
779
+ this.bootstrapRequest = { sessionId: session.id, generation, promise };
335
780
  return promise;
336
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
+ }
337
799
  };
800
+ function sameActivityGeneration(left, right) {
801
+ return left.local === right.local && left.publicKey === right.publicKey && left.session === right.session;
802
+ }
338
803
  function currentPath() {
339
804
  if (typeof window !== "undefined") {
340
805
  return window.location.pathname;
@@ -414,14 +879,14 @@ var UploadService = class {
414
879
  constructor(client) {
415
880
  this.client = client;
416
881
  }
417
- async init(filename, contentType, sizeBytes, opts) {
882
+ async init(filename, contentType2, sizeBytes, opts) {
418
883
  const idempotencyKey = opts?.idempotencyKey ?? generateIdempotencyKey();
419
884
  return safe(
420
885
  this.client.post(
421
886
  "/api/v1/uploads/init",
422
887
  {
423
888
  filename,
424
- content_type: contentType,
889
+ content_type: contentType2,
425
890
  size_bytes: sizeBytes
426
891
  },
427
892
  { idempotencyKey }
@@ -522,6 +987,99 @@ var DeliveryService = class {
522
987
  }
523
988
  };
524
989
 
990
+ // src/chat-wire.ts
991
+ function senderType(message) {
992
+ if (message.sender_side === "account") return "customer";
993
+ if (message.sender_side === "system") return "system";
994
+ return message.authorship === "ai" ? "bot" : "agent";
995
+ }
996
+ function contentType(message) {
997
+ if (message.content_type === "rich") {
998
+ return message.structured_content?.["kind"] === "reaction" ? "reaction" : "interactive";
999
+ }
1000
+ if (message.content_type === "media") {
1001
+ const mime = message.media[0]?.mime_type ?? "";
1002
+ if (mime.startsWith("image/")) return "image";
1003
+ if (mime.startsWith("video/")) return "video";
1004
+ if (mime.startsWith("audio/")) return "audio";
1005
+ return "document";
1006
+ }
1007
+ return "text";
1008
+ }
1009
+ function unresolvedReply(id) {
1010
+ return { id, sender_type: "system", preview: "Earlier message" };
1011
+ }
1012
+ function chatMessagesFromWire(messages) {
1013
+ const converted = messages.map((message) => ({
1014
+ id: message.id,
1015
+ sequence: message.sequence,
1016
+ sender_type: senderType(message),
1017
+ content: message.content,
1018
+ content_type: contentType(message),
1019
+ attachments: Array.isArray(message.media) ? message.media : [],
1020
+ metadata: message.structured_content ?? {},
1021
+ reply_to: message.reply_to_message_id ? unresolvedReply(message.reply_to_message_id) : void 0,
1022
+ edited_at: message.edited_at,
1023
+ deleted_at: message.deleted_at,
1024
+ created_at: message.created_at
1025
+ }));
1026
+ const byId = new Map(converted.map((message) => [message.id, message]));
1027
+ return converted.map((message) => {
1028
+ const targetId = message.reply_to?.id;
1029
+ const target = targetId ? byId.get(targetId) : void 0;
1030
+ if (!targetId || !target) return message;
1031
+ return {
1032
+ ...message,
1033
+ reply_to: {
1034
+ id: targetId,
1035
+ sender_type: target.sender_type,
1036
+ preview: messagePreview(target.content)
1037
+ }
1038
+ };
1039
+ });
1040
+ }
1041
+ function chatMessageFromWire(message) {
1042
+ return chatMessagesFromWire([message])[0];
1043
+ }
1044
+ function chatRealtimeUpdateFromWire(envelope) {
1045
+ if (envelope.type === "message_created" && envelope.data) {
1046
+ return {
1047
+ kind: "message",
1048
+ message: chatMessageFromWire(envelope.data)
1049
+ };
1050
+ }
1051
+ if (envelope.type === "message_updated" && envelope.data) {
1052
+ return {
1053
+ kind: "message_updated",
1054
+ message: chatMessageFromWire(envelope.data)
1055
+ };
1056
+ }
1057
+ if (envelope.type === "read" && envelope.data) {
1058
+ const read = envelope.data;
1059
+ if (read.reader_side === "business" && typeof read.occurred_at === "string") {
1060
+ return { kind: "business_read", occurredAt: read.occurred_at };
1061
+ }
1062
+ return { kind: "none" };
1063
+ }
1064
+ if (envelope.type === "history_imported") {
1065
+ return { kind: "reload" };
1066
+ }
1067
+ if (envelope.type === "ai_mode_changed" && envelope.data) {
1068
+ const mode = envelope.data.mode;
1069
+ if (mode === "auto" || mode === "assist" || mode === "off") {
1070
+ return { kind: "ai_mode", mode };
1071
+ }
1072
+ return { kind: "none" };
1073
+ }
1074
+ if (envelope.type === "handoff_requested") {
1075
+ return { kind: "handoff" };
1076
+ }
1077
+ if (envelope.type === "resync") {
1078
+ return { kind: "resync", latestSequence: envelope.latest_sequence };
1079
+ }
1080
+ return { kind: "none" };
1081
+ }
1082
+
525
1083
  // src/internal/events.ts
526
1084
  var MUTATION_ERROR = "mutation:error";
527
1085
  var AUTH_TOKEN_CHANGED = "auth:token-changed";
@@ -584,11 +1142,13 @@ var SupportService = class {
584
1142
  * Claim the anonymous session for the verified Link identity: the server
585
1143
  * verifies the access token, rotates the session token (adopted here, so
586
1144
  * every subsequent request rides the bound session), folds the guest cart
587
- * in, and adopts or merges the widget conversation.
1145
+ * in, and opens or reuses the account's canonical direct Chat thread.
588
1146
  *
589
1147
  * Idempotent and single-flight: concurrent calls share one request, and a
590
1148
  * token already claimed by this instance short-circuits without a network
591
- * round trip. Emits `session:claimed` on the client's event channel.
1149
+ * round trip. A session already bound to another Link account is rejected
1150
+ * rather than transferring that account's cart. Emits `session:claimed` on
1151
+ * the client's event channel.
592
1152
  */
593
1153
  async claimConversation() {
594
1154
  const accessToken = this.client.getAccessToken();
@@ -625,7 +1185,7 @@ var SupportService = class {
625
1185
  this.client.publishSessionClaimed(this.claimedName);
626
1186
  this.client.events.emit(SESSION_CLAIMED, {
627
1187
  sessionToken: result.value.session_token,
628
- threadId: result.value.thread_id ?? null,
1188
+ threadId: result.value.thread.id,
629
1189
  customerName: result.value.customer_name
630
1190
  });
631
1191
  }
@@ -638,6 +1198,25 @@ var SupportService = class {
638
1198
  this.claimInFlightToken = null;
639
1199
  }
640
1200
  }
1201
+ /**
1202
+ * Retry verified external-history discovery for the current storefront.
1203
+ * Launch scope is intentionally WhatsApp-only because it is the registered
1204
+ * bidirectional transport. Results are per envelope and omit provider ids,
1205
+ * contact values, account ids, and message bodies.
1206
+ */
1207
+ async claimExternalChats() {
1208
+ if (!this.client.getAccessToken()) {
1209
+ return err(new CimplifyError("not_authenticated", "No access token to claim with"));
1210
+ }
1211
+ return safe(
1212
+ (async () => {
1213
+ const businessId = await this.client.resolveBusinessId();
1214
+ return this.client.post("/v1/me/chats/external-claims", {
1215
+ business_id: businessId
1216
+ });
1217
+ })()
1218
+ );
1219
+ }
641
1220
  /**
642
1221
  * Peek at the conversation without creating one: resolves the existing
643
1222
  * thread (or null) so a page load can hydrate history without littering
@@ -651,17 +1230,22 @@ var SupportService = class {
651
1230
  if (!result.value.conversation) return ok(null);
652
1231
  return ok({
653
1232
  conversation: result.value.conversation,
654
- messages: result.value.messages
1233
+ messages: chatMessagesFromWire(result.value.messages)
655
1234
  });
656
1235
  }
657
1236
  /** Open (or resume) the customer's widget conversation. */
658
1237
  async openConversation(opts) {
659
1238
  const idempotencyKey = opts?.idempotencyKey ?? generateIdempotencyKey();
660
- return safe(
1239
+ const result = await safe(
661
1240
  this.client.post("/api/v1/support/conversation", void 0, {
662
1241
  idempotencyKey
663
1242
  })
664
1243
  );
1244
+ if (!result.ok) return result;
1245
+ return ok({
1246
+ conversation: result.value.conversation,
1247
+ messages: chatMessagesFromWire(result.value.messages)
1248
+ });
665
1249
  }
666
1250
  /**
667
1251
  * Send a message in the active conversation.
@@ -673,7 +1257,7 @@ var SupportService = class {
673
1257
  */
674
1258
  async sendMessage(content, opts) {
675
1259
  const idempotencyKey = opts?.idempotencyKey ?? generateIdempotencyKey();
676
- return safe(
1260
+ const result = await safe(
677
1261
  this.client.post(
678
1262
  "/api/v1/support/conversation/messages",
679
1263
  {
@@ -685,6 +1269,7 @@ var SupportService = class {
685
1269
  { idempotencyKey }
686
1270
  )
687
1271
  );
1272
+ return result.ok ? ok(chatMessageFromWire(result.value.message)) : result;
688
1273
  }
689
1274
  /**
690
1275
  * Poll for messages. `after` pages forward (live updates); `before` pages
@@ -692,21 +1277,27 @@ var SupportService = class {
692
1277
  */
693
1278
  async getMessages(options = {}) {
694
1279
  const params = new URLSearchParams();
695
- if (options.after) params.set("after", options.after);
696
- if (options.before) params.set("before", options.before);
1280
+ if (options.afterSequence !== void 0)
1281
+ params.set("after_sequence", String(options.afterSequence));
1282
+ if (options.beforeSequence !== void 0)
1283
+ params.set("before_sequence", String(options.beforeSequence));
697
1284
  if (options.limit) params.set("limit", String(options.limit));
698
1285
  const query = params.toString();
699
1286
  const path = query ? `/api/v1/support/conversation/messages?${query}` : "/api/v1/support/conversation/messages";
700
- return safe(this.client.get(path));
1287
+ const result = await safe(this.client.get(path));
1288
+ return result.ok ? ok(chatMessagesFromWire(result.value)) : result;
701
1289
  }
702
1290
  /** React to a message in the active conversation by its `ChatMessage.id`. */
703
1291
  async reactToMessage(messageId, emoji) {
704
- return safe(
1292
+ const clientId = generateIdempotencyKey();
1293
+ const result = await safe(
705
1294
  this.client.post(
706
1295
  `/api/v1/support/conversation/messages/${messageId}/reactions`,
707
- { emoji }
1296
+ { emoji, client_id: clientId },
1297
+ { idempotencyKey: clientId }
708
1298
  )
709
1299
  );
1300
+ return result.ok ? ok(chatMessageFromWire(result.value.message)) : result;
710
1301
  }
711
1302
  /** Ask for a person: flags the thread for the merchant's inbox. */
712
1303
  async requestHuman(opts) {
@@ -721,456 +1312,108 @@ var SupportService = class {
721
1312
  }
722
1313
  };
723
1314
 
724
- // src/payment-authorization/providers/paystack.ts
725
- async function openPaystackPopup(options, signal) {
726
- if (typeof window === "undefined") {
727
- return { success: false, error: "PROVIDER_UNAVAILABLE" };
728
- }
729
- let PaystackPop;
730
- try {
731
- const imported = await import('@paystack/inline-js');
732
- PaystackPop = imported.default;
733
- } catch {
734
- return { success: false, error: "PROVIDER_UNAVAILABLE" };
735
- }
736
- return new Promise((resolve) => {
737
- let settled = false;
738
- let providerStarted = false;
739
- const resolveOnce = (result) => {
740
- if (settled) {
741
- return;
742
- }
743
- settled = true;
744
- if (signal) {
745
- signal.removeEventListener("abort", onAbort);
746
- }
747
- resolve(result);
748
- };
749
- const onAbort = () => {
750
- resolveOnce({
751
- success: false,
752
- // Once Paystack owns the transaction, cancellation is ambiguous. The
753
- // adapter converts this into submitted so Cimplify polling decides.
754
- error: providerStarted ? "SUBMISSION_UNKNOWN" : "CANCELLED"
755
- });
756
- };
757
- if (signal?.aborted) {
758
- resolveOnce({ success: false, error: "CANCELLED" });
759
- return;
760
- }
761
- if (signal) {
762
- signal.addEventListener("abort", onAbort, { once: true });
763
- }
764
- try {
765
- const popup = new PaystackPop();
766
- popup.newTransaction({
767
- key: options.key,
768
- email: options.email,
769
- amount: options.amount,
770
- currency: options.currency,
771
- reference: options.reference,
772
- accessCode: options.accessCode,
773
- onSuccess: (transaction) => {
774
- resolveOnce({
775
- success: true,
776
- reference: transaction.reference ?? options.reference
777
- });
778
- },
779
- onCancel: () => {
780
- resolveOnce({ success: false, error: "PAYMENT_CANCELLED" });
781
- },
782
- onError: (error) => {
783
- resolveOnce({
784
- success: false,
785
- error: error?.message || "PAYMENT_FAILED"
786
- });
787
- }
788
- });
789
- providerStarted = true;
790
- } catch {
791
- resolveOnce({ success: false, error: "POPUP_BLOCKED" });
792
- }
793
- });
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;
794
1321
  }
795
- var PaystackAuthorizationAdapter = class {
796
- constructor(openPopup = openPaystackPopup) {
797
- this.openPopup = openPopup;
798
- this.provider = "paystack";
799
- }
800
- async authorize(context) {
801
- const result = await this.openPopup(
802
- {
803
- key: context.action.public_key,
804
- email: context.email,
805
- accessCode: context.action.client_secret,
806
- reference: context.reference || context.action.client_secret,
807
- currency: context.currency
808
- },
809
- context.signal
810
- );
811
- if (result.success) {
812
- return { status: "submitted", reference: result.reference || context.reference };
813
- }
814
- if (result.error === "SUBMISSION_UNKNOWN") {
815
- return { status: "submitted", reference: context.reference };
816
- }
817
- if (!result.error || result.error === "CANCELLED" || result.error === "PAYMENT_CANCELLED") {
818
- return { status: "cancelled" };
819
- }
820
- if (result.error === "POPUP_BLOCKED") {
821
- return {
822
- status: "failed",
823
- code: "POPUP_BLOCKED",
824
- message: "Unable to open the Paystack payment popup. Please allow popups and try again.",
825
- recoverable: true
826
- };
827
- }
828
- if (result.error === "PROVIDER_UNAVAILABLE") {
829
- return {
830
- status: "failed",
831
- code: "PROVIDER_UNAVAILABLE",
832
- message: "Paystack is unavailable in this environment.",
833
- recoverable: false
834
- };
835
- }
836
- return {
837
- status: "failed",
838
- code: "PAYMENT_FAILED",
839
- message: result.error || "Paystack payment could not be submitted.",
840
- recoverable: true
841
- };
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);
842
1334
  }
843
- };
844
-
845
- // src/payment-authorization/providers/stripe.ts
846
- async function loadStripeFromPeer(publicKey) {
1335
+ }
1336
+ function parseSessionSyncPayload(serialized) {
1337
+ if (!serialized) return null;
847
1338
  try {
848
- const imported = await import('@stripe/stripe-js');
849
- return imported.loadStripe ? imported.loadStripe(publicKey) : 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;
850
1341
  } catch {
851
1342
  return null;
852
1343
  }
853
1344
  }
854
- function createStripeModal() {
855
- if (typeof document === "undefined" || !document.body) return null;
856
- const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
857
- const root = document.createElement("div");
858
- root.setAttribute("role", "dialog");
859
- root.setAttribute("aria-modal", "true");
860
- root.setAttribute("aria-label", "Secure card payment");
861
- Object.assign(root.style, {
862
- position: "fixed",
863
- inset: "0",
864
- zIndex: "2147483647",
865
- display: "grid",
866
- placeItems: "center",
867
- padding: "16px",
868
- background: "rgba(15, 23, 42, 0.58)"
869
- });
870
- const form = document.createElement("form");
871
- Object.assign(form.style, {
872
- width: "min(100%, 480px)",
873
- maxHeight: "calc(100vh - 32px)",
874
- overflow: "auto",
875
- borderRadius: "12px",
876
- padding: "24px",
877
- background: "#fff",
878
- boxShadow: "0 24px 80px rgba(15, 23, 42, 0.32)"
879
- });
880
- const title = document.createElement("h2");
881
- title.textContent = "Complete card payment";
882
- Object.assign(title.style, { margin: "0 0 18px", font: "600 18px/1.4 system-ui" });
883
- const paymentMount = document.createElement("div");
884
- paymentMount.setAttribute("data-cimplify-stripe-payment-element", "");
885
- const error = document.createElement("p");
886
- error.setAttribute("role", "alert");
887
- Object.assign(error.style, {
888
- minHeight: "20px",
889
- margin: "14px 0 0",
890
- color: "#b91c1c",
891
- font: "14px/1.4 system-ui"
892
- });
893
- const actions = document.createElement("div");
894
- Object.assign(actions.style, {
895
- display: "flex",
896
- justifyContent: "flex-end",
897
- gap: "10px",
898
- marginTop: "18px"
899
- });
900
- const cancelButton = document.createElement("button");
901
- cancelButton.type = "button";
902
- cancelButton.textContent = "Cancel";
903
- Object.assign(cancelButton.style, {
904
- border: "1px solid #cbd5e1",
905
- borderRadius: "8px",
906
- padding: "10px 16px",
907
- background: "#fff",
908
- color: "#0f172a",
909
- cursor: "pointer"
910
- });
911
- const submitButton = document.createElement("button");
912
- submitButton.type = "submit";
913
- submitButton.textContent = "Pay securely";
914
- Object.assign(submitButton.style, {
915
- border: "0",
916
- borderRadius: "8px",
917
- padding: "10px 16px",
918
- background: "#0f172a",
919
- color: "#fff",
920
- cursor: "pointer"
921
- });
922
- actions.append(cancelButton, submitButton);
923
- form.append(title, paymentMount, error, actions);
924
- root.append(form);
925
- document.body.append(root);
926
- cancelButton.focus();
927
- 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]);
928
1348
  }
929
- function stripeFailure(error) {
930
- const message = error?.message || "Stripe could not authorize this payment.";
931
- if (error?.code === "payment_intent_authentication_failure" || error?.type === "invalid_request_error") {
932
- return {
933
- status: "failed",
934
- code: "AUTHORIZATION_FAILED",
935
- message,
936
- recoverable: true
937
- };
938
- }
939
- 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;
940
1357
  }
941
- var StripeAuthorizationAdapter = class {
942
- constructor(loadStripe = loadStripeFromPeer) {
943
- this.loadStripe = loadStripe;
944
- this.provider = "stripe";
945
- }
946
- async authorize(context) {
947
- if (typeof window === "undefined" || typeof document === "undefined" || context.signal?.aborted) {
948
- return context.signal?.aborted ? { status: "cancelled" } : {
949
- status: "failed",
950
- code: "PROVIDER_UNAVAILABLE",
951
- message: "Stripe Payment Element is unavailable in this environment.",
952
- recoverable: false
953
- };
954
- }
955
- let stripe;
956
- try {
957
- stripe = await this.loadStripe(context.action.public_key);
958
- } catch (error) {
959
- return {
960
- status: "failed",
961
- code: "PROVIDER_UNAVAILABLE",
962
- message: error instanceof Error ? error.message : "Stripe.js could not be loaded.",
963
- recoverable: true
964
- };
965
- }
966
- if (context.signal?.aborted) {
967
- return { status: "cancelled" };
968
- }
969
- if (!stripe) {
970
- return {
971
- status: "failed",
972
- code: "PROVIDER_UNAVAILABLE",
973
- message: "Stripe.js could not be loaded.",
974
- recoverable: false
975
- };
976
- }
977
- const modal = createStripeModal();
978
- if (!modal) {
979
- return {
980
- status: "failed",
981
- code: "PROVIDER_UNAVAILABLE",
982
- message: "Stripe Payment Element could not be mounted.",
983
- recoverable: false
984
- };
985
- }
986
- let paymentElement;
987
- let elements;
988
- try {
989
- elements = stripe.elements({ clientSecret: context.action.client_secret });
990
- paymentElement = elements.create("payment");
991
- paymentElement.mount(modal.paymentMount);
992
- } catch {
993
- modal.root.remove();
994
- modal.previousFocus?.focus();
995
- return {
996
- status: "failed",
997
- code: "PROVIDER_UNAVAILABLE",
998
- message: "Stripe Payment Element could not be initialized.",
999
- recoverable: false
1000
- };
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
+ }
1001
1375
  }
1002
- if (context.signal?.aborted) {
1003
- if (paymentElement.destroy) paymentElement.destroy();
1004
- else paymentElement.unmount?.();
1005
- modal.root.remove();
1006
- modal.previousFocus?.focus();
1007
- return { status: "cancelled" };
1008
- }
1009
- return new Promise((resolve) => {
1010
- let settled = false;
1011
- let submitting = false;
1012
- let confirmationStarted = false;
1013
- const cleanup = () => {
1014
- context.signal?.removeEventListener("abort", onAbort);
1015
- modal.form.removeEventListener("submit", onSubmit);
1016
- modal.cancelButton.removeEventListener("click", onCancel);
1017
- if (paymentElement.destroy) paymentElement.destroy();
1018
- else paymentElement.unmount?.();
1019
- modal.root.remove();
1020
- modal.previousFocus?.focus();
1021
- };
1022
- const settle = (outcome) => {
1023
- if (settled) return;
1024
- settled = true;
1025
- cleanup();
1026
- resolve(outcome);
1027
- };
1028
- const onAbort = () => {
1029
- if (confirmationStarted) {
1030
- settle({ status: "submitted", reference: context.reference });
1031
- return;
1032
- }
1033
- settle({ status: "cancelled" });
1034
- };
1035
- const onCancel = () => {
1036
- if (!confirmationStarted) settle({ status: "cancelled" });
1037
- };
1038
- const onSubmit = async (event) => {
1039
- event.preventDefault();
1040
- if (submitting || settled) return;
1041
- submitting = true;
1042
- modal.submitButton.disabled = true;
1043
- modal.error.textContent = "";
1044
- try {
1045
- const validation = elements.submit ? await elements.submit() : {};
1046
- if (validation.error) {
1047
- modal.error.textContent = validation.error.message || "Check your payment details.";
1048
- submitting = false;
1049
- modal.submitButton.disabled = false;
1050
- return;
1051
- }
1052
- if (settled || context.signal?.aborted) {
1053
- settle({ status: "cancelled" });
1054
- return;
1055
- }
1056
- confirmationStarted = true;
1057
- modal.cancelButton.disabled = true;
1058
- const confirmation = await stripe.confirmPayment({
1059
- elements,
1060
- confirmParams: { return_url: context.returnUrl || window.location.href },
1061
- redirect: "if_required"
1062
- });
1063
- if (settled) return;
1064
- if (confirmation.error) {
1065
- settle(stripeFailure(confirmation.error));
1066
- return;
1067
- }
1068
- settle({
1069
- status: "submitted",
1070
- reference: confirmation.paymentIntent?.id || context.reference
1071
- });
1072
- } catch (error) {
1073
- if (confirmationStarted) {
1074
- settle({ status: "submitted", reference: context.reference });
1075
- return;
1076
- }
1077
- settle(
1078
- stripeFailure({
1079
- message: error instanceof Error ? error.message : "Stripe confirmation failed."
1080
- })
1081
- );
1082
- }
1083
- };
1084
- modal.form.addEventListener("submit", onSubmit);
1085
- modal.cancelButton.addEventListener("click", onCancel);
1086
- context.signal?.addEventListener("abort", onAbort, { once: true });
1087
- paymentElement.on?.("escape", onCancel);
1088
- paymentElement.on?.("loaderror", (event) => {
1089
- if (confirmationStarted) return;
1090
- settle({
1091
- status: "failed",
1092
- code: "PROVIDER_UNAVAILABLE",
1093
- message: event?.error?.message || "Stripe Payment Element failed to load.",
1094
- recoverable: true
1095
- });
1096
- });
1097
- });
1098
- }
1099
- };
1100
-
1101
- // src/payment-authorization/registry.ts
1102
- function normalizeProvider(provider) {
1103
- 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;
1104
1386
  }
1105
- var PaymentAuthorizationAdapterRegistry = class {
1106
- constructor(adapters = []) {
1107
- this.adapters = /* @__PURE__ */ new Map();
1108
- for (const adapter of adapters) {
1109
- this.register(adapter);
1110
- }
1111
- }
1112
- register(adapter) {
1113
- const provider = normalizeProvider(adapter.provider);
1114
- if (!provider) {
1115
- throw new TypeError("Payment authorization adapter provider cannot be empty.");
1116
- }
1117
- this.adapters.set(provider, adapter);
1118
- return this;
1119
- }
1120
- unregister(provider) {
1121
- return this.adapters.delete(normalizeProvider(provider));
1122
- }
1123
- get(provider) {
1124
- return this.adapters.get(normalizeProvider(provider));
1125
- }
1126
- supports(provider) {
1127
- return this.adapters.has(normalizeProvider(provider));
1128
- }
1129
- providers() {
1130
- return [...this.adapters.keys()];
1131
- }
1132
- async authorize(action, options) {
1133
- const provider = normalizeProvider(action.provider);
1134
- if (!provider) {
1135
- return {
1136
- status: "failed",
1137
- code: "PROVIDER_UNAVAILABLE",
1138
- message: "Card authorization response did not identify a payment provider.",
1139
- recoverable: false
1140
- };
1141
- }
1142
- const adapter = this.adapters.get(provider);
1143
- if (!adapter) {
1144
- return {
1145
- status: "failed",
1146
- code: "PROVIDER_UNAVAILABLE",
1147
- message: `Card authorization provider '${provider}' is not supported by this SDK client.`,
1148
- recoverable: false
1149
- };
1150
- }
1151
- try {
1152
- const normalizedAction = action.provider === provider ? action : { ...action, provider };
1153
- return await adapter.authorize({ ...options, action: normalizedAction });
1154
- } catch (error) {
1155
- return {
1156
- status: "failed",
1157
- code: "PAYMENT_FAILED",
1158
- message: error instanceof Error ? error.message : `Card authorization provider '${provider}' failed unexpectedly.`,
1159
- recoverable: true
1160
- };
1161
- }
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
+ };
1162
1395
  }
1163
- };
1164
- function createDefaultPaymentAuthorizationAdapterRegistry() {
1165
- return new PaymentAuthorizationAdapterRegistry([
1166
- new PaystackAuthorizationAdapter(),
1167
- new StripeAuthorizationAdapter()
1168
- ]);
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
+ };
1169
1413
  }
1170
1414
 
1171
1415
  // src/client.ts
1172
1416
  var SESSION_TOKEN_STORAGE_KEY = "cimplify_session_token";
1173
- var SESSION_SYNC_STORAGE_KEY = "cimplify_session_sync";
1174
1417
  var ORDER_TOKEN_PREFIX = "cimplify_ot_";
1175
1418
  var SESSION_TOKEN_HEADER = "x-session-token";
1176
1419
  var LOCATION_ID_QUERY_PARAMETER = "location_id";
@@ -1178,8 +1421,37 @@ var ORDER_TOKEN_TTL_MS = 24 * 60 * 60 * 1e3;
1178
1421
  var DEFAULT_TIMEOUT_MS = 5e3;
1179
1422
  var DEFAULT_MAX_RETRIES = 3;
1180
1423
  var DEFAULT_RETRY_DELAY_MS = 1e3;
1181
- function sleep(ms) {
1182
- 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());
1183
1455
  }
1184
1456
  function nextInit(cacheOptions) {
1185
1457
  if (!cacheOptions) return {};
@@ -1293,6 +1565,9 @@ var CimplifyClient = class {
1293
1565
  this.businessId = null;
1294
1566
  this.businessIdResolvePromise = null;
1295
1567
  this.publicKeyEpoch = 0;
1568
+ this.stopCrossTabSessionWatch = null;
1569
+ this.latestSessionSyncAt = Number.NEGATIVE_INFINITY;
1570
+ this.latestSessionSyncOrderKey = null;
1296
1571
  this.inflightRequests = /* @__PURE__ */ new Map();
1297
1572
  /**
1298
1573
  * App-level event channel. Subscribe to surface mutation rollbacks, outbox
@@ -1314,7 +1589,11 @@ var CimplifyClient = class {
1314
1589
  this.authorizationAdapters = config.authorizationAdapterRegistry ?? createDefaultPaymentAuthorizationAdapterRegistry();
1315
1590
  this.fetchImpl = (config.fetch ?? globalThis.fetch).bind(globalThis);
1316
1591
  this.sessionToken = this.loadSessionToken() ?? this.mintSessionToken();
1317
- this.watchCrossTabSession();
1592
+ const persistedSessionSync = this.loadSessionSyncPayload();
1593
+ if (persistedSessionSync) this.observeSessionSync(persistedSessionSync);
1594
+ this.stopCrossTabSessionWatch = subscribeCrossTabSession((payload) => {
1595
+ this.applyCrossTabSession(payload);
1596
+ });
1318
1597
  if (!this.publicKey && !config.suppressPublicKeyWarning) {
1319
1598
  console.warn(
1320
1599
  '[Cimplify] No public key found. Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in your environment, or pass { publicKey: "cpk_..." } to createCimplifyClient().'
@@ -1341,6 +1620,12 @@ var CimplifyClient = class {
1341
1620
  this.stopPageViewTracking?.();
1342
1621
  this.stopPageViewTracking = null;
1343
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
+ }
1344
1629
  getAccessToken() {
1345
1630
  return this.accessToken;
1346
1631
  }
@@ -1368,7 +1653,7 @@ var CimplifyClient = class {
1368
1653
  this.hooks.onSessionChange?.({
1369
1654
  previousToken: previous,
1370
1655
  newToken: token,
1371
- source: "manual"
1656
+ source: SESSION_CHANGE_SOURCE.MANUAL
1372
1657
  });
1373
1658
  this.events.emit(AUTH_TOKEN_CHANGED, { token });
1374
1659
  }
@@ -1377,7 +1662,8 @@ var CimplifyClient = class {
1377
1662
  * refresh restore) binds the anonymous session so the conversation, cart
1378
1663
  * and orders survive a cleared browser. Best-effort by design — a failed
1379
1664
  * claim leaves the session anonymous, exactly as it was. Called by the
1380
- * auth flows, never by the raw token setter.
1665
+ * merchant auth flows, never by the raw token setter. Global Link clients
1666
+ * have no storefront tenant and therefore activate without this step.
1381
1667
  */
1382
1668
  autoClaimChatIdentity() {
1383
1669
  if (typeof window === "undefined" || !this.sessionToken || !this.accessToken) return;
@@ -1411,7 +1697,7 @@ var CimplifyClient = class {
1411
1697
  this.hooks.onSessionChange?.({
1412
1698
  previousToken: previous,
1413
1699
  newToken: null,
1414
- source: "clear"
1700
+ source: SESSION_CHANGE_SOURCE.CLEAR
1415
1701
  });
1416
1702
  this.events.emit(AUTH_TOKEN_CHANGED, { token: null });
1417
1703
  this.events.emit(SESSION_CLEARED, {});
@@ -1419,9 +1705,29 @@ var CimplifyClient = class {
1419
1705
  getSessionEpoch() {
1420
1706
  return this.sessionEpoch;
1421
1707
  }
1708
+ /** Monotonic fence for async work scoped to the current storefront tenant. */
1709
+ getPublicKeyEpoch() {
1710
+ return this.publicKeyEpoch;
1711
+ }
1422
1712
  getSessionToken() {
1423
1713
  return this.sessionToken;
1424
1714
  }
1715
+ /** Whether the current transport session is known to have been claimed for
1716
+ * a Link identity, even when its in-memory bearer is absent after reload or
1717
+ * a failed refresh. The sync marker is written only after canonical claim
1718
+ * rotation and is replaced with `cleared` when a fresh session is minted. */
1719
+ hasClaimedSessionAuthority() {
1720
+ if (!this.sessionToken) return false;
1721
+ try {
1722
+ const payload = parseSessionSyncPayload(
1723
+ this.safeStorage()?.getItem(SESSION_SYNC_STORAGE_KEY) ?? null
1724
+ );
1725
+ if (!payload) return false;
1726
+ return payload.type === "claimed" && payload.token === this.sessionToken;
1727
+ } catch {
1728
+ return false;
1729
+ }
1730
+ }
1425
1731
  publishSessionClaimed(customerName) {
1426
1732
  if (!this.sessionToken) return;
1427
1733
  this.broadcastSessionSync({
@@ -1431,41 +1737,49 @@ var CimplifyClient = class {
1431
1737
  });
1432
1738
  }
1433
1739
  broadcastSessionSync(payload) {
1740
+ const envelope = {
1741
+ ...payload,
1742
+ at: this.nextSessionSyncTimestamp()
1743
+ };
1434
1744
  try {
1435
- this.safeStorage()?.setItem(
1436
- SESSION_SYNC_STORAGE_KEY,
1437
- JSON.stringify({ ...payload, at: Date.now() })
1438
- );
1745
+ this.safeStorage()?.setItem(SESSION_SYNC_STORAGE_KEY, JSON.stringify(envelope));
1439
1746
  } catch {
1440
1747
  }
1748
+ publishLocalSessionSync(envelope);
1441
1749
  }
1442
- watchCrossTabSession() {
1443
- if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
1444
- window.addEventListener("storage", (event) => {
1445
- if (event.key !== SESSION_SYNC_STORAGE_KEY || !event.newValue) return;
1446
- let payload;
1447
- try {
1448
- payload = JSON.parse(event.newValue);
1449
- } catch {
1450
- return;
1451
- }
1452
- if (!payload?.token || payload.token === this.sessionToken) return;
1453
- if (payload.type === "claimed") {
1454
- this.sessionToken = payload.token;
1455
- this.sessionEpoch++;
1456
- this.events.emit(SESSION_CLAIMED, {
1457
- sessionToken: payload.token,
1458
- threadId: null,
1459
- customerName: payload.name ?? ""
1460
- });
1461
- } else if (payload.type === "cleared") {
1462
- this.accessToken = null;
1463
- this.sessionToken = payload.token;
1464
- this.sessionEpoch++;
1465
- this.events.emit(AUTH_TOKEN_CHANGED, { token: null });
1466
- this.events.emit(SESSION_CLEARED, {});
1467
- }
1468
- });
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;
1469
1783
  }
1470
1784
  setOrderToken(orderId, token) {
1471
1785
  try {
@@ -1529,17 +1843,24 @@ var CimplifyClient = class {
1529
1843
  setPublicKey(publicKey) {
1530
1844
  const normalized = publicKey.trim();
1531
1845
  if (normalized === this.publicKey) return;
1846
+ const restartPageViewTracking = this.stopPageViewTracking !== null;
1847
+ if (restartPageViewTracking) {
1848
+ this.stopTrackingPageViews();
1849
+ }
1532
1850
  this.publicKey = normalized;
1533
1851
  this.publicKeyEpoch += 1;
1534
1852
  this.businessId = null;
1535
1853
  this.businessIdResolvePromise = null;
1536
1854
  this.inflightRequests.clear();
1537
- this._activity?.resetSession();
1855
+ this._activity?.resetForStorefrontIdentityChange();
1538
1856
  delete this.context.location_id;
1539
1857
  if (this.accessToken !== null) {
1540
1858
  this.setAccessToken(null);
1541
1859
  }
1542
1860
  this.events.emit(SESSION_CLEARED, {});
1861
+ if (restartPageViewTracking) {
1862
+ this.stopPageViewTracking = this.activity.autoTrackPageViews();
1863
+ }
1543
1864
  }
1544
1865
  getBusinessId() {
1545
1866
  return this.businessId;
@@ -1580,8 +1901,8 @@ var CimplifyClient = class {
1580
1901
  }
1581
1902
  }
1582
1903
  }
1583
- async authedFetch(url, options, timeoutMs) {
1584
- return this.resilientFetch(url, options, timeoutMs);
1904
+ async authedFetch(url, options, timeoutMs, retryPolicy, signal) {
1905
+ return this.resilientFetch(url, options, timeoutMs, retryPolicy, signal);
1585
1906
  }
1586
1907
  // Storage guards: even the `window.localStorage` property access throws in
1587
1908
  // some sandboxed iframes and strict private-browsing modes. These run in the
@@ -1603,6 +1924,13 @@ var CimplifyClient = class {
1603
1924
  return null;
1604
1925
  }
1605
1926
  }
1927
+ loadSessionSyncPayload() {
1928
+ try {
1929
+ return parseSessionSyncPayload(this.safeStorage()?.getItem(SESSION_SYNC_STORAGE_KEY) ?? null);
1930
+ } catch {
1931
+ return null;
1932
+ }
1933
+ }
1606
1934
  /**
1607
1935
  * Mint the session token client-side so the very first request already
1608
1936
  * carries it. Without this, parallel first-load requests each get a
@@ -1658,7 +1986,7 @@ var CimplifyClient = class {
1658
1986
  }
1659
1987
  return headers;
1660
1988
  }
1661
- async resilientFetch(url, options, timeoutMs) {
1989
+ async resilientFetch(url, options, timeoutMs, retryPolicy, signal) {
1662
1990
  const method = options.method || "GET";
1663
1991
  const path = url.replace(this.baseUrl, "").replace(this.linkApiUrl, "");
1664
1992
  const startTime = Date.now();
@@ -1672,15 +2000,31 @@ var CimplifyClient = class {
1672
2000
  };
1673
2001
  this.hooks.onRequestStart?.(context);
1674
2002
  let lastError;
1675
- 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++) {
1676
2010
  try {
2011
+ if (signal?.aborted) {
2012
+ throw new CimplifyError(ErrorCode.CANCELLED, "Request was cancelled.", true);
2013
+ }
1677
2014
  const controller = new AbortController();
1678
2015
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs ?? this.timeout);
1679
- const response = await this.fetchImpl(url, {
1680
- ...options,
1681
- signal: controller.signal
1682
- });
1683
- 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
+ }
1684
2028
  this.captureSessionToken(response);
1685
2029
  if (response.ok) {
1686
2030
  this.hooks.onRequestSuccess?.({
@@ -1690,17 +2034,19 @@ var CimplifyClient = class {
1690
2034
  });
1691
2035
  return response;
1692
2036
  }
1693
- if ((response.status === 429 || response.status === 503) && attempt < this.maxRetries) {
2037
+ if ((response.status === 429 || response.status === 503) && attempt < maxRetries) {
1694
2038
  retryCount++;
1695
- const retryAfter = response.headers.get("Retry-After");
1696
- 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);
1697
2043
  this.hooks.onRetry?.({
1698
2044
  ...context,
1699
2045
  attempt: retryCount,
1700
2046
  delayMs: delay,
1701
2047
  error: new Error(`Retryable: ${response.status}`)
1702
2048
  });
1703
- await sleep(delay);
2049
+ await sleep(delay, signal);
1704
2050
  continue;
1705
2051
  }
1706
2052
  if (response.status >= 400 && response.status < 500) {
@@ -1718,22 +2064,22 @@ var CimplifyClient = class {
1718
2064
  });
1719
2065
  return response;
1720
2066
  }
1721
- if (response.status >= 500 && attempt < this.maxRetries) {
2067
+ if (response.status >= 500 && attempt < maxRetries) {
1722
2068
  retryCount++;
1723
- const delay = this.retryDelay * Math.pow(2, attempt);
2069
+ const delay = withPositiveJitter(this.retryDelay * Math.pow(2, attempt), jitterRatio);
1724
2070
  this.hooks.onRetry?.({
1725
2071
  ...context,
1726
2072
  attempt: retryCount,
1727
2073
  delayMs: delay,
1728
2074
  error: new Error(`Server error: ${response.status}`)
1729
2075
  });
1730
- await sleep(delay);
2076
+ await sleep(delay, signal);
1731
2077
  continue;
1732
2078
  }
1733
2079
  this.hooks.onRequestError?.({
1734
2080
  ...context,
1735
2081
  error: new CimplifyError(
1736
- "SERVER_ERROR",
2082
+ ErrorCode.SERVER_ERROR,
1737
2083
  `Server error ${response.status} after ${retryCount} retries`,
1738
2084
  false
1739
2085
  ),
@@ -1745,9 +2091,12 @@ var CimplifyClient = class {
1745
2091
  return response;
1746
2092
  } catch (error) {
1747
2093
  lastError = error;
2094
+ if (signal?.aborted) {
2095
+ throw new CimplifyError(ErrorCode.CANCELLED, "Request was cancelled.", true);
2096
+ }
1748
2097
  const networkError = toNetworkError(error, this.isTestMode());
1749
2098
  const errorRetryable = isRetryable(error);
1750
- if (!errorRetryable || attempt >= this.maxRetries) {
2099
+ if (!errorRetryable || attempt >= maxRetries) {
1751
2100
  this.hooks.onRequestError?.({
1752
2101
  ...context,
1753
2102
  error: networkError,
@@ -1758,14 +2107,14 @@ var CimplifyClient = class {
1758
2107
  throw networkError;
1759
2108
  }
1760
2109
  retryCount++;
1761
- const delay = this.retryDelay * Math.pow(2, attempt);
2110
+ const delay = withPositiveJitter(this.retryDelay * Math.pow(2, attempt), jitterRatio);
1762
2111
  this.hooks.onRetry?.({
1763
2112
  ...context,
1764
2113
  attempt: retryCount,
1765
2114
  delayMs: delay,
1766
2115
  error: networkError
1767
2116
  });
1768
- await sleep(delay);
2117
+ await sleep(delay, signal);
1769
2118
  }
1770
2119
  }
1771
2120
  const finalError = toNetworkError(lastError, this.isTestMode());
@@ -1802,16 +2151,24 @@ var CimplifyClient = class {
1802
2151
  return request;
1803
2152
  }
1804
2153
  async get(path, opts) {
1805
- const key = this.getDedupeKey("get", path, opts?.cacheOptions);
1806
- return this.deduplicatedRequest(key, async () => {
1807
- const response = await this.authedFetch(this.buildUrl(this.baseUrl, path), {
1808
- method: "GET",
1809
- credentials: this.credentials,
1810
- headers: this.getHeaders(),
1811
- ...nextInit(opts?.cacheOptions)
1812
- });
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
+ );
1813
2167
  return this.handleRestResponse(response);
1814
- });
2168
+ };
2169
+ if (opts?.signal) return request();
2170
+ const key = this.getDedupeKey("get", path, opts?.cacheOptions);
2171
+ return this.deduplicatedRequest(key, request);
1815
2172
  }
1816
2173
  async post(path, body, opts) {
1817
2174
  const response = await this.authedFetch(
@@ -1822,7 +2179,9 @@ var CimplifyClient = class {
1822
2179
  headers: mergeOptionHeaders(this.getHeaders(), opts),
1823
2180
  body: body ? JSON.stringify(body) : void 0
1824
2181
  },
1825
- opts?.timeoutMs
2182
+ opts?.timeoutMs,
2183
+ opts?.retryPolicy,
2184
+ opts?.signal
1826
2185
  );
1827
2186
  return this.handleRestResponse(response);
1828
2187
  }
@@ -1835,7 +2194,9 @@ var CimplifyClient = class {
1835
2194
  headers: mergeOptionHeaders(this.getHeaders(), opts),
1836
2195
  body: body ? JSON.stringify(body) : void 0
1837
2196
  },
1838
- opts?.timeoutMs
2197
+ opts?.timeoutMs,
2198
+ opts?.retryPolicy,
2199
+ opts?.signal
1839
2200
  );
1840
2201
  return this.handleRestResponse(response);
1841
2202
  }
@@ -1847,21 +2208,31 @@ var CimplifyClient = class {
1847
2208
  credentials: this.credentials,
1848
2209
  headers: mergeOptionHeaders(this.getHeaders(), opts)
1849
2210
  },
1850
- opts?.timeoutMs
2211
+ opts?.timeoutMs,
2212
+ opts?.retryPolicy,
2213
+ opts?.signal
1851
2214
  );
1852
2215
  return this.handleRestResponse(response);
1853
2216
  }
1854
2217
  async linkGet(path, opts) {
1855
- const key = this.getDedupeKey("linkGet", path, opts?.cacheOptions);
1856
- return this.deduplicatedRequest(key, async () => {
1857
- const response = await this.authedFetch(`${this.linkApiUrl}${path}`, {
1858
- method: "GET",
1859
- credentials: this.credentials,
1860
- headers: this.getHeaders(),
1861
- ...nextInit(opts?.cacheOptions)
1862
- });
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
+ );
1863
2231
  return this.handleRestResponse(response);
1864
- });
2232
+ };
2233
+ if (opts?.signal) return request();
2234
+ const key = this.getDedupeKey("linkGet", path, opts?.cacheOptions);
2235
+ return this.deduplicatedRequest(key, request);
1865
2236
  }
1866
2237
  async linkPost(path, body, opts) {
1867
2238
  const response = await this.authedFetch(
@@ -1872,25 +2243,33 @@ var CimplifyClient = class {
1872
2243
  headers: mergeOptionHeaders(this.getHeaders(), opts),
1873
2244
  body: body ? JSON.stringify(body) : void 0
1874
2245
  },
1875
- opts?.timeoutMs
2246
+ opts?.timeoutMs,
2247
+ opts?.retryPolicy,
2248
+ opts?.signal
1876
2249
  );
1877
2250
  return this.handleRestResponse(response);
1878
2251
  }
1879
2252
  async linkDelete(path, opts) {
1880
- const response = await this.authedFetch(`${this.linkApiUrl}${path}`, {
1881
- method: "DELETE",
1882
- credentials: this.credentials,
1883
- headers: mergeOptionHeaders(this.getHeaders(), opts)
1884
- });
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
+ );
1885
2264
  return this.handleRestResponse(response);
1886
2265
  }
1887
2266
  async handleRestResponse(response) {
1888
2267
  const json = await response.json();
1889
2268
  if (!response.ok) {
1890
- 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;
1891
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";
1892
2271
  const retryable = typeof json.error === "object" && typeof json.error?.retryable === "boolean" ? json.error.retryable : false;
1893
- if (errorCode === "IDEMPOTENCY_MISMATCH") {
2272
+ if (errorCode === ErrorCode.IDEMPOTENCY_MISMATCH) {
1894
2273
  const originalCreatedAt = typeof json.error === "object" && json.error?.original_created_at || "";
1895
2274
  throw new IdempotencyMismatchError(errorMessage, originalCreatedAt);
1896
2275
  }
@@ -1907,7 +2286,7 @@ var CimplifyClient = class {
1907
2286
  if (json?.success === false || json?.error?.code || json?.error?.message) {
1908
2287
  const error = enrichError(
1909
2288
  new CimplifyError(
1910
- json.error?.code || "API_ERROR",
2289
+ json.error?.code || ErrorCode.API_ERROR,
1911
2290
  json.error?.message || "An error occurred",
1912
2291
  json.error?.retryable || false
1913
2292
  ),
@@ -1938,6 +2317,12 @@ var CimplifyClient = class {
1938
2317
  }
1939
2318
  return this._checkout;
1940
2319
  }
2320
+ get purchaseIntents() {
2321
+ if (!this._purchaseIntents) {
2322
+ this._purchaseIntents = new PurchaseIntentService(this);
2323
+ }
2324
+ return this._purchaseIntents;
2325
+ }
1941
2326
  get orders() {
1942
2327
  if (!this._orders) {
1943
2328
  this._orders = new OrderQueries(this);
@@ -2048,4 +2433,4 @@ function createCimplifyClient(config = {}) {
2048
2433
  return new CimplifyClient(config);
2049
2434
  }
2050
2435
 
2051
- export { AUTH_TOKEN_CHANGED, ActivityService, CimplifyClient, DeliveryService, MUTATION_ERROR, PaymentAuthorizationAdapterRegistry, PaystackAuthorizationAdapter, PlacesService, SESSION_CLAIMED, SESSION_CLEARED, StoreCreditQueries, StripeAuthorizationAdapter, SubscriptionService, SupportService, UploadService, 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 };