@visa/cli 4.1.0-rc.19 → 4.1.0-rc.20

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.
@@ -141,6 +141,31 @@ export declare function debugShotMaskPlan(fields: FieldMap): {
141
141
  }[];
142
142
  };
143
143
  export declare function isStripeLinkConsumerRequest(url: string): boolean;
144
+ export declare function suppressStripeLink(page: Page, evidence: EvidenceLog): Promise<void>;
145
+ export type LinkQuietResult = {
146
+ fired: boolean;
147
+ waitedMs: number;
148
+ };
149
+ /**
150
+ * Wait for the suppressed Stripe Link lookup to fire and settle BEFORE the
151
+ * submit click. Stripe debounces its consumer-session lookup ~300ms after the
152
+ * email input changes; our fill→click gap is single-digit ms, so the (aborted)
153
+ * lookup used to land INSIDE Stripe's in-flight submit chain and kill it
154
+ * silently — the click looked accepted but tokenization never ran and the page
155
+ * sat on the form until the outcome deadline (#5879: three identical live
156
+ * stalls at donate.stripe.com). Verified live A/B on that page: instant click →
157
+ * dead submit, no /v1/payment_methods; lookup settled first → tokenization and
158
+ * the confirm step both reached.
159
+ *
160
+ * If the lookup already fired, only the short settle applies (lets Stripe's
161
+ * abort handling unwind). If it never fires — non-Link page variants, no email
162
+ * field — the bound expires and the click proceeds as before.
163
+ */
164
+ export declare function waitForLinkLookupQuiet(page: Page, opts?: {
165
+ boundMs?: number;
166
+ settleMs?: number;
167
+ delay?: (ms: number) => Promise<void>;
168
+ }): Promise<LinkQuietResult>;
144
169
  export declare function prepareCheckout(opts: PrepareCheckoutOptions, store?: PreparedCheckoutSessionStore): Promise<PrepareCheckoutResult>;
145
170
  export declare function submitApprovedCheckout(reviewId: string, opts: SubmitApprovedCheckoutOptions, store?: PreparedCheckoutSessionStore): Promise<CheckoutResult>;
146
171
  export declare function cancelPreparedCheckout(reviewId: string, detail?: string, store?: PreparedCheckoutSessionStore): Promise<CheckoutResult>;
@@ -630,11 +630,17 @@ async function captureDebugShot(page, dir, reviewId, label, evidence, fields) {
630
630
  export function isStripeLinkConsumerRequest(url) {
631
631
  return /(?:^|\/\/)([a-z0-9.-]*\.)?stripe\.com\/v1\/consumers\/sessions\/(?:lookup|start_verification)\b/i.test(url);
632
632
  }
633
- async function suppressStripeLink(page, evidence) {
634
- let suppressed = 0;
633
+ // Keyed by Page so the tracker installed at prepare time is reachable from the
634
+ // approved-submit leg without threading through the session store types.
635
+ const linkSuppressionByPage = new WeakMap();
636
+ // Exported for the link-quiet unit tests (a fake Page captures the route
637
+ // handler); production callers stay inside this module.
638
+ export async function suppressStripeLink(page, evidence) {
639
+ const state = { suppressed: 0, waiters: [] };
640
+ linkSuppressionByPage.set(page, state);
635
641
  await page.route((u) => isStripeLinkConsumerRequest(typeof u === 'string' ? u : u.href), (route) => {
636
- suppressed += 1;
637
- if (suppressed === 1) {
642
+ state.suppressed += 1;
643
+ if (state.suppressed === 1) {
638
644
  // origin + pathname only — never the full URL. The lookup carries the
639
645
  // email in the POST body today, but keep an operator email out of the
640
646
  // evidence log even if Stripe moves a param to the query string (#5708).
@@ -649,9 +655,45 @@ async function suppressStripeLink(page, evidence) {
649
655
  }
650
656
  evidence.step('note', { linkSuppressed: safe });
651
657
  }
658
+ for (const wake of state.waiters.splice(0))
659
+ wake();
652
660
  return route.abort();
653
661
  });
654
662
  }
663
+ /**
664
+ * Wait for the suppressed Stripe Link lookup to fire and settle BEFORE the
665
+ * submit click. Stripe debounces its consumer-session lookup ~300ms after the
666
+ * email input changes; our fill→click gap is single-digit ms, so the (aborted)
667
+ * lookup used to land INSIDE Stripe's in-flight submit chain and kill it
668
+ * silently — the click looked accepted but tokenization never ran and the page
669
+ * sat on the form until the outcome deadline (#5879: three identical live
670
+ * stalls at donate.stripe.com). Verified live A/B on that page: instant click →
671
+ * dead submit, no /v1/payment_methods; lookup settled first → tokenization and
672
+ * the confirm step both reached.
673
+ *
674
+ * If the lookup already fired, only the short settle applies (lets Stripe's
675
+ * abort handling unwind). If it never fires — non-Link page variants, no email
676
+ * field — the bound expires and the click proceeds as before.
677
+ */
678
+ export async function waitForLinkLookupQuiet(page, opts = {}) {
679
+ const boundMs = opts.boundMs ?? 1500;
680
+ const settleMs = opts.settleMs ?? 250;
681
+ const delay = opts.delay ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
682
+ const state = linkSuppressionByPage.get(page);
683
+ if (!state)
684
+ return { fired: false, waitedMs: 0 };
685
+ const started = Date.now();
686
+ if (state.suppressed === 0) {
687
+ await Promise.race([
688
+ new Promise((resolve) => state.waiters.push(resolve)),
689
+ delay(boundMs),
690
+ ]);
691
+ }
692
+ const fired = state.suppressed > 0;
693
+ if (fired)
694
+ await delay(settleMs);
695
+ return { fired, waitedMs: Date.now() - started };
696
+ }
655
697
  // Payer-chosen amount inputs. Deliberately payment-link-specific (Stripe's
656
698
  // customUnitAmount): on an ordinary checkout the total is merchant-controlled
657
699
  // and typing into anything price-like must never happen.
@@ -1076,6 +1118,10 @@ export async function submitApprovedCheckout(reviewId, opts, store = defaultPrep
1076
1118
  evidence.setSnapshotSummary(await snapshotSummary(page));
1077
1119
  return makeResult('failed', state.fields, evidence, requiresAdapter, 'no submit control detected');
1078
1120
  }
1121
+ // The suppressed Link lookup must settle before the click or it breaks
1122
+ // Stripe's submit chain mid-flight (#5879) — see waitForLinkLookupQuiet.
1123
+ const linkQuiet = await waitForLinkLookupQuiet(page);
1124
+ evidence.step('note', { linkQuiet });
1079
1125
  evidence.step('submit', { clicked: true, target: submit.desc });
1080
1126
  await submit.click();
1081
1127
  await settle(page);