@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
|
-
|
|
634
|
-
|
|
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);
|