shopstack 0.2.6 → 0.3.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.
package/src/client.js CHANGED
@@ -1,7 +1,15 @@
1
1
  import { randomBytes, randomUUID } from "node:crypto";
2
- import WebSocket from "ws";
2
+ import { readReservationProgress } from "./reservation-progress.js";
3
+ import { DEFAULT_API_URL, resolveProfileBaseUrl } from "./config.js";
4
+ import { checkoutMonitor } from "./checkout-monitor.js";
3
5
 
4
6
  const TERMINAL_STATUSES = new Set(["complete", "failed", "cancelled"]);
7
+ const TERMINAL_RESERVATION_STATUSES = new Set([
8
+ "confirmed",
9
+ "cancelled",
10
+ "booking_unknown",
11
+ "cancellation_unknown",
12
+ ]);
5
13
 
6
14
  export class ShopstackApiError extends Error {
7
15
  constructor(message, { code = "api_error", requestId, status } = {}) {
@@ -17,11 +25,11 @@ function idempotencyKey(prefix) {
17
25
  return `${prefix}-${randomUUID()}`;
18
26
  }
19
27
 
20
- function loginRecoveryKey() {
21
- return `login_recovery_${randomBytes(32).toString("base64url")}`;
28
+ function signupRecoveryKey() {
29
+ return `signup_recovery_${randomBytes(32).toString("base64url")}`;
22
30
  }
23
31
 
24
- function loginAttemptKey(accountType, email) {
32
+ function signupAttemptKey(accountType, email) {
25
33
  return `${accountType}\u0000${email.trim().toLowerCase()}`;
26
34
  }
27
35
 
@@ -31,122 +39,17 @@ function delay(milliseconds) {
31
39
  : new Promise((resolve) => setTimeout(resolve, milliseconds));
32
40
  }
33
41
 
34
- function presentationRevision(checkout) {
35
- return Number.isInteger(checkout?.presentation_revision)
36
- ? checkout.presentation_revision
37
- : checkout.revision;
38
- }
39
-
40
- class CheckoutUpdateWatcher {
41
- constructor(client, checkoutId) {
42
- this.client = client;
43
- this.checkoutId = checkoutId;
44
- this.latestRevision = 0;
45
- this.waiter = undefined;
46
- this.socket = undefined;
47
- this.unavailable = typeof client.WebSocket !== "function";
48
- }
49
-
50
- async connect() {
51
- if (this.socket || this.unavailable) return;
52
- try {
53
- const subscription = await this.client.createCheckoutUpdateSubscription(
54
- this.checkoutId,
55
- );
56
- const socket = new this.client.WebSocket(subscription.socket_url, [
57
- subscription.protocol,
58
- subscription.token,
59
- ]);
60
- this.socket = socket;
61
- socket.addEventListener("message", (event) => {
62
- let payload;
63
- try {
64
- payload = JSON.parse(String(event.data));
65
- } catch {
66
- return;
67
- }
68
- if (
69
- payload?.type !== "checkout_changed" ||
70
- !Number.isInteger(payload.presentation_revision) ||
71
- payload.presentation_revision < 1 ||
72
- typeof payload.terminal !== "boolean"
73
- ) {
74
- return;
75
- }
76
- this.latestRevision = Math.max(
77
- this.latestRevision,
78
- payload.presentation_revision,
79
- );
80
- this.waiter?.();
81
- });
82
- const unavailable = () => {
83
- this.unavailable = true;
84
- this.waiter?.();
85
- };
86
- socket.addEventListener("error", unavailable);
87
- socket.addEventListener("close", unavailable);
88
- } catch {
89
- this.unavailable = true;
90
- }
91
- }
92
-
93
- async wait(afterRevision, timeoutMs) {
94
- await this.connect();
95
- if (this.latestRevision > afterRevision) return { kind: "changed" };
96
- if (!this.unavailable) {
97
- const changed = await new Promise((resolve) => {
98
- let settled = false;
99
- const settle = (value) => {
100
- if (settled) return;
101
- settled = true;
102
- this.waiter = undefined;
103
- clearTimeout(timer);
104
- resolve(value);
105
- };
106
- this.waiter = () =>
107
- settle(
108
- this.latestRevision > afterRevision ? "changed" : "unavailable",
109
- );
110
- const timer = setTimeout(
111
- () => settle("timeout"),
112
- Math.max(1, Math.min(timeoutMs, 25_000)),
113
- );
114
- });
115
- if (changed === "changed") return { kind: "changed" };
116
- if (changed === "timeout") return { kind: "refresh" };
117
- }
118
- try {
119
- const checkout = await this.client.waitForCheckoutUpdate(
120
- this.checkoutId,
121
- afterRevision,
122
- Math.max(1, Math.min(25, Math.ceil(timeoutMs / 1_000))),
123
- );
124
- return checkout === undefined
125
- ? { kind: "refresh" }
126
- : { checkout, kind: "checkout" };
127
- } catch {
128
- return { kind: "unavailable" };
129
- }
130
- }
131
-
132
- close() {
133
- try {
134
- this.socket?.close(1000, "Checkout monitoring complete");
135
- } catch {
136
- // Monitoring teardown must not alter the checkout result.
137
- }
138
- }
139
- }
140
-
141
42
  export class ShopstackClient {
142
43
  constructor({
143
44
  apiKey,
144
- baseUrl = "https://api.shopstack.ai/v1",
45
+ baseUrl = process.env.SHOPSTACK_API_URL ?? DEFAULT_API_URL,
145
46
  fetch: fetchImplementation = globalThis.fetch,
146
- webSocket: webSocketImplementation = WebSocket,
47
+ webSocket = globalThis.WebSocket,
147
48
  } = {}) {
148
49
  if (typeof baseUrl !== "string" || !/^https?:\/\//u.test(baseUrl)) {
149
- throw new TypeError("baseUrl must be an HTTP(S) URL");
50
+ throw new TypeError(
51
+ "Set baseUrl or SHOPSTACK_API_URL to the intended Shopstack HTTP(S) API endpoint.",
52
+ );
150
53
  }
151
54
  if (typeof fetchImplementation !== "function") {
152
55
  throw new TypeError("a fetch implementation is required");
@@ -154,7 +57,7 @@ export class ShopstackClient {
154
57
  this.apiKey = apiKey;
155
58
  this.baseUrl = baseUrl.replace(/\/+$/u, "");
156
59
  this.fetch = fetchImplementation;
157
- this.WebSocket = webSocketImplementation;
60
+ this.webSocket = webSocket;
158
61
  }
159
62
 
160
63
  async request(
@@ -164,76 +67,115 @@ export class ShopstackClient {
164
67
  body,
165
68
  idempotencyKey: mutationKey,
166
69
  method = "GET",
167
- loginRecoveryKey: recoveryKey,
70
+ onProgress,
71
+ signupRecoveryKey: recoveryKey,
72
+ signal,
168
73
  } = {},
169
74
  ) {
170
75
  const headers = new Headers({ Accept: "application/json" });
76
+ const progress =
77
+ typeof onProgress === "function" &&
78
+ method === "POST" &&
79
+ /^\/reservations(?:\/|$)/u.test(path);
80
+ if (progress) headers.set("Accept", "application/x-ndjson");
171
81
  const authorization = bearerToken ?? this.apiKey;
172
82
  if (authorization) {
173
83
  headers.set("Authorization", `Bearer ${authorization}`);
174
84
  }
175
85
  if (body !== undefined) headers.set("Content-Type", "application/json");
176
86
  if (mutationKey) headers.set("Idempotency-Key", mutationKey);
177
- if (recoveryKey) headers.set("Login-Recovery-Key", recoveryKey);
87
+ if (recoveryKey) headers.set("Signup-Recovery-Key", recoveryKey);
178
88
  const response = await this.fetch(`${this.baseUrl}${path}`, {
179
89
  body: body === undefined ? undefined : JSON.stringify(body),
180
90
  headers,
181
91
  method,
92
+ signal,
182
93
  });
183
- const raw = await response.text();
184
94
  let payload;
185
- try {
186
- payload = raw ? JSON.parse(raw) : undefined;
187
- } catch {
188
- throw new ShopstackApiError("Shopstack returned invalid JSON.", {
189
- status: response.status,
190
- });
95
+ let status = response.status;
96
+ if (
97
+ progress &&
98
+ response.headers.get("content-type")?.split(";")[0] ===
99
+ "application/x-ndjson"
100
+ ) {
101
+ try {
102
+ ({ payload, status } = await readReservationProgress(
103
+ response,
104
+ onProgress,
105
+ ));
106
+ } catch {
107
+ const requestId = response.headers.get("x-request-id");
108
+ throw new ShopstackApiError(
109
+ "The reservation response was interrupted or invalid. Check the reservation state before retrying; booking may already have occurred.",
110
+ {
111
+ code: "reservation_response_incomplete",
112
+ requestId: /^req_[a-f0-9]{32}$/u.test(requestId ?? "")
113
+ ? requestId
114
+ : undefined,
115
+ status: 502,
116
+ },
117
+ );
118
+ }
119
+ } else {
120
+ const raw = await response.text();
121
+ try {
122
+ payload = raw ? JSON.parse(raw) : undefined;
123
+ } catch {
124
+ throw new ShopstackApiError("Shopstack returned invalid JSON.", {
125
+ status: response.status,
126
+ });
127
+ }
191
128
  }
192
- if (!response.ok) {
129
+ if (status < 200 || status >= 300) {
193
130
  const error = payload?.error;
194
131
  throw new ShopstackApiError(
195
132
  typeof error?.message === "string"
196
133
  ? error.message
197
- : `Shopstack request failed with status ${response.status}.`,
134
+ : `Shopstack request failed with status ${status}.`,
198
135
  {
199
136
  code: typeof error?.code === "string" ? error.code : "api_error",
200
137
  requestId:
201
138
  typeof error?.request_id === "string"
202
139
  ? error.request_id
203
140
  : undefined,
204
- status: response.status,
141
+ status,
205
142
  },
206
143
  );
207
144
  }
208
145
  return payload;
209
146
  }
210
147
 
211
- _startAccountLogin({ accountType, email, idempotencyKey: key, recoveryKey }) {
212
- return this.request("/login", {
148
+ _startAccountSignup({
149
+ accountType,
150
+ email,
151
+ idempotencyKey: key,
152
+ recoveryKey,
153
+ }) {
154
+ return this.request("/accounts", {
213
155
  body: { account_type: accountType, email },
214
156
  idempotencyKey: key,
215
157
  method: "POST",
216
- loginRecoveryKey: recoveryKey,
158
+ signupRecoveryKey: recoveryKey,
217
159
  });
218
160
  }
219
161
 
220
- startAccountLogin({ accountType, email } = {}) {
221
- return this._startAccountLogin({
162
+ startAccountSignup({ accountType, email } = {}) {
163
+ return this._startAccountSignup({
222
164
  accountType,
223
165
  email,
224
166
  idempotencyKey: idempotencyKey("account"),
225
- recoveryKey: loginRecoveryKey(),
167
+ recoveryKey: signupRecoveryKey(),
226
168
  });
227
169
  }
228
170
 
229
- pollAccountLogin(loginId, pollToken) {
230
- return this.request(`/login/${encodeURIComponent(loginId)}`, {
171
+ pollAccountSignup(signupId, pollToken) {
172
+ return this.request(`/accounts/${encodeURIComponent(signupId)}`, {
231
173
  bearerToken: pollToken,
232
174
  });
233
175
  }
234
176
 
235
- async waitForAccountLogin(
236
- loginId,
177
+ async waitForAccountSignup(
178
+ signupId,
237
179
  pollToken,
238
180
  { pollIntervalMs = 2_000, timeoutMs = 15 * 60 * 1_000 } = {},
239
181
  ) {
@@ -241,16 +183,16 @@ export class ShopstackClient {
241
183
  while (true) {
242
184
  if (Date.now() - startedAt >= timeoutMs) {
243
185
  throw new ShopstackApiError("Email verification timed out.", {
244
- code: "login_timeout",
186
+ code: "signup_timeout",
245
187
  });
246
188
  }
247
- const current = await this.pollAccountLogin(loginId, pollToken);
189
+ const current = await this.pollAccountSignup(signupId, pollToken);
248
190
  if (current.status === "verified") return current;
249
191
  await delay(pollIntervalMs);
250
192
  }
251
193
  }
252
194
 
253
- async login({
195
+ async signup({
254
196
  accountType,
255
197
  email,
256
198
  onProgress,
@@ -260,17 +202,17 @@ export class ShopstackClient {
260
202
  timeoutMs = 15 * 60 * 1_000,
261
203
  } = {}) {
262
204
  const normalizedEmail = email.trim().toLowerCase();
263
- const memoryKey = loginAttemptKey(accountType, normalizedEmail);
264
- this._pendingLogins ??= new Map();
205
+ const memoryKey = signupAttemptKey(accountType, normalizedEmail);
206
+ this._pendingSignups ??= new Map();
265
207
  const storage = persistence ?? {
266
208
  completePending: async (state, result) => {
267
209
  this.apiKey = result.api_key;
268
- this._pendingLogins.delete(memoryKey);
210
+ this._pendingSignups.delete(memoryKey);
269
211
  },
270
- deletePending: async () => this._pendingLogins.delete(memoryKey),
271
- loadPending: async () => this._pendingLogins.get(memoryKey),
212
+ deletePending: async () => this._pendingSignups.delete(memoryKey),
213
+ loadPending: async () => this._pendingSignups.get(memoryKey),
272
214
  savePending: async (state) => {
273
- this._pendingLogins.set(memoryKey, structuredClone(state));
215
+ this._pendingSignups.set(memoryKey, structuredClone(state));
274
216
  },
275
217
  };
276
218
  const publish = async (state, details = {}) => {
@@ -281,6 +223,7 @@ export class ShopstackClient {
281
223
  accountType,
282
224
  email: normalizedEmail,
283
225
  });
226
+ resolveProfileBaseUrl(pending, this.baseUrl);
284
227
  if (pending?.expiresAt && Date.parse(pending.expiresAt) <= Date.now()) {
285
228
  await storage.deletePending(pending);
286
229
  await publish("expired", {
@@ -291,10 +234,11 @@ export class ShopstackClient {
291
234
  }
292
235
  pending ??= {
293
236
  accountType,
237
+ baseUrl: this.baseUrl,
294
238
  attemptId: `attempt-${randomUUID()}`,
295
239
  email: normalizedEmail,
296
240
  idempotencyKey: idempotencyKey("account"),
297
- recoveryKey: loginRecoveryKey(),
241
+ recoveryKey: signupRecoveryKey(),
298
242
  };
299
243
  await storage.savePending(pending);
300
244
  await publish("pending", {
@@ -302,8 +246,8 @@ export class ShopstackClient {
302
246
  email: normalizedEmail,
303
247
  });
304
248
  try {
305
- if (!pending.loginId || !pending.pollToken) {
306
- const started = await this._startAccountLogin({
249
+ if (!pending.signupId || !pending.pollToken) {
250
+ const started = await this._startAccountSignup({
307
251
  accountType,
308
252
  email: normalizedEmail,
309
253
  idempotencyKey: pending.idempotencyKey,
@@ -313,7 +257,7 @@ export class ShopstackClient {
313
257
  ...pending,
314
258
  expiresAt: started.expires_at,
315
259
  pollToken: started.poll_token,
316
- loginId: started.id,
260
+ signupId: started.id,
317
261
  };
318
262
  await storage.savePending(pending);
319
263
  if (typeof onVerificationRequired === "function") {
@@ -336,8 +280,8 @@ export class ShopstackClient {
336
280
  email: normalizedEmail,
337
281
  expires_at: pending.expiresAt,
338
282
  });
339
- const result = await this.waitForAccountLogin(
340
- pending.loginId,
283
+ const result = await this.waitForAccountSignup(
284
+ pending.signupId,
341
285
  pending.pollToken,
342
286
  { pollIntervalMs, timeoutMs },
343
287
  );
@@ -354,7 +298,7 @@ export class ShopstackClient {
354
298
  const { api_key: _credential, ...completed } = result;
355
299
  return completed;
356
300
  } catch (error) {
357
- if (error?.code === "login_expired") {
301
+ if (error?.code === "signup_expired") {
358
302
  await storage.deletePending(pending);
359
303
  await publish("expired", {
360
304
  account_type: accountType,
@@ -367,7 +311,7 @@ export class ShopstackClient {
367
311
  });
368
312
  } else if (
369
313
  error?.code === "idempotency_conflict" ||
370
- error?.code === "invalid_login_recovery"
314
+ error?.code === "invalid_signup_recovery"
371
315
  ) {
372
316
  await storage.deletePending(pending);
373
317
  await publish("conflict", {
@@ -427,6 +371,96 @@ export class ShopstackClient {
427
371
  });
428
372
  }
429
373
 
374
+ createReservation(reservation, { idempotencyKey: key, onProgress } = {}) {
375
+ return this.request("/reservations", {
376
+ body: reservation,
377
+ idempotencyKey: key ?? idempotencyKey("reservation"),
378
+ onProgress,
379
+ method: "POST",
380
+ });
381
+ }
382
+
383
+ getReservation(reservationId) {
384
+ return this.request(`/reservations/${encodeURIComponent(reservationId)}`);
385
+ }
386
+
387
+ listReservationOptions(reservationId, { limit = 6, offset = 0 } = {}) {
388
+ const query = new URLSearchParams({
389
+ offset: String(offset),
390
+ limit: String(limit),
391
+ });
392
+ return this.request(
393
+ `/reservations/${encodeURIComponent(reservationId)}/options?${query.toString()}`,
394
+ );
395
+ }
396
+
397
+ getReservationOption(reservationId, optionId) {
398
+ return this.request(
399
+ `/reservations/${encodeURIComponent(reservationId)}/options/${encodeURIComponent(optionId)}`,
400
+ );
401
+ }
402
+
403
+ sendReservationMessage(
404
+ reservationId,
405
+ content,
406
+ expectedRevision,
407
+ { idempotencyKey: key, onProgress } = {},
408
+ ) {
409
+ return this.request(
410
+ `/reservations/${encodeURIComponent(reservationId)}/messages`,
411
+ {
412
+ body: { content, expected_revision: expectedRevision },
413
+ idempotencyKey: key ?? idempotencyKey("reservation-message"),
414
+ onProgress,
415
+ method: "POST",
416
+ },
417
+ );
418
+ }
419
+
420
+ cancelReservation(
421
+ reservationId,
422
+ expectedRevision,
423
+ { idempotencyKey: key, onProgress } = {},
424
+ ) {
425
+ return this.request(
426
+ `/reservations/${encodeURIComponent(reservationId)}/cancel`,
427
+ {
428
+ body: { expected_revision: expectedRevision },
429
+ idempotencyKey: key ?? idempotencyKey("reservation-cancel"),
430
+ onProgress,
431
+ method: "POST",
432
+ },
433
+ );
434
+ }
435
+
436
+ async runReservation(reservationRequest, options = {}) {
437
+ let reservation = await this.createReservation(reservationRequest, {
438
+ idempotencyKey: options.idempotencyKey,
439
+ });
440
+ while (!TERMINAL_RESERVATION_STATUSES.has(reservation.status)) {
441
+ if (typeof options.onTurn === "function") {
442
+ await options.onTurn(reservation);
443
+ }
444
+ if (typeof options.respond !== "function") return reservation;
445
+ const content = await options.respond(reservation);
446
+ if (typeof content !== "string" || content.trim().length === 0) {
447
+ return reservation;
448
+ }
449
+ reservation = await this.sendReservationMessage(
450
+ reservation.id,
451
+ content.trim(),
452
+ reservation.revision,
453
+ {
454
+ idempotencyKey: `reservation-message-${reservation.id}-${reservation.revision}`,
455
+ },
456
+ );
457
+ }
458
+ if (typeof options.onTurn === "function") {
459
+ await options.onTurn(reservation);
460
+ }
461
+ return reservation;
462
+ }
463
+
430
464
  createCheckout(checkout, { idempotencyKey: key } = {}) {
431
465
  return this.request("/checkout", {
432
466
  body: checkout,
@@ -439,29 +473,37 @@ export class ShopstackClient {
439
473
  return this.request(`/checkout/${encodeURIComponent(checkoutId)}`);
440
474
  }
441
475
 
442
- createLiveView(checkoutId, { idempotencyKey: key } = {}) {
476
+ waitForCheckoutUpdate(checkoutId, { after, wait = 25, signal } = {}) {
477
+ if (!Number.isSafeInteger(after) || after < 0) {
478
+ throw new TypeError("after must be a non-negative safe integer.");
479
+ }
480
+ if (!Number.isSafeInteger(wait) || wait < 1 || wait > 25) {
481
+ throw new TypeError("wait must be an integer from 1 through 25.");
482
+ }
483
+ const query = new URLSearchParams({
484
+ after: String(after),
485
+ wait: String(wait),
486
+ });
443
487
  return this.request(
444
- `/checkout/${encodeURIComponent(checkoutId)}/live-view`,
445
- {
446
- idempotencyKey: key ?? idempotencyKey("live-view"),
447
- method: "POST",
448
- },
488
+ `/checkout/${encodeURIComponent(checkoutId)}/updates?${query}`,
489
+ { signal },
449
490
  );
450
491
  }
451
492
 
452
- createCheckoutUpdateSubscription(checkoutId) {
493
+ subscribeToCheckoutUpdates(checkoutId, { signal } = {}) {
453
494
  return this.request(`/checkout/${encodeURIComponent(checkoutId)}/updates`, {
454
495
  method: "POST",
496
+ signal,
455
497
  });
456
498
  }
457
499
 
458
- waitForCheckoutUpdate(checkoutId, afterRevision, waitSeconds = 25) {
459
- const query = new URLSearchParams({
460
- after: String(afterRevision),
461
- wait: String(waitSeconds),
462
- });
500
+ createLiveView(checkoutId, { idempotencyKey: key } = {}) {
463
501
  return this.request(
464
- `/checkout/${encodeURIComponent(checkoutId)}/updates?${query.toString()}`,
502
+ `/checkout/${encodeURIComponent(checkoutId)}/live-view`,
503
+ {
504
+ idempotencyKey: key ?? idempotencyKey("live-view"),
505
+ method: "POST",
506
+ },
465
507
  );
466
508
  }
467
509
 
@@ -491,12 +533,6 @@ export class ShopstackClient {
491
533
  return this.request(`/checkout/${encodeURIComponent(checkoutId)}/events`);
492
534
  }
493
535
 
494
- listArtifacts(checkoutId) {
495
- return this.request(
496
- `/checkout/${encodeURIComponent(checkoutId)}/artifacts`,
497
- );
498
- }
499
-
500
536
  providePaymentDetails(checkoutId, card, { idempotencyKey: key } = {}) {
501
537
  return this.request(
502
538
  `/checkout/${encodeURIComponent(checkoutId)}/payment-details`,
@@ -530,16 +566,12 @@ export class ShopstackClient {
530
566
  let checkout = await this.createCheckout(checkoutRequest, {
531
567
  idempotencyKey: options.idempotencyKey,
532
568
  });
533
- if (typeof options.onCreated === "function") {
534
- await options.onCreated(checkout);
535
- }
536
569
  let progressFingerprint;
537
570
  const publishProgress = async (current) => {
538
571
  if (typeof options.onProgress !== "function") return;
539
572
  const fingerprint = JSON.stringify([
540
573
  current.status,
541
574
  current.revision,
542
- current.presentation_revision,
543
575
  current.activity,
544
576
  current.intent?.name,
545
577
  current.intent?.phase,
@@ -551,20 +583,20 @@ export class ShopstackClient {
551
583
  progressFingerprint = fingerprint;
552
584
  await options.onProgress(current);
553
585
  };
554
- const watcher = new CheckoutUpdateWatcher(this, checkout.id);
586
+ await publishProgress(checkout);
555
587
  let handledPaymentRevision;
556
588
  let handledApprovalId;
557
589
  let handledMessageRevision;
590
+ const monitor = checkoutMonitor(this, checkout.id, this.webSocket);
558
591
  try {
559
- while (true) {
560
- await publishProgress(checkout);
561
- if (TERMINAL_STATUSES.has(checkout.status)) return checkout;
562
- const elapsed = Date.now() - startedAt;
563
- if (elapsed >= timeoutMs) {
592
+ while (!TERMINAL_STATUSES.has(checkout.status)) {
593
+ if (Date.now() - startedAt >= timeoutMs) {
564
594
  throw new ShopstackApiError("Checkout monitoring timed out.", {
565
595
  code: "checkout_timeout",
566
596
  });
567
597
  }
598
+ checkout = await this.getCheckout(checkout.id);
599
+ await publishProgress(checkout);
568
600
  if (
569
601
  checkout.required_input?.type === "payment_card" &&
570
602
  handledPaymentRevision !== checkout.revision
@@ -573,10 +605,9 @@ export class ShopstackClient {
573
605
  handledPaymentRevision = checkout.revision;
574
606
  const card = await options.paymentDetails(checkout);
575
607
  if (card === undefined) return checkout;
576
- checkout = await this.providePaymentDetails(checkout.id, card, {
608
+ await this.providePaymentDetails(checkout.id, card, {
577
609
  idempotencyKey: `payment-details-${checkout.id}-${checkout.revision}`,
578
610
  });
579
- handledMessageRevision = checkout.revision;
580
611
  continue;
581
612
  }
582
613
  if (
@@ -588,13 +619,13 @@ export class ShopstackClient {
588
619
  handledApprovalId = checkout.approval.id;
589
620
  const approved = await options.approve(checkout.approval, checkout);
590
621
  if (typeof approved !== "boolean") return checkout;
591
- checkout = await this.decidePaymentApproval(
622
+ await this.decidePaymentApproval(
592
623
  checkout.id,
593
624
  checkout.approval.id,
594
625
  approved,
595
626
  { idempotencyKey: `approval-${checkout.approval.id}` },
596
627
  );
597
- if (!approved) return checkout;
628
+ if (!approved) return this.getCheckout(checkout.id);
598
629
  continue;
599
630
  }
600
631
  if (
@@ -612,27 +643,16 @@ export class ShopstackClient {
612
643
  });
613
644
  continue;
614
645
  }
615
- const remainingMs = timeoutMs - (Date.now() - startedAt);
616
- const update = await watcher.wait(
617
- presentationRevision(checkout),
618
- remainingMs,
619
- );
620
- if (update.kind === "unavailable") {
621
- const pollIntervalMs =
622
- options.pollIntervalMs ??
623
- (checkout.status === "help_required" ||
624
- checkout.status === "approval_required"
625
- ? 5_000
626
- : 2_000);
627
- await delay(Math.min(pollIntervalMs, remainingMs));
646
+ if (!TERMINAL_STATUSES.has(checkout.status)) {
647
+ await monitor.wait(
648
+ checkout.presentation_revision ?? 0,
649
+ timeoutMs - (Date.now() - startedAt),
650
+ );
628
651
  }
629
- checkout =
630
- update.kind === "checkout"
631
- ? update.checkout
632
- : await this.getCheckout(checkout.id);
633
652
  }
653
+ return checkout;
634
654
  } finally {
635
- watcher.close();
655
+ monitor.close();
636
656
  }
637
657
  }
638
658
  }