@visa/cli 4.1.0-rc.17 → 4.1.0-rc.171
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/README.md +189 -232
- package/dist/checkout-engine/adapters/generic.d.ts +67 -0
- package/dist/checkout-engine/adapters/generic.js +233 -14
- package/dist/checkout-engine/adapters/index.d.ts +4 -1
- package/dist/checkout-engine/adapters/index.js +10 -3
- package/dist/checkout-engine/adapters/shopify.d.ts +55 -0
- package/dist/checkout-engine/adapters/shopify.js +514 -0
- package/dist/checkout-engine/amount.d.ts +15 -0
- package/dist/checkout-engine/amount.js +72 -0
- package/dist/checkout-engine/cli-engine.d.ts +259 -4
- package/dist/checkout-engine/cli-engine.js +797 -43
- package/dist/checkout-engine/confirmed-merchants.d.ts +31 -0
- package/dist/checkout-engine/confirmed-merchants.js +165 -0
- package/dist/checkout-engine/detect.d.ts +1 -1
- package/dist/checkout-engine/detect.js +26 -0
- package/dist/checkout-engine/evidence.d.ts +4 -1
- package/dist/checkout-engine/evidence.js +51 -6
- package/dist/checkout-engine/executor.d.ts +62 -4
- package/dist/checkout-engine/executor.js +402 -122
- package/dist/checkout-engine/hosted-approval.d.ts +124 -7
- package/dist/checkout-engine/hosted-approval.js +381 -54
- package/dist/checkout-engine/index.d.ts +8 -2
- package/dist/checkout-engine/index.js +7 -1
- package/dist/checkout-engine/instrument.d.ts +7 -0
- package/dist/checkout-engine/instrument.js +4 -0
- package/dist/checkout-engine/known-merchants.d.ts +10 -0
- package/dist/checkout-engine/known-merchants.js +38 -0
- package/dist/checkout-engine/live-fill-approval.d.ts +5 -20
- package/dist/checkout-engine/live-fill-approval.js +20 -51
- package/dist/checkout-engine/mandate/card-mandate.d.ts +121 -0
- package/dist/checkout-engine/mandate/card-mandate.js +227 -0
- package/dist/checkout-engine/mandate/mandate-ledger.d.ts +178 -0
- package/dist/checkout-engine/mandate/mandate-ledger.js +395 -0
- package/dist/checkout-engine/outcome.d.ts +2 -2
- package/dist/checkout-engine/outcome.js +36 -1
- package/dist/checkout-engine/owner-only-file.d.ts +9 -0
- package/dist/checkout-engine/owner-only-file.js +20 -1
- package/dist/checkout-engine/receipt-dir.d.ts +6 -0
- package/dist/checkout-engine/receipt-dir.js +8 -0
- package/dist/checkout-engine/receipt.d.ts +42 -2
- package/dist/checkout-engine/receipt.js +43 -14
- package/dist/checkout-engine/trace-handles.d.ts +8 -0
- package/dist/checkout-engine/trace-handles.js +12 -0
- package/dist/checkout-engine/types.d.ts +20 -2
- package/dist/checkout-engine/unresolved-charges.d.ts +34 -0
- package/dist/checkout-engine/unresolved-charges.js +125 -0
- package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +53 -1
- package/dist/checkout-engine/vgs-gateway/server-mint-client.js +78 -10
- package/dist/checkout-engine/vgs-live-instrument.d.ts +38 -35
- package/dist/checkout-engine/vgs-live-instrument.js +51 -74
- package/dist/checkout-engine/vic-confirmation.d.ts +18 -0
- package/dist/checkout-engine/vic-confirmation.js +9 -3
- package/dist/checkout-engine/web-bot-auth.d.ts +92 -0
- package/dist/checkout-engine/web-bot-auth.js +159 -0
- package/dist/cli.js +665 -502
- package/dist/mcp-apps/ucp-checkout.html +280 -0
- package/dist/mcp-server/index.js +516 -174
- package/dist/skills/pair-visa-agent/RUNTIMES.md +93 -0
- package/dist/skills/pair-visa-agent/SKILL.md +465 -221
- package/dist/subway-direct.mjs +1 -0
- package/install.ps1 +3 -41
- package/install.sh +4 -36
- package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
- package/package.json +18 -14
- package/server.json +3 -3
- package/dist/checkout-engine/inline-target.d.ts +0 -13
- package/dist/checkout-engine/inline-target.js +0 -37
- package/dist/checkout-engine/pay-args.d.ts +0 -14
- package/dist/checkout-engine/pay-args.js +0 -44
- package/dist/checkout-engine/pay.d.ts +0 -1
- package/dist/checkout-engine/pay.js +0 -13
- package/dist/checkout-engine/repo-env.d.ts +0 -11
- package/dist/checkout-engine/repo-env.js +0 -23
- package/dist/checkout-engine/run-live-fill.d.ts +0 -1
- package/dist/checkout-engine/run-live-fill.js +0 -443
- package/dist/checkout-engine/vgs-gateway/fetch-credential.d.mts +0 -74
- package/dist/checkout-engine/vgs-gateway/fetch-credential.mjs +0 -248
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// <select> dropdowns, handles split vs combined expiry, two- vs four-digit
|
|
3
3
|
// years, and split first/last name. It is the fallback that should beat any
|
|
4
4
|
// well-behaved guest checkout on its own.
|
|
5
|
+
import { detectFields } from '../detect.js';
|
|
5
6
|
import { maskCvc, maskExpiry, maskPan, redactContact } from '../evidence.js';
|
|
6
7
|
function pad2(n) {
|
|
7
8
|
return String(n).padStart(2, '0');
|
|
@@ -73,6 +74,51 @@ export function scrubFillErrorMessage(message, value) {
|
|
|
73
74
|
scrubbed = scrubbed.split(value).join('<redacted>');
|
|
74
75
|
return scrubbed;
|
|
75
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* A fill failure in a few words, for a message a human reads.
|
|
79
|
+
*
|
|
80
|
+
* Playwright's error is a multi-line call log — useful in the evidence file,
|
|
81
|
+
* unreadable in a refusal message and in the receipt an operator opens a week
|
|
82
|
+
* later. The refusal names WHICH fields refused; without this it never says
|
|
83
|
+
* WHY, so diagnosing a merchant we cannot drive means either reproducing it or
|
|
84
|
+
* reading someone's evidence JSON. Each cause maps to a different fix:
|
|
85
|
+
*
|
|
86
|
+
* not editable — the input exists but is readonly/disabled at fill time
|
|
87
|
+
* (a custom widget owning the value, or a not-yet-ready
|
|
88
|
+
* form). Typing will not help; the field needs an adapter
|
|
89
|
+
* or a longer wait.
|
|
90
|
+
* not a text field — a non-input element pretending to be one. Needs an
|
|
91
|
+
* adapter that drives the widget.
|
|
92
|
+
* not visible /
|
|
93
|
+
* detached — a re-render race. The reveal loop is the lever.
|
|
94
|
+
*
|
|
95
|
+
* Input is already scrubbed by scrubFillErrorMessage; this only ever shortens.
|
|
96
|
+
*/
|
|
97
|
+
export function summarizeFillFailure(error) {
|
|
98
|
+
if (!error)
|
|
99
|
+
return 'no reason recorded';
|
|
100
|
+
if (/not an? <input>|not.*\[contenteditable\]/i.test(error))
|
|
101
|
+
return 'not a text field';
|
|
102
|
+
if (/element is not visible/i.test(error))
|
|
103
|
+
return 'not visible';
|
|
104
|
+
if (/not attached to the DOM|detached/i.test(error))
|
|
105
|
+
return 'detached from the page';
|
|
106
|
+
if (/element is not enabled/i.test(error))
|
|
107
|
+
return 'disabled';
|
|
108
|
+
if (/not editable/i.test(error)) {
|
|
109
|
+
const timeout = /Timeout (\d+)ms exceeded/i.exec(error);
|
|
110
|
+
return timeout
|
|
111
|
+
? `not editable within ${Math.round(Number(timeout[1]) / 1000)}s`
|
|
112
|
+
: 'not editable';
|
|
113
|
+
}
|
|
114
|
+
if (/Timeout (\d+)ms exceeded/i.test(error)) {
|
|
115
|
+
const timeout = /Timeout (\d+)ms exceeded/i.exec(error);
|
|
116
|
+
return `timed out after ${Math.round(Number(timeout[1]) / 1000)}s`;
|
|
117
|
+
}
|
|
118
|
+
// Unrecognized: the first line, bounded. Better a clipped real message than a
|
|
119
|
+
// confident wrong summary.
|
|
120
|
+
return error.split('\n')[0].slice(0, 120);
|
|
121
|
+
}
|
|
76
122
|
const DEFAULT_FILL_TIMEOUT_MS = 5000;
|
|
77
123
|
async function fillOne(page, role, entry, value, displayValue, fillTimeoutMs) {
|
|
78
124
|
const base = {
|
|
@@ -97,13 +143,24 @@ async function fillOne(page, role, entry, value, displayValue, fillTimeoutMs) {
|
|
|
97
143
|
return { ...base, ok: false, error: scrubFillErrorMessage(err.message, value) };
|
|
98
144
|
}
|
|
99
145
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
146
|
+
/**
|
|
147
|
+
* The contact record and the page rarely agree on name shape: the record may
|
|
148
|
+
* carry fullName while the page wants first/last inputs, or vice versa. Derive
|
|
149
|
+
* the missing shape so either page can be filled from either record.
|
|
150
|
+
*/
|
|
151
|
+
export function contactNameShapes(contact, cardholderName) {
|
|
152
|
+
const fullName = contact.fullName ??
|
|
153
|
+
(contact.firstName && contact.lastName
|
|
154
|
+
? `${contact.firstName} ${contact.lastName}`
|
|
155
|
+
: cardholderName);
|
|
156
|
+
const first = contact.firstName ?? (fullName?.split(/\s+/)[0] || undefined);
|
|
157
|
+
const last = contact.lastName ?? (fullName?.split(/\s+/).slice(1).join(' ') || undefined);
|
|
158
|
+
return { fullName, first, last };
|
|
159
|
+
}
|
|
160
|
+
async function fillFields(page, fields, credential, contact, opts = {}) {
|
|
103
161
|
const fillTimeoutMs = opts.fillTimeoutMs ?? DEFAULT_FILL_TIMEOUT_MS;
|
|
104
162
|
const filled = [];
|
|
105
|
-
const first = contact
|
|
106
|
-
const last = contact.lastName ?? credential.cardholderName.split(/\s+/).slice(1).join(' ') ?? '';
|
|
163
|
+
const { fullName, first, last } = contactNameShapes(contact, credential?.cardholderName);
|
|
107
164
|
// Order matters a little: contact/name before card is harmless, but we fill
|
|
108
165
|
// card fields explicitly per role so order is not load-bearing.
|
|
109
166
|
const jobs = [];
|
|
@@ -120,26 +177,31 @@ export async function fillFieldMap(page, fields, credential, contact, opts = {})
|
|
|
120
177
|
run: async () => fillOne(page, role, entry, value(), display(), fillTimeoutMs),
|
|
121
178
|
});
|
|
122
179
|
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
180
|
+
if (credential) {
|
|
181
|
+
add('number', fields.number, () => credential.pan, () => maskPan(credential.pan));
|
|
182
|
+
add('cvc', fields.cvc, () => credential.cvc, () => maskCvc(credential.cvc));
|
|
183
|
+
}
|
|
184
|
+
if (fullName)
|
|
185
|
+
add('name', fields.name, () => fullName, () => redactContact('name', fullName));
|
|
186
|
+
if (first)
|
|
187
|
+
add('nameFirst', fields.nameFirst, () => first, () => redactContact('nameFirst', first));
|
|
188
|
+
if (last)
|
|
189
|
+
add('nameLast', fields.nameLast, () => last, () => redactContact('nameLast', last));
|
|
128
190
|
// Expiry display values are always redacted: the expiry is part of the
|
|
129
191
|
// keyable credential (DPAN + expiry + DAVV) and never enters the log.
|
|
130
|
-
if (fields.expCombined) {
|
|
192
|
+
if (credential && fields.expCombined) {
|
|
131
193
|
const e = fields.expCombined;
|
|
132
194
|
const v = expCombinedValue(e, credential.expMonth, credential.expYear);
|
|
133
195
|
add('expCombined', e, () => v, () => maskExpiry());
|
|
134
196
|
}
|
|
135
|
-
if (fields.expMonth) {
|
|
197
|
+
if (credential && fields.expMonth) {
|
|
136
198
|
const e = fields.expMonth;
|
|
137
199
|
const value = e.tag === 'select'
|
|
138
200
|
? (monthOptionValue(e.options ?? [], credential.expMonth) ?? pad2(credential.expMonth))
|
|
139
201
|
: pad2(credential.expMonth);
|
|
140
202
|
add('expMonth', e, () => value, () => maskExpiry());
|
|
141
203
|
}
|
|
142
|
-
if (fields.expYear) {
|
|
204
|
+
if (credential && fields.expYear) {
|
|
143
205
|
const e = fields.expYear;
|
|
144
206
|
let value;
|
|
145
207
|
if (e.tag === 'select') {
|
|
@@ -157,6 +219,8 @@ export async function fillFieldMap(page, fields, credential, contact, opts = {})
|
|
|
157
219
|
// the evidence log is built to be persistable.
|
|
158
220
|
if (contact.email)
|
|
159
221
|
add('email', fields.email, () => contact.email, () => redactContact('email', contact.email));
|
|
222
|
+
if (contact.phone)
|
|
223
|
+
add('phone', fields.phone, () => contact.phone, () => redactContact('phone', contact.phone));
|
|
160
224
|
if (contact.addressLine1)
|
|
161
225
|
add('addressLine1', fields.addressLine1, () => contact.addressLine1, () => redactContact('addressLine1', contact.addressLine1));
|
|
162
226
|
if (contact.addressLine2)
|
|
@@ -185,6 +249,154 @@ export async function fillFieldMap(page, fields, credential, contact, opts = {})
|
|
|
185
249
|
}
|
|
186
250
|
return filled;
|
|
187
251
|
}
|
|
252
|
+
// Credential-free contact prefill for merchants that must calculate shipping,
|
|
253
|
+
// tax, and the final total before a human reviews the payment.
|
|
254
|
+
export async function fillContactFieldMap(page, fields, contact, opts = {}) {
|
|
255
|
+
return fillFields(page, fields, null, contact, opts);
|
|
256
|
+
}
|
|
257
|
+
// The shared post-approval fill routine used by every adapter.
|
|
258
|
+
export async function fillFieldMap(page, fields, credential, contact, opts = {}) {
|
|
259
|
+
return fillFields(page, fields, credential, contact, opts);
|
|
260
|
+
}
|
|
261
|
+
/** Option text that identifies a card-paying choice, most specific first. */
|
|
262
|
+
const CARD_OPTION_PATTERNS = [
|
|
263
|
+
/^\s*visa\s*$/i,
|
|
264
|
+
/credit\s*card|card\s*payment/i,
|
|
265
|
+
/^\s*(mastercard|master\s*card)\s*$/i,
|
|
266
|
+
/\bcard\b/i,
|
|
267
|
+
];
|
|
268
|
+
/**
|
|
269
|
+
* Attribute selectors for a card-number input that survive a panel re-render,
|
|
270
|
+
* tried in order. The originally detected locator is tried first so a page that
|
|
271
|
+
* does NOT re-render keeps its higher-confidence match.
|
|
272
|
+
*/
|
|
273
|
+
const CARD_NUMBER_FALLBACK_SELECTORS = [
|
|
274
|
+
'input[autocomplete="cc-number"]',
|
|
275
|
+
'input[name*="creditcardnumber" i]',
|
|
276
|
+
'input[name*="cardnumber" i]',
|
|
277
|
+
'input[id*="cardnumber" i]',
|
|
278
|
+
'input[name*="cc-number" i]',
|
|
279
|
+
];
|
|
280
|
+
/** First selector that resolves to a visible input, or null if none do. */
|
|
281
|
+
async function firstVisibleCardNumberLocator(page, detectedLocator, timeoutMs) {
|
|
282
|
+
for (const selector of [detectedLocator, ...CARD_NUMBER_FALLBACK_SELECTORS]) {
|
|
283
|
+
try {
|
|
284
|
+
await page
|
|
285
|
+
.locator(selector)
|
|
286
|
+
.first()
|
|
287
|
+
.waitFor({ state: 'visible', timeout: Math.max(500, Math.floor(timeoutMs / 3)) });
|
|
288
|
+
return selector;
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
/** Card-credential roles that share the panel a payment select unfolds. */
|
|
297
|
+
const CARD_GROUP_ROLES = ['number', 'cvc', 'expCombined', 'expMonth', 'expYear'];
|
|
298
|
+
/**
|
|
299
|
+
* Re-detect and adopt fresh entries for every card field after the panel is
|
|
300
|
+
* unfolded. Injected for tests; the executor's own detector is used in
|
|
301
|
+
* production.
|
|
302
|
+
*/
|
|
303
|
+
export async function refreshCardGroupFromPage(page, fields, detect = detectFields) {
|
|
304
|
+
let fresh;
|
|
305
|
+
try {
|
|
306
|
+
fresh = (await detect(page)).fields;
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
const adopted = [];
|
|
312
|
+
for (const role of CARD_GROUP_ROLES) {
|
|
313
|
+
const next = fresh[role];
|
|
314
|
+
if (!next || next.visible === false)
|
|
315
|
+
continue;
|
|
316
|
+
const current = fields[role];
|
|
317
|
+
// Only ever replace an entry we could not have filled anyway. A field that
|
|
318
|
+
// is already visible was detected against the live DOM and keeps its
|
|
319
|
+
// higher-confidence match.
|
|
320
|
+
if (current && current.visible !== false)
|
|
321
|
+
continue;
|
|
322
|
+
fields[role] = next;
|
|
323
|
+
adopted.push(role);
|
|
324
|
+
}
|
|
325
|
+
return adopted;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Reveal card fields that a checkout keeps collapsed until a payment method is
|
|
329
|
+
* chosen.
|
|
330
|
+
*
|
|
331
|
+
* `fillFields` skips any entry with `visible === false`, so a card-number input
|
|
332
|
+
* sitting inside a folded panel is never even attempted — the generic adapter
|
|
333
|
+
* then reports `ok: false` ("fill incomplete") without having typed anything.
|
|
334
|
+
* That is the correct default: filling an invisible input is how a credential
|
|
335
|
+
* gets typed into the wrong place. But a payment-method `<select>` guarding the
|
|
336
|
+
* card panel is common enough to be worth handling, and the recovery is a
|
|
337
|
+
* single deterministic interaction rather than a guess.
|
|
338
|
+
*
|
|
339
|
+
* We only ever SELECT a card option — never a wallet, bank transfer, or
|
|
340
|
+
* anything else — and we only act when the card field is already detected but
|
|
341
|
+
* hidden. If nothing changes, the caller proceeds exactly as before and still
|
|
342
|
+
* fails closed.
|
|
343
|
+
*
|
|
344
|
+
* Mutates `fields.number.visible` on success so the subsequent fill attempts
|
|
345
|
+
* the field it just revealed.
|
|
346
|
+
*/
|
|
347
|
+
export async function revealCollapsedCardSection(page, fields, opts = {}) {
|
|
348
|
+
const number = fields.number;
|
|
349
|
+
if (!number || number.visible !== false)
|
|
350
|
+
return { revealed: false, via: null };
|
|
351
|
+
const timeoutMs = opts.timeoutMs ?? 5_000;
|
|
352
|
+
const selects = page.locator('select');
|
|
353
|
+
const count = await selects.count().catch(() => 0);
|
|
354
|
+
for (let i = 0; i < Math.min(count, 12); i++) {
|
|
355
|
+
const select = selects.nth(i);
|
|
356
|
+
// Read option labels through the locator API rather than page.evaluate.
|
|
357
|
+
// A bundled build rewrites the function passed to evaluate() and the
|
|
358
|
+
// injected helper is not defined in page scope, so it throws at runtime —
|
|
359
|
+
// silently, once a catch treats it as "this select didn't match". Staying
|
|
360
|
+
// on the locator API keeps this working in source and bundled alike.
|
|
361
|
+
let labels;
|
|
362
|
+
try {
|
|
363
|
+
labels = await select.locator('option').allTextContents();
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
for (const pattern of CARD_OPTION_PATTERNS) {
|
|
369
|
+
const label = labels.map((l) => l.trim()).find((l) => l && pattern.test(l));
|
|
370
|
+
if (!label)
|
|
371
|
+
continue;
|
|
372
|
+
try {
|
|
373
|
+
await select.selectOption({ label }, { timeout: timeoutMs });
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
// Re-acquire the field instead of waiting on the detected locator.
|
|
379
|
+
// Unfolding the panel typically re-renders it, and the detector's
|
|
380
|
+
// synthetic `data-ca-id` attribute does not survive that — waiting on the
|
|
381
|
+
// old locator times out even though the field is now on screen and
|
|
382
|
+
// fillable. Stable attribute selectors survive the re-render.
|
|
383
|
+
const revealedLocator = await firstVisibleCardNumberLocator(page, number.locator, timeoutMs);
|
|
384
|
+
if (!revealedLocator)
|
|
385
|
+
continue;
|
|
386
|
+
number.locator = revealedLocator;
|
|
387
|
+
number.visible = true;
|
|
388
|
+
// The number is not alone in that panel: cvc and expiry were re-rendered
|
|
389
|
+
// with it and still carry stale, invisible entries. Filling only the
|
|
390
|
+
// number would trade "adapter fill incomplete" for "credential fill
|
|
391
|
+
// incomplete: missing cvc, expiry" — still a failed purchase, still after
|
|
392
|
+
// a credential was minted. Re-detect and adopt fresh entries for the
|
|
393
|
+
// whole card group.
|
|
394
|
+
await refreshCardGroupFromPage(page, fields);
|
|
395
|
+
return { revealed: true, via: label };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return { revealed: false, via: null };
|
|
399
|
+
}
|
|
188
400
|
export class GenericAdapter {
|
|
189
401
|
name = 'generic';
|
|
190
402
|
matches(_detected) {
|
|
@@ -192,10 +404,17 @@ export class GenericAdapter {
|
|
|
192
404
|
return true;
|
|
193
405
|
}
|
|
194
406
|
async fill(page, fields, credential, contact) {
|
|
407
|
+
const reveal = await revealCollapsedCardSection(page, fields);
|
|
195
408
|
const filled = await fillFieldMap(page, fields, credential, contact);
|
|
409
|
+
const ok = filled.some((f) => f.role === 'number' && f.ok);
|
|
196
410
|
return {
|
|
197
|
-
ok
|
|
411
|
+
ok,
|
|
198
412
|
filled,
|
|
413
|
+
...(ok || !reveal.revealed
|
|
414
|
+
? {}
|
|
415
|
+
: {
|
|
416
|
+
detail: `revealed the card section via "${reveal.via}" but the number field still did not fill`,
|
|
417
|
+
}),
|
|
199
418
|
};
|
|
200
419
|
}
|
|
201
420
|
}
|
|
@@ -2,6 +2,9 @@ import type { DetectResult } from '../detect.js';
|
|
|
2
2
|
import type { CheckoutAdapter } from './generic.js';
|
|
3
3
|
export type { CheckoutAdapter } from './generic.js';
|
|
4
4
|
export { GenericAdapter } from './generic.js';
|
|
5
|
+
export { ShopifyAdapter } from './shopify.js';
|
|
5
6
|
export { StripeLikeAdapter } from './stripe-like.js';
|
|
6
7
|
export { fillFieldMap, resolveLocator } from './generic.js';
|
|
7
|
-
export declare function selectAdapter(detected: DetectResult
|
|
8
|
+
export declare function selectAdapter(detected: DetectResult, pageKind?: {
|
|
9
|
+
shopify?: boolean;
|
|
10
|
+
}): CheckoutAdapter;
|
|
@@ -2,14 +2,21 @@
|
|
|
2
2
|
// the universal fallback and always matches last. Selection is a pure
|
|
3
3
|
// function of an already-run detection — adapters never re-detect.
|
|
4
4
|
import { GenericAdapter } from './generic.js';
|
|
5
|
+
import { ShopifyAdapter } from './shopify.js';
|
|
5
6
|
import { StripeLikeAdapter } from './stripe-like.js';
|
|
6
7
|
export { GenericAdapter } from './generic.js';
|
|
8
|
+
export { ShopifyAdapter } from './shopify.js';
|
|
7
9
|
export { StripeLikeAdapter } from './stripe-like.js';
|
|
8
10
|
export { fillFieldMap, resolveLocator } from './generic.js';
|
|
9
|
-
const SPECIFIC = [new StripeLikeAdapter()];
|
|
10
11
|
const FALLBACK = new GenericAdapter();
|
|
11
|
-
export function selectAdapter(detected) {
|
|
12
|
-
|
|
12
|
+
export function selectAdapter(detected, pageKind = {}) {
|
|
13
|
+
// Shopify owns the outer checkout form and can still delegate card fields to
|
|
14
|
+
// an iframe, so it gets first crack and reuses fillFieldMap for both.
|
|
15
|
+
const shopify = new ShopifyAdapter(detected);
|
|
16
|
+
if (pageKind.shopify === true)
|
|
17
|
+
return shopify;
|
|
18
|
+
const specific = [shopify, new StripeLikeAdapter()];
|
|
19
|
+
for (const a of specific) {
|
|
13
20
|
if (a.matches(detected))
|
|
14
21
|
return a;
|
|
15
22
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Page } from 'playwright-core';
|
|
2
|
+
import { type PageAmountRead } from '../amount.js';
|
|
3
|
+
import { type DetectResult, type FieldMap } from '../detect.js';
|
|
4
|
+
import type { CardCredential } from '../instrument.js';
|
|
5
|
+
import type { Contact, FillResult, FilledField } from '../types.js';
|
|
6
|
+
import { type CheckoutAdapter } from './generic.js';
|
|
7
|
+
type ShopifySummary = {
|
|
8
|
+
subtotalMinor: number | null;
|
|
9
|
+
shippingMinor: number | null;
|
|
10
|
+
taxMinor: number | null;
|
|
11
|
+
discountMinor: number;
|
|
12
|
+
totalMinor: number | null;
|
|
13
|
+
currency: string | null;
|
|
14
|
+
verified: boolean;
|
|
15
|
+
};
|
|
16
|
+
export declare function parseShopifySummary(text: string): ShopifySummary;
|
|
17
|
+
export declare function readShopifyAmount(page: Page, requireVerified: boolean): Promise<PageAmountRead>;
|
|
18
|
+
export declare function readStableShopifyAmount(page: Page, timeoutMs?: number): Promise<PageAmountRead>;
|
|
19
|
+
export declare function isShopifyCheckoutPage(page: Page): Promise<boolean>;
|
|
20
|
+
export declare function detectShopifyChallenge(page: Page): Promise<{
|
|
21
|
+
signal: string;
|
|
22
|
+
} | null>;
|
|
23
|
+
/**
|
|
24
|
+
* The en-US variant of a localized Shopify checkout URL, or null when it is
|
|
25
|
+
* already English (or not locale-suffixed). Shopify renders the checkout in
|
|
26
|
+
* the URL's trailing locale segment, and amount reconciliation reads the
|
|
27
|
+
* order summary by its ENGLISH labels — a store whose primary market is not
|
|
28
|
+
* English serves /checkouts/cn/<token>/<locale> and the total never parses
|
|
29
|
+
* (observed live 2026-08-16: /es-us rendered "Precio total" and the review
|
|
30
|
+
* refused fail-closed on a good checkout). The locale segment is
|
|
31
|
+
* presentation-only: swapping it keeps the same checkout session and token.
|
|
32
|
+
*/
|
|
33
|
+
export declare function shopifyEnglishCheckoutUrl(current: string): string | null;
|
|
34
|
+
export declare function missingContactRoles(filled: FilledField[], expected: string[]): string[];
|
|
35
|
+
/**
|
|
36
|
+
* The contact surface to prefill. Shipping fields when the checkout has them —
|
|
37
|
+
* but a digital-goods (no-shipping) Shopify checkout renders exactly one
|
|
38
|
+
* address block and marks every field autocomplete="billing ..." (observed
|
|
39
|
+
* live 2026-08-16: all 18 candidates billing-classified, so the shipping map
|
|
40
|
+
* came back empty and the prefill reported every role missing). That billing
|
|
41
|
+
* block IS the primary contact surface, under its base roles.
|
|
42
|
+
*/
|
|
43
|
+
export declare function contactPrefillFieldMap(detected: DetectResult): {
|
|
44
|
+
fields: FieldMap;
|
|
45
|
+
surface: 'shipping' | 'billing-only';
|
|
46
|
+
};
|
|
47
|
+
export declare class ShopifyAdapter implements CheckoutAdapter {
|
|
48
|
+
private readonly detected;
|
|
49
|
+
name: string;
|
|
50
|
+
constructor(detected: DetectResult);
|
|
51
|
+
matches(detected: DetectResult): boolean;
|
|
52
|
+
prepareContact(page: Page, contact: Contact): Promise<FillResult>;
|
|
53
|
+
fill(page: Page, _fields: FieldMap, credential: CardCredential, _contact: Contact): Promise<FillResult>;
|
|
54
|
+
}
|
|
55
|
+
export {};
|