@nextcommerce/campaigns-os 1.57.0 → 1.59.0
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/CHANGELOG.md +60 -0
- package/campaign-spec/dist/checkout-flow.d.ts +7 -0
- package/campaign-spec/dist/checkout-flow.js +17 -1
- package/compatibility.json +1 -1
- package/contracts/campaign-cart-sdk-support-policy.v0.json +2 -2
- package/contracts/commerce-surface-catalog.json +26 -27
- package/contracts/effects.v1.json +5 -5
- package/contracts/release-ledger.json +432 -0
- package/contracts/supported-surface.json +4 -4
- package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
- package/docs/build-packet.md +22 -8
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/qa-and-test-orders.md +54 -12
- package/docs/runtime-readiness.md +1 -1
- package/docs/skills-revision.md +10 -10
- package/package.json +1 -1
- package/schemas/campaign-runtime-build-context.v0.schema.json +2 -1
- package/schemas/campaign-runtime-build-packet.v0.schema.json +2 -1
- package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
- package/skills/campaign-readback-classification/SKILL.md +3 -3
- package/skills/campaign-run-evidence/SKILL.md +3 -3
- package/skills/contribution-intake/SKILL.md +3 -3
- package/skills/next-campaigns-build/SKILL.md +6 -6
- package/skills/next-campaigns-os/SKILL.md +3 -3
- package/skills/next-campaigns-os-setup/SKILL.md +3 -3
- package/skills/next-campaigns-polish/SKILL.md +3 -3
- package/skills/next-campaigns-qa/SKILL.md +3 -3
- package/skills.json +10 -10
- package/src/built-site-scope.mjs +91 -0
- package/src/cli.mjs +15 -23
- package/src/doctor/checks.mjs +73 -3
- package/src/doctor/next-step.mjs +3 -14
- package/src/polish-node.mjs +5 -4
- package/src/qa-browser.mjs +137 -2
- package/src/qa-cart-entry.mjs +105 -0
- package/src/qa-node.mjs +3 -1
- package/src/qa-verdict.mjs +24 -1
- package/src/sdk-attribute-index.mjs +8 -7
- package/src/source-html-intake.mjs +27 -18
- package/src/stage-record.mjs +200 -33
package/src/qa-cart-entry.mjs
CHANGED
|
@@ -35,6 +35,10 @@ export const CART_ENTRY_CODES = Object.freeze({
|
|
|
35
35
|
ENTRY_CONTROL_MISSING: "cart_entry_control_missing",
|
|
36
36
|
// The control was clicked but the page never reached the checkout URL.
|
|
37
37
|
ENTRY_NO_NAVIGATION: "cart_entry_no_navigation",
|
|
38
|
+
// A select page's bundle slot shows an empty variant control (size,
|
|
39
|
+
// colour) with no in-stock option to choose, so its Next would refuse.
|
|
40
|
+
// Fired before the checkout control is clicked (campaigns-os#667).
|
|
41
|
+
ENTRY_VARIANT_UNFILLED: "cart_entry_variant_unfilled",
|
|
38
42
|
// The SDK cart held zero items at submit time. Fired before the budget
|
|
39
43
|
// reservation and before the submit click.
|
|
40
44
|
CART_EMPTY_BEFORE_SUBMIT: "cart_empty_before_submit",
|
|
@@ -427,6 +431,107 @@ export function chooseCartEntryControl(controls = [], requested = []) {
|
|
|
427
431
|
// returns the store's `totalQuantity` — and the debugger's cart store second
|
|
428
432
|
// (`window.nextDebug.stores.cart`, present with `?debugger=true`), which is
|
|
429
433
|
// the only public place the line items and their package ids are readable.
|
|
434
|
+
// The variant controls a select page renders per bundle slot: the
|
|
435
|
+
// campaign-cart SDK injects a native select into each slot's
|
|
436
|
+
// [data-next-variant-selectors] (inside a .next-slot-variant-field). The
|
|
437
|
+
// olympus-mv-two-step starter, and pages built from it, hide that select behind
|
|
438
|
+
// a visible os-dropdown (a toggle button plus os-dropdown-item rows carrying
|
|
439
|
+
// `value`); a row click is what sets the select and dispatches `change`.
|
|
440
|
+
export const SLOT_VARIANT_SELECT_SELECTOR = "[data-next-variant-selectors] select, .next-slot-variant-field select";
|
|
441
|
+
export const SLOT_VARIANT_FIELD_SELECTOR = ".next-slot-variant-field";
|
|
442
|
+
export const SLOT_VARIANT_DROPDOWN_TOGGLE_SELECTOR = "os-dropdown .os-card__variant-dropdown-toggle";
|
|
443
|
+
export const SLOT_VARIANT_DROPDOWN_ROW_SELECTOR = "os-dropdown os-dropdown-item";
|
|
444
|
+
|
|
445
|
+
export const slotVariantKey = ({ bundle_id: bundleId, slot, variant_code: code } = {}) => `${bundleId ?? ""}|${slot}|${code ?? ""}`;
|
|
446
|
+
|
|
447
|
+
// evaluate() body: the next slot variant action, one per call because a
|
|
448
|
+
// choice can re-render its slot. In document order over the visible slot
|
|
449
|
+
// variant selects (a select counts as visible when it or its field is):
|
|
450
|
+
// - a field showing a dropdown UI whose key is not in `picked` gets a pick:
|
|
451
|
+
// the row matching the select's current value when that row is in stock
|
|
452
|
+
// (so the cart stays what the SDK chose), else the first in-stock row. The
|
|
453
|
+
// caller clicks the toggle and the row like a shopper, because a page may
|
|
454
|
+
// count a field as chosen only when a row was clicked. Pre-filled selects
|
|
455
|
+
// are picked too. Returned as { pick } with the field's index among
|
|
456
|
+
// SLOT_VARIANT_FIELD_SELECTOR matches and the row's index among the
|
|
457
|
+
// field's SLOT_VARIANT_DROPDOWN_ROW_SELECTOR matches;
|
|
458
|
+
// - a select with no dropdown UI is filled here only when it is empty, with
|
|
459
|
+
// its first in-stock option and input + change dispatched ({ filled });
|
|
460
|
+
// a pre-filled one is left alone.
|
|
461
|
+
// In stock: enabled, not hidden, non-empty value; for a row also not
|
|
462
|
+
// [disabled], aria-disabled, .next-oos, .next-variant-unavailable or
|
|
463
|
+
// data-available="false", and its native option not disabled. Nothing to
|
|
464
|
+
// choose returns { unfillable } naming the slot; nothing left returns
|
|
465
|
+
// { done: true }.
|
|
466
|
+
export function slotVariantStepScript() {
|
|
467
|
+
return ({ selector, fieldSelector, toggleSelector, rowSelector, picked = [] }) => {
|
|
468
|
+
const clean = (value) => String(value || "").replace(/\s+/g, " ").trim();
|
|
469
|
+
const isVisible = (element) => {
|
|
470
|
+
if (!element) return false;
|
|
471
|
+
const rect = element.getBoundingClientRect();
|
|
472
|
+
const style = getComputedStyle(element);
|
|
473
|
+
return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
|
|
474
|
+
};
|
|
475
|
+
const holderOf = (select) => select.closest(fieldSelector) || select.closest("[data-next-variant-selectors]");
|
|
476
|
+
const visible = Array.from(new Set(document.querySelectorAll(selector)))
|
|
477
|
+
.filter((select) => !select.disabled && (isVisible(select) || isVisible(holderOf(select))));
|
|
478
|
+
const slotRoots = Array.from(new Set(visible.map((select) => select.closest("[data-next-variant-selectors]") || holderOf(select))));
|
|
479
|
+
const fields = Array.from(document.querySelectorAll(fieldSelector));
|
|
480
|
+
const describe = (select) => {
|
|
481
|
+
const field = select.closest(fieldSelector);
|
|
482
|
+
const index = Number.parseInt(field?.getAttribute("data-next-slot-index") ?? "", 10);
|
|
483
|
+
const root = select.closest("[data-next-variant-selectors]") || holderOf(select);
|
|
484
|
+
return {
|
|
485
|
+
bundle_id: clean(field?.getAttribute("data-next-bundle-id")) || null,
|
|
486
|
+
slot: Number.isFinite(index) ? index + 1 : slotRoots.indexOf(root) + 1,
|
|
487
|
+
variant_code: clean(select.getAttribute("data-next-variant-code") || select.getAttribute("data-variant-code") || field?.getAttribute("data-next-variant-code") || select.name) || null,
|
|
488
|
+
};
|
|
489
|
+
};
|
|
490
|
+
const keyOf = (info) => `${info.bundle_id ?? ""}|${info.slot}|${info.variant_code ?? ""}`;
|
|
491
|
+
const optionInStock = (option) => !option.disabled && !option.hidden && clean(option.value) !== "";
|
|
492
|
+
for (const select of visible) {
|
|
493
|
+
const info = describe(select);
|
|
494
|
+
const field = select.closest(fieldSelector);
|
|
495
|
+
const toggle = field ? Array.from(field.querySelectorAll(toggleSelector)).find(isVisible) : null;
|
|
496
|
+
if (toggle) {
|
|
497
|
+
if (picked.includes(keyOf(info))) continue;
|
|
498
|
+
const rows = Array.from(field.querySelectorAll(rowSelector));
|
|
499
|
+
// One in-stock rule: a row is in stock when its own markup says so and
|
|
500
|
+
// its native option (if any) passes optionInStock.
|
|
501
|
+
const optionsByValue = new Map(Array.from(select.options).map((option) => [clean(option.value), option]));
|
|
502
|
+
const rowInStock = (row) => {
|
|
503
|
+
const value = clean(row.getAttribute("value"));
|
|
504
|
+
if (!value || row.hidden || row.hasAttribute("disabled") || row.getAttribute("aria-disabled") === "true") return false;
|
|
505
|
+
if (row.classList.contains("next-oos") || row.classList.contains("next-variant-unavailable") || row.getAttribute("data-available") === "false") return false;
|
|
506
|
+
const option = optionsByValue.get(value);
|
|
507
|
+
return !option || optionInStock(option);
|
|
508
|
+
};
|
|
509
|
+
const current = clean(select.value);
|
|
510
|
+
const row = (current && rows.find((candidate) => clean(candidate.getAttribute("value")) === current && rowInStock(candidate))) || rows.find(rowInStock);
|
|
511
|
+
if (!row) return { unfillable: { ...info, options: rows.length, via: "dropdown" } };
|
|
512
|
+
const fieldIndex = fields.indexOf(field);
|
|
513
|
+
const rowIndex = rows.indexOf(row);
|
|
514
|
+
if (fieldIndex < 0 || rowIndex < 0) {
|
|
515
|
+
return { unfillable: { ...info, options: rows.length, via: "dropdown", detail: `the dropdown ${fieldIndex < 0 ? "field" : "row"} could not be located` } };
|
|
516
|
+
}
|
|
517
|
+
return { pick: { ...info, value: clean(row.getAttribute("value")), field_index: fieldIndex, row_index: rowIndex } };
|
|
518
|
+
}
|
|
519
|
+
if (clean(select.value) !== "") continue;
|
|
520
|
+
const option = Array.from(select.options).find(optionInStock);
|
|
521
|
+
if (!option) return { unfillable: { ...info, options: select.options.length, via: "select" } };
|
|
522
|
+
select.value = option.value;
|
|
523
|
+
select.dispatchEvent(new Event("input", { bubbles: true }));
|
|
524
|
+
select.dispatchEvent(new Event("change", { bubbles: true }));
|
|
525
|
+
return { filled: { ...info, value: option.value } };
|
|
526
|
+
}
|
|
527
|
+
return { done: true };
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function slotVariantLabel({ bundle_id: bundleId, slot, variant_code: code } = {}) {
|
|
532
|
+
return `slot ${slot}${bundleId ? ` (bundle "${bundleId}")` : ""}${code ? ` ${code}` : " variant"}`;
|
|
533
|
+
}
|
|
534
|
+
|
|
430
535
|
// The enriched line list on `getCartData()` is deliberately NOT read: it is
|
|
431
536
|
// always empty on the shipped SDK (campaign-cart#36; see the cart-state
|
|
432
537
|
// verification section of docs/qa-and-test-orders.md), so it would call every
|
package/src/qa-node.mjs
CHANGED
|
@@ -37,7 +37,7 @@ import { assessReceiptPurchase } from "./qa-analytics-correctness.mjs";
|
|
|
37
37
|
import { trackingQaAssertion, trackingRunScopeRows } from "./qa-tracking-params.mjs";
|
|
38
38
|
import { contentParamNotRequestedRows, contentParamQaAssertion } from "./qa-content-params.mjs";
|
|
39
39
|
import { policyLinkNotRequestedRows, policyLinkQaAssertion } from "./qa-policy-links.mjs";
|
|
40
|
-
import { createVerdict, isFindingAssertion, QA_ASSERTION_FAMILY_VOCABULARY, SESSION_ENDING_DISPOSITIONS, SEVERITY, STATUS, validateVerdict } from "./qa-verdict.mjs";
|
|
40
|
+
import { createVerdict, formatTestOrderSummary, isFindingAssertion, QA_ASSERTION_FAMILY_VOCABULARY, SESSION_ENDING_DISPOSITIONS, SEVERITY, STATUS, validateVerdict } from "./qa-verdict.mjs";
|
|
41
41
|
import { normalizeSdkMetaName, lookupSdkIgnoredMetaTag } from "./sdk-meta-tags.mjs";
|
|
42
42
|
import { annotateQaAssertionCauses, formatCauseReportLines, formatCauseTag } from "./finding-cause.mjs";
|
|
43
43
|
import { promoteQaVerdict, writeQaSidecar } from "./qa-sidecar.mjs";
|
|
@@ -3577,6 +3577,8 @@ function output(value, args) {
|
|
|
3577
3577
|
console.log("This is a passing proof with exceptions. Report these exceptions to the operator; do not clear or waive them, or change markup just to make them pass.");
|
|
3578
3578
|
}
|
|
3579
3579
|
console.log(`Counts: ${Object.entries(value.counts).map(([status, count]) => `${count} ${status}`).join(", ")}`);
|
|
3580
|
+
const testOrderSummary = formatTestOrderSummary(value.verdict);
|
|
3581
|
+
if (testOrderSummary) console.log(testOrderSummary);
|
|
3580
3582
|
printCauseLines(value.verdict);
|
|
3581
3583
|
printThemeGateLines(value.theme_gate, value.packet_path, value.report_path);
|
|
3582
3584
|
if (value.commercial) {
|
package/src/qa-verdict.mjs
CHANGED
|
@@ -256,7 +256,8 @@ export function summarizePurchaseProof({ verdict = null, proofPolicy = null } =
|
|
|
256
256
|
return {
|
|
257
257
|
declared_order_path_depth: optionalString(proofPolicy?.order_path_depth),
|
|
258
258
|
declared_typed_card_depth: optionalString(proofPolicy?.typed_card_depth),
|
|
259
|
-
order_paths_executed: orders.length,
|
|
259
|
+
order_paths_executed: orders.filter((order) => !isTestOrderRetry(order)).length,
|
|
260
|
+
test_order_attempts: orders.length,
|
|
260
261
|
orders_created: created.length,
|
|
261
262
|
orders_verified: orders.filter((order) => order.verification?.verified === true).length,
|
|
262
263
|
// null, not false, when nothing ran: "no order was out of test mode" and
|
|
@@ -265,6 +266,28 @@ export function summarizePurchaseProof({ verdict = null, proofPolicy = null } =
|
|
|
265
266
|
};
|
|
266
267
|
}
|
|
267
268
|
|
|
269
|
+
// A re-run of a planned path is its own test_orders[] entry with `attempt` 2;
|
|
270
|
+
// it is an attempt, not another path. Entries written before `attempt` existed
|
|
271
|
+
// each count as a path, which is what they were counted as then.
|
|
272
|
+
function isTestOrderRetry(order) {
|
|
273
|
+
const attempt = Number(order?.attempt);
|
|
274
|
+
return Number.isInteger(attempt) && attempt > 1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function plural(count, noun) {
|
|
278
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// The human summary line for a run that drove typed-card test orders: planned
|
|
282
|
+
// paths, attempts (a re-run is a second attempt at the same path) and orders
|
|
283
|
+
// the platform created, kept apart so a path that was retried is not read as
|
|
284
|
+
// two paths against the --max-test-orders cap. null when no order path ran.
|
|
285
|
+
export function formatTestOrderSummary(verdict) {
|
|
286
|
+
const summary = summarizePurchaseProof({ verdict });
|
|
287
|
+
if (!summary.test_order_attempts) return null;
|
|
288
|
+
return `Test orders: ${plural(summary.order_paths_executed, "path")}, ${plural(summary.test_order_attempts, "attempt")}, ${plural(summary.orders_created, "order")} created`;
|
|
289
|
+
}
|
|
290
|
+
|
|
268
291
|
// The browser placeholder-text residue gate (H3.1) emits one assertion per
|
|
269
292
|
// page as `template-residue:<page_id>:placeholder-text`. Both the emitter
|
|
270
293
|
// (qa-browser.mjs) and the stage-ledger summary below build the id from this
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// checks (#303).
|
|
3
3
|
//
|
|
4
4
|
// Source: the SDK's own source in NextCommerceCo/campaign-cart at tag
|
|
5
|
-
// v0.4.
|
|
5
|
+
// v0.4.42 (the SDK pin every certified starter family ships today): every
|
|
6
6
|
// data-next-* name the SDK's non-test TypeScript names as a string, plus every
|
|
7
7
|
// `dataset.next*` property it reads or writes, turned back into its
|
|
8
8
|
// attribute. Only the data-next-* names are kept: the checks that read this
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
// whole from the source at a named tag, never edited by hand.
|
|
16
16
|
//
|
|
17
17
|
// paths=('src/**/*.ts' ':!src/**/*.test.ts' ':!src/**/tests/**' ':!src/docs/**')
|
|
18
|
-
// { git -C ../campaign-cart grep -h -oP 'data-next-[a-z0-9]+(?:-[a-z0-9]+)*-?' v0.4.
|
|
19
|
-
// git -C ../campaign-cart grep -h -oP 'dataset\.next[A-Z][A-Za-z0-9]*' v0.4.
|
|
18
|
+
// { git -C ../campaign-cart grep -h -oP 'data-next-[a-z0-9]+(?:-[a-z0-9]+)*-?' v0.4.42 -- "${paths[@]}"
|
|
19
|
+
// git -C ../campaign-cart grep -h -oP 'dataset\.next[A-Z][A-Za-z0-9]*' v0.4.42 -- "${paths[@]}" \
|
|
20
20
|
// | sed 's/^dataset\.//' | perl -pe 's/([A-Z])/-\L$1/g; s/^/data-/'
|
|
21
21
|
// } | sort -u
|
|
22
22
|
//
|
|
@@ -30,14 +30,15 @@
|
|
|
30
30
|
// manifests never did (the address block's, data-next-i18n,
|
|
31
31
|
// data-next-bump-section, data-next-button-text, the variant option state).
|
|
32
32
|
// At v0.4.41 it returns every name of the v0.4.38 list this replaces, plus
|
|
33
|
-
// those and data-next-phone-e164 / data-next-phone-country.
|
|
33
|
+
// those and data-next-phone-e164 / data-next-phone-country. At v0.4.42 it
|
|
34
|
+
// returns the same 149 names as at v0.4.41.
|
|
34
35
|
//
|
|
35
36
|
// A name ending in "-" (data-next-class-) is a prefix the SDK reads with any
|
|
36
37
|
// suffix.
|
|
37
38
|
|
|
38
39
|
import { RELEASED_SDK_VERSION_PATTERN } from "../campaign-spec/dist/index.js";
|
|
39
40
|
|
|
40
|
-
export const SDK_ATTRIBUTE_INDEX_VERSION = "0.4.
|
|
41
|
+
export const SDK_ATTRIBUTE_INDEX_VERSION = "0.4.42";
|
|
41
42
|
|
|
42
43
|
export const SDK_DATA_NEXT_ATTRIBUTES = Object.freeze([
|
|
43
44
|
"data-next-accordion",
|
|
@@ -364,8 +365,8 @@ export const SDK_TEMPLATE_PLACEHOLDERS = Object.freeze({
|
|
|
364
365
|
});
|
|
365
366
|
|
|
366
367
|
// The SDK versions whose renderer files above were verified unchanged from
|
|
367
|
-
// v0.4.38 (git blob ids equal at v0.4.38, v0.4.39, v0.4.40 and v0.4.
|
|
368
|
+
// v0.4.38 (git blob ids equal at v0.4.38, v0.4.39, v0.4.40, v0.4.41 and v0.4.42, for
|
|
368
369
|
// the eight renderer files cited above plus cart-item-list.enhancer.ts and
|
|
369
370
|
// order-item-list.enhancer.ts). A page whose loader pins another version
|
|
370
371
|
// cannot pass the placeholder check.
|
|
371
|
-
export const SDK_TEMPLATE_PLACEHOLDERS_VERIFIED_PINS = Object.freeze(["0.4.38", "0.4.39", "0.4.40", "0.4.41"]);
|
|
372
|
+
export const SDK_TEMPLATE_PLACEHOLDERS_VERIFIED_PINS = Object.freeze(["0.4.38", "0.4.39", "0.4.40", "0.4.41", "0.4.42"]);
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./source-html-manifest.mjs";
|
|
5
5
|
// Built output of campaign-spec (same import shape as src/page-kit-sdk-version.mjs),
|
|
6
6
|
// so build-time wiring and spec-time analysis share one edge resolver.
|
|
7
|
-
import { checkoutPathFrom, declineRouteTarget, forwardRouteTarget, forwardTargetPage, isCheckoutStepPage } from "../campaign-spec/dist/index.js";
|
|
7
|
+
import { checkoutPathFrom, declineRouteTarget, forwardRouteTarget, forwardTargetPage, isCheckoutStepPage, isPrePaymentPage } from "../campaign-spec/dist/index.js";
|
|
8
8
|
import { isAbsoluteHttpUrl, normalizePageKitRoute, normalizePublicRouteSlug, stripPublicRoutePrefix } from "./route-identity.mjs";
|
|
9
9
|
|
|
10
10
|
const CPK_PAGE_TYPES = new Set(["product", "checkout", "upsell", "receipt"]);
|
|
@@ -570,11 +570,12 @@ function pageKitProjectionForPage(page, { pageById, publicRouteSlug, outputDir,
|
|
|
570
570
|
const frontmatter = { page_type: pageType };
|
|
571
571
|
if (permalinkRequired) frontmatter.permalink = publicRoute;
|
|
572
572
|
|
|
573
|
-
const
|
|
574
|
-
|
|
573
|
+
const forwardUrl = nextUrlForPage(page, pageById, publicRouteSlug);
|
|
574
|
+
const checkoutFlow = checkoutFlowFrontmatter(page, forwardUrl, { specPages, pageById, publicRouteSlug });
|
|
575
|
+
if (checkoutFlow) Object.assign(frontmatter, checkoutFlow);
|
|
576
|
+
else if (forwardUrl) frontmatter.next_url = forwardUrl;
|
|
575
577
|
const declineUrl = declineUrlForPage(page, pageById, publicRouteSlug);
|
|
576
|
-
if (declineUrl && declineUrl !==
|
|
577
|
-
Object.assign(frontmatter, checkoutFlowFrontmatter(page, { specPages, pageById, publicRouteSlug }));
|
|
578
|
+
if (declineUrl && declineUrl !== forwardUrl) frontmatter.decline_url = declineUrl;
|
|
578
579
|
|
|
579
580
|
return {
|
|
580
581
|
target_path: targetPath,
|
|
@@ -587,24 +588,32 @@ function pageKitProjectionForPage(page, { pageById, publicRouteSlug, outputDir,
|
|
|
587
588
|
};
|
|
588
589
|
}
|
|
589
590
|
|
|
590
|
-
// Multi-step checkout wiring (campaigns-os#641)
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
//
|
|
591
|
+
// Multi-step checkout wiring (campaigns-os#641) for a select or checkout_step
|
|
592
|
+
// page on a path to a Checkout. The keys follow the starter-template contract
|
|
593
|
+
// (campaign-cart-starter-templates#223). Null for any other page, whose
|
|
594
|
+
// next_url stays its own forward link: a landing page's call-to-action reads
|
|
595
|
+
// next_url, so pointing it past the Checkout would skip payment.
|
|
596
|
+
// next_url where a completed order goes: the Checkout's post-payment
|
|
597
|
+
// destination (its own next_url), the meaning next_url has on a
|
|
598
|
+
// single-page checkout. The page's next-success-url meta tag
|
|
599
|
+
// carries it, because the SDK reads that tag for express orders
|
|
600
|
+
// placed on this page; an express order on step 1 must land on
|
|
601
|
+
// the first upsell, not on step 2.
|
|
602
|
+
// next_step this page's forward link: a step form's
|
|
603
|
+
// data-next-checkout-step, a select page's checkout link.
|
|
599
604
|
// step_number on a checkout_step only: its position among the steps of its
|
|
600
605
|
// path, from 1, for the form's data-next-step-number.
|
|
601
|
-
function checkoutFlowFrontmatter(page, { specPages, pageById, publicRouteSlug }) {
|
|
602
|
-
|
|
606
|
+
function checkoutFlowFrontmatter(page, forwardUrl, { specPages, pageById, publicRouteSlug }) {
|
|
607
|
+
// A path of two or more pages means the page has a resolvable forward link,
|
|
608
|
+
// so forwardUrl is set; the guard keeps the pair together regardless, never
|
|
609
|
+
// emitting the Checkout's destination without a next_step to navigate by.
|
|
610
|
+
if (!isPrePaymentPage(page) || !forwardUrl) return null;
|
|
603
611
|
const path = checkoutPathFrom(specPages, page);
|
|
604
|
-
if (!path || path.length < 2) return
|
|
612
|
+
if (!path || path.length < 2) return null;
|
|
605
613
|
const out = {};
|
|
606
614
|
const successUrl = nextUrlForPage(path[path.length - 1], pageById, publicRouteSlug);
|
|
607
|
-
if (successUrl) out.
|
|
615
|
+
if (successUrl) out.next_url = successUrl;
|
|
616
|
+
out.next_step = forwardUrl;
|
|
608
617
|
if (isCheckoutStepPage(page)) out.step_number = checkoutStepNumber(page, specPages);
|
|
609
618
|
return out;
|
|
610
619
|
}
|