@checkbox-site/cli 0.4.5 → 0.4.7
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 +1 -1
- package/dist/checkbox.js +1126 -146
- package/package.json +16 -13
package/dist/checkbox.js
CHANGED
|
@@ -3335,7 +3335,7 @@ var init_operations = __esm({
|
|
|
3335
3335
|
/**
|
|
3336
3336
|
* Register the Checkout Session params your code uses.
|
|
3337
3337
|
*
|
|
3338
|
-
* Registers exactly the object your code passes to stripe.checkout.sessions.create (customer, URLs and secrets are not needed and are ignored; `warnings` says what was not stored). It is judged exactly like checkStripeCheckoutSession, discounts included. A config with the same name, or for the same Price ids, replaces the older one (`replaced`). Test runs mirror it into Stripe test mode; when it matches your latest live checkout, the scanned test page counts as an observation of your live checkout. Re-register after changing your checkout code, then call createTestRun.
|
|
3338
|
+
* Registers exactly the object your code passes to stripe.checkout.sessions.create, as `params` (that object sent alone as the body is accepted too; customer, URLs and secrets are not needed and are ignored; `warnings` says what was not stored). It is judged exactly like checkStripeCheckoutSession, discounts included. A config with the same name, or for the same Price ids, replaces the older one (`replaced`). Test runs mirror it into Stripe test mode; when it matches your latest live checkout, the scanned test page counts as an observation of your live checkout. Re-register after changing your checkout code, then call createTestRun.
|
|
3339
3339
|
*
|
|
3340
3340
|
* @param site Unique identifier of the site (site_…).
|
|
3341
3341
|
* @see POST /v1/sites/{site}/stripe_checkout_configs (operation `createStripeCheckoutConfig`, scope `test_runs:write`)
|
|
@@ -3617,7 +3617,7 @@ var SDK_VERSION;
|
|
|
3617
3617
|
var init_version = __esm({
|
|
3618
3618
|
"../sdk-node/src/version.ts"() {
|
|
3619
3619
|
"use strict";
|
|
3620
|
-
SDK_VERSION = "0.2.
|
|
3620
|
+
SDK_VERSION = "0.2.5";
|
|
3621
3621
|
}
|
|
3622
3622
|
});
|
|
3623
3623
|
|
|
@@ -4484,7 +4484,23 @@ function exitCodeFor(report, strict) {
|
|
|
4484
4484
|
if (report.verdict === "needs_review" && strict) return EXIT.needs_review;
|
|
4485
4485
|
return EXIT.ok;
|
|
4486
4486
|
}
|
|
4487
|
+
function renderSchedule(report, out, opts) {
|
|
4488
|
+
const c = out.c;
|
|
4489
|
+
const notices = report.notices ?? [];
|
|
4490
|
+
const where2 = report.jurisdictions === "all" || !report.jurisdictions ? "all jurisdictions" : report.jurisdictions.join(", ");
|
|
4491
|
+
const lines = [`${c.bold("NOTICE SCHEDULE")} ${notices.length} notice${notices.length === 1 ? "" : "s"} to send \xB7 ${where2} \xB7 rules ${report.rules_version ?? "?"} \xB7 catalog ${report.catalog_version ?? "?"}`];
|
|
4492
|
+
for (const n of notices.slice(0, opts.verbose ? 200 : 12)) {
|
|
4493
|
+
lines.push(` ${n.send_date ?? "?"} ${n.kind ?? "notice"}${n.occasion ? c.dim(` \xB7 ${n.occasion}`) : ""}${n.days_before ? c.dim(` (${n.days_before} days before)`) : ""}${n.citations?.length ? c.dim(` \xB7 ${n.citations.slice(0, 2).join("; ")}${n.citations.length > 2 ? " \u2026" : ""}`) : ""}`);
|
|
4494
|
+
}
|
|
4495
|
+
if (!opts.verbose && notices.length > 12) lines.push(c.dim(` \u2026 ${notices.length - 12} more (--verbose or --json)`));
|
|
4496
|
+
const notRequired = report.not_required ?? [];
|
|
4497
|
+
if (notRequired.length && opts.verbose) for (const n of notRequired) lines.push(c.dim(` not required: ${n.kind ?? ""}${n.reason ? ` \u2014 ${n.reason}` : ""}`));
|
|
4498
|
+
if (report.next_action?.message) lines.push("", `${c.cyan("Next:")} ${wrap(report.next_action.message, 110, " ")}`);
|
|
4499
|
+
if (report.disclaimer) lines.push(c.dim(report.disclaimer));
|
|
4500
|
+
return lines;
|
|
4501
|
+
}
|
|
4487
4502
|
function renderReport(report, out, opts) {
|
|
4503
|
+
if (!report.verdict && Array.isArray(report.notices)) return renderSchedule(report, out, opts);
|
|
4488
4504
|
const c = out.c;
|
|
4489
4505
|
const icon2 = (s2) => s2 === "pass" ? c.green("\u2713") : s2 === "fail" ? c.red("\u2717") : s2 === "needs_review" ? c.yellow("?") : c.dim("\u2013");
|
|
4490
4506
|
const verdict = report.verdict ?? "unknown";
|
|
@@ -4518,7 +4534,9 @@ function renderReport(report, out, opts) {
|
|
|
4518
4534
|
lines.push(...JSON.stringify(report.correction.patch, null, 2).split("\n").map((l) => ` ${l}`));
|
|
4519
4535
|
}
|
|
4520
4536
|
if (report.template && verdict !== "pass" && verdict !== "not_applicable") {
|
|
4521
|
-
|
|
4537
|
+
const toFill = report.template.placeholders ?? [];
|
|
4538
|
+
const label = !report.template.verified ? c.bold("Suggested template") : toFill.length ? `${c.bold(`Suggested template (passes the checks once you replace ${toFill.join(", ")} with your real value${toFill.length === 1 ? "" : "s"})`)}` : `${c.bold("Suggested template (passes the checks)")} ${c.green("(verified)")}`;
|
|
4539
|
+
lines.push("", label, ` Subject: ${report.template.subject}`, ...report.template.text.split("\n").map((l) => ` ${l}`));
|
|
4522
4540
|
}
|
|
4523
4541
|
if (report.next_action?.message) lines.push("", `${c.cyan("Next:")} ${wrap(report.next_action.message, 110, " ")}`);
|
|
4524
4542
|
if (report.disclaimer) lines.push(c.dim(report.disclaimer));
|
|
@@ -4736,16 +4754,63 @@ var init_stripe_ids = __esm({
|
|
|
4736
4754
|
});
|
|
4737
4755
|
|
|
4738
4756
|
// ../core/src/stripe-key.ts
|
|
4757
|
+
function stripeKeyPermissionLine(filter) {
|
|
4758
|
+
return STRIPE_KEY_PERMISSIONS.filter(filter ?? (() => true)).map((p) => `${p.resource}: ${p.access}`).join(", ");
|
|
4759
|
+
}
|
|
4760
|
+
function approvalKeyAsk(opts = {}) {
|
|
4761
|
+
if (opts.stripe === false) return "";
|
|
4762
|
+
const create = `create a restricted key in Stripe Test mode (${STRIPE_TEST_API_KEYS_URL} \u2192 Create restricted key) ticking ${stripeKeyPermissionLine()}, and put it in the project's env file (never in the chat)`;
|
|
4763
|
+
return `${opts.stripe ? "In the same message" : "If the site sells through Stripe, in the same message"}, ask them to tick \u201CLet your agent store a Stripe test-mode key\u201D on that page${opts.keyFound ? "" : ` and, if the project has no rk_test_ key yet, to ${create}`}: one approval then covers the key and the test subscription.`;
|
|
4764
|
+
}
|
|
4765
|
+
var STRIPE_KEY_PERMISSIONS, STRIPE_TEST_API_KEYS_URL;
|
|
4739
4766
|
var init_stripe_key = __esm({
|
|
4740
4767
|
"../core/src/stripe-key.ts"() {
|
|
4741
4768
|
"use strict";
|
|
4769
|
+
STRIPE_KEY_PERMISSIONS = [
|
|
4770
|
+
{ resource: "Customers", access: "Write", why: "Hosted cancellation page: finds the subscriber by email (Read). Test runs: create a test customer (Write, test mode only).", uses: ["fixes", "test_runs"], optional: true, writeWhen: "Used only in test mode (test runs)." },
|
|
4771
|
+
{ resource: "Products", access: "Read", why: "Plan names next to your prices.", uses: ["checks", "test_runs"] },
|
|
4772
|
+
{ resource: "Prices", access: "Read", why: "Amounts, billing intervals and trial lengths, compared with what your pages say.", uses: ["checks", "test_runs"] },
|
|
4773
|
+
{ resource: "Coupons", access: "Read", why: "Only when your hosted cancellation page offers a coupon as the save offer.", uses: ["fixes"], optional: true },
|
|
4774
|
+
{ resource: "Subscriptions", access: "Write", why: "Read: which trials subscribers actually get. Write: cancels when a subscriber asks on your hosted cancellation page, and test runs create test subscriptions.", uses: ["checks", "fixes", "test_runs"], writeWhen: "Used only after you turn on the hosted cancellation page (you approve it in one click), or in test mode." },
|
|
4775
|
+
{ resource: "Test clocks", access: "Write", why: "Test runs fast-forward through trial end and renewal (test-mode keys only; nothing is charged).", uses: ["test_runs"], optional: true, writeWhen: "Used only in test mode." },
|
|
4776
|
+
{ resource: "Customer portal", access: "Write", why: "Read: whether subscribers can cancel online and your portal link. Write: test runs open the portal to cancel the test subscription.", uses: ["checks", "test_runs"], writeWhen: "Used only in test mode (test runs)." },
|
|
4777
|
+
{ resource: "Checkout Sessions", access: "Write", why: "Read: your checkout text and terms-consent setting. Write: test runs start a test checkout.", uses: ["checks", "test_runs"], writeWhen: "Used only in test mode (test runs)." },
|
|
4778
|
+
{ resource: "Payment Links", access: "Write", why: "Read: consent and renewal text on your Payment Links. Write: the Payment Link fix, only when you click Approve.", uses: ["checks", "fixes"], writeWhen: "Used only after you click Approve on the Payment Link fix, or in test mode." },
|
|
4779
|
+
{ resource: "Webhook Endpoints", access: "Write", why: "Registers Checkbox\u2019s webhook endpoint, so renewals and cancellations are seen as they happen.", uses: ["checks"], writeWhen: "Used once, when you connect the key, to add Checkbox\u2019s own webhook endpoint (and to remove it when you disconnect); nothing else." }
|
|
4780
|
+
];
|
|
4781
|
+
STRIPE_TEST_API_KEYS_URL = "https://dashboard.stripe.com/test/apikeys";
|
|
4742
4782
|
}
|
|
4743
4783
|
});
|
|
4744
4784
|
|
|
4745
4785
|
// ../core/src/go-live.ts
|
|
4786
|
+
function goLiveSteps(input = {}) {
|
|
4787
|
+
const out = [];
|
|
4788
|
+
if (!input.domainVerified) out.push({ key: "verify_domain", who: "person", text: "Verify your domain (a DNS record or a file on your site). Live checks start only on a verified public domain." });
|
|
4789
|
+
if (input.termsOfServiceRequired && !input.termsOfServiceUrlSet) out.push({ key: "stripe_terms_url", who: "person", text: GO_LIVE_TERMS_URL_STEP, url: STRIPE_PUBLIC_DETAILS });
|
|
4790
|
+
if (!input.liveKeyConnected)
|
|
4791
|
+
out.push({
|
|
4792
|
+
key: "live_stripe_key",
|
|
4793
|
+
who: "person",
|
|
4794
|
+
text: "Switch the dashboard to Live mode and connect your live Stripe key (rk_live_\u2026) with the same permissions as your test key (the checklist next to the key field: Read for the checks; Write only for fixes you approve and to add Checkbox\u2019s webhook endpoint). Only a person adds live keys; an agent never does."
|
|
4795
|
+
});
|
|
4796
|
+
if (input.checkoutKind === "payment_link")
|
|
4797
|
+
out.push({ key: "live_checkout", who: "person", text: "Create the live Payment Link in Stripe live mode with the same settings as the test link Checkbox checked (renewal terms, consent checkbox, cancellation link); then your agent puts the live link on your site in place of the test link." });
|
|
4798
|
+
else if (input.checkoutKind === "stripe_checkout")
|
|
4799
|
+
out.push({ key: "live_checkout", who: "agent", text: "Ship the same Checkout Session params Checkbox checked with your live Price ids (the live Stripe secret key in your production settings is set by a person)." });
|
|
4800
|
+
out.push({
|
|
4801
|
+
key: "promote_site",
|
|
4802
|
+
who: "person",
|
|
4803
|
+
text: `Promote the site to live: Checkbox copies your Checkbox Notices settings, confirmed cancellation policy, support email and New York answers.${input.checkboxSends === false ? "" : " If Checkbox sends your emails, preview them and confirm once that it may email your real subscribers."}`
|
|
4804
|
+
});
|
|
4805
|
+
return out;
|
|
4806
|
+
}
|
|
4807
|
+
var STRIPE_PUBLIC_DETAILS, GO_LIVE_TERMS_URL_STEP, GO_LIVE_STEPS;
|
|
4746
4808
|
var init_go_live = __esm({
|
|
4747
4809
|
"../core/src/go-live.ts"() {
|
|
4748
4810
|
"use strict";
|
|
4811
|
+
STRIPE_PUBLIC_DETAILS = "https://dashboard.stripe.com/settings/public";
|
|
4812
|
+
GO_LIVE_TERMS_URL_STEP = 'Set your Terms of Service URL in the Stripe Dashboard (Settings > Public details). Your checkout asks shoppers to accept your terms (consent_collection.terms_of_service = "required"), and Stripe refuses those Checkout Sessions until that URL is set.';
|
|
4813
|
+
GO_LIVE_STEPS = goLiveSteps().map((s2) => s2.text);
|
|
4749
4814
|
}
|
|
4750
4815
|
});
|
|
4751
4816
|
|
|
@@ -4988,7 +5053,7 @@ var init_california = __esm({
|
|
|
4988
5053
|
notes: notes(
|
|
4989
5054
|
AB2863_APPLICABILITY,
|
|
4990
5055
|
"Misrepresentation is only partly machine-detectable; the auto-renewal language check catches the most common case (offers that hide or deny the renewal).",
|
|
4991
|
-
'Severity raised from medium to critical (catalog 2026.11.01.1, docs/legal/LITIGATION_PRIORITIES.md): misrepresentation is the theory behind the largest recent outcomes \u2014 "cancel at any time" without a prorated refund (Sanderson v. Whoop, sent to a jury in 2026), "no commitment" packs that renewed (FTC v. Shutterstock
|
|
5056
|
+
'Severity raised from medium to critical (catalog 2026.11.01.1, docs/legal/LITIGATION_PRIORITIES.md): misrepresentation is the theory behind the largest recent outcomes \u2014 "cancel at any time" without a prorated refund (Sanderson v. Whoop, sent to a jury in 2026), "no commitment" packs that renewed (FTC v. Shutterstock: proposed $35M order filed May 2026), a hidden early-termination fee (Adobe: Justice Department complaint filed June 2024).',
|
|
4992
5057
|
'CHECK GAP: no check yet flags "cancel anytime" / "no commitment" copy contradicted by the refund or term policy, or a one-time purchase that enrolls the buyer in a subscription (product opportunity: a rollover and misrepresentation check). terms.refund_policy_consistent covers advertised refund promises.'
|
|
4993
5058
|
)
|
|
4994
5059
|
}),
|
|
@@ -11321,6 +11386,9 @@ function introAmountOf(amount, o) {
|
|
|
11321
11386
|
return void 0;
|
|
11322
11387
|
}
|
|
11323
11388
|
function unverifiedReason(ctx) {
|
|
11389
|
+
const known = ctx.referencePlans ?? [];
|
|
11390
|
+
if (known.length) return `none of your plans (${known.slice(0, 4).map((p) => p.label ?? p.name ?? p.id).join(", ")}) was matched to this checkout's price (the Price id it sells is not one Checkbox has for them)`;
|
|
11391
|
+
if ((ctx.checkoutConfigs?.length ?? 0) > 0) return "the price this checkout sells is not in your registered Checkout configs or your billing platform's prices";
|
|
11324
11392
|
const chosen = ctx.facts?.planPriceId;
|
|
11325
11393
|
if (chosen && ctx.config) return `the chosen price ${chosen} is not among the active recurring prices read from your billing platform`;
|
|
11326
11394
|
if (chosen) return `the chosen price ${chosen} could not be read (no billing platform connected)`;
|
|
@@ -11461,8 +11529,8 @@ function sameProductName(a, b) {
|
|
|
11461
11529
|
function declaredPriceIds(x) {
|
|
11462
11530
|
return [x.price_id, x.stripe_price, x.priceId, x.id].filter((v) => typeof v === "string" && v.trim().length > 0);
|
|
11463
11531
|
}
|
|
11464
|
-
function reconcileDeclaredPlans(raw, billing, platform = "Stripe") {
|
|
11465
|
-
const prices = billing.filter((p) => p && p.amount > 0 && p.interval);
|
|
11532
|
+
function reconcileDeclaredPlans(raw, billing, platform = "Stripe", checkoutTrials) {
|
|
11533
|
+
const prices = billing.filter((p) => p && p.amount > 0 && p.interval).map((p) => checkoutTrials?.has(p.id) ? { ...p, trialDays: checkoutTrials.get(p.id) } : p);
|
|
11466
11534
|
const out = [...raw];
|
|
11467
11535
|
const conflicts = [];
|
|
11468
11536
|
if (!prices.length || !raw.length) return { plans: out, conflicts };
|
|
@@ -12103,6 +12171,13 @@ function checkoutSessionsOf(config) {
|
|
|
12103
12171
|
if (!Array.isArray(raw)) return [];
|
|
12104
12172
|
return raw.filter((s2) => !!s2 && typeof s2 === "object" && typeof s2.id === "string" && Array.isArray(s2.lineItems));
|
|
12105
12173
|
}
|
|
12174
|
+
function withTestRunSessions(sessions, runSessionIds) {
|
|
12175
|
+
if (!runSessionIds.size) return [...sessions];
|
|
12176
|
+
return sessions.map((s2) => !s2.checkboxTestRun && runSessionIds.has(s2.id) ? { ...s2, checkboxTestRun: true } : s2);
|
|
12177
|
+
}
|
|
12178
|
+
function testRunSessionIds(runs) {
|
|
12179
|
+
return new Set(runs.map((r) => r.state?.sessionId).filter((x) => typeof x === "string" && x.startsWith("cs_")));
|
|
12180
|
+
}
|
|
12106
12181
|
function newestFirst(sessions) {
|
|
12107
12182
|
return sessions.map((s2, i) => ({ s: s2, i })).sort((a, b) => (typeof b.s.created === "number" ? b.s.created : -Infinity) - (typeof a.s.created === "number" ? a.s.created : -Infinity) || a.i - b.i).map((x) => x.s);
|
|
12108
12183
|
}
|
|
@@ -14917,7 +14992,8 @@ function inAppPath(input) {
|
|
|
14917
14992
|
return { kind: "in_app", state: "works", detail: "a test cancellation completed online" };
|
|
14918
14993
|
}
|
|
14919
14994
|
if (hostedStep2) return void 0;
|
|
14920
|
-
const
|
|
14995
|
+
const onHosted = (flow.steps ?? []).some((s2) => s2.source !== "snippet" && isHostedCancelUrl(s2.url));
|
|
14996
|
+
const kinds = (flow.blockers ?? []).map(classifyBlocker).filter((k) => !(onHosted && k === "contact"));
|
|
14921
14997
|
if (kinds.some((k) => k === "contact" || k === "not_found" || k === "obstacle")) {
|
|
14922
14998
|
return { kind: "in_app", state: "broken", detail: `the test cancellation did not complete (${flow.blockers.join("; ")})` };
|
|
14923
14999
|
}
|
|
@@ -14994,7 +15070,7 @@ function offlineOnlyCancellation(input) {
|
|
|
14994
15070
|
const hosted = input.ctx.hostedCancel;
|
|
14995
15071
|
const helpHits = [];
|
|
14996
15072
|
for (const c of captures) {
|
|
14997
|
-
if (isFetchedDocument(c) || c.pageProblem || c.pageKind === "checkout") continue;
|
|
15073
|
+
if (isFetchedDocument(c) || c.pageProblem || c.pageKind === "checkout" || isHostedCancelUrl(c.url)) continue;
|
|
14998
15074
|
const sentence = contactOnlySentence(c.text);
|
|
14999
15075
|
if (!sentence) continue;
|
|
15000
15076
|
if (ownOnlineCancelControls(c, hosted).length) continue;
|
|
@@ -15005,7 +15081,7 @@ function offlineOnlyCancellation(input) {
|
|
|
15005
15081
|
}
|
|
15006
15082
|
return { where: `${c.pageKind === "account" ? "account page" : c.pageKind === "cancel" ? "cancel page" : "page"} ${c.url.split("#")[0]}`, quote: sentence, capture: c };
|
|
15007
15083
|
}
|
|
15008
|
-
const contactOnlyUrls = new Set(captures.filter((c) => !isFetchedDocument(c) && contactOnlySentence(c.text)).map((c) => stripUrl(c.url)));
|
|
15084
|
+
const contactOnlyUrls = new Set(captures.filter((c) => !isFetchedDocument(c) && !isHostedCancelUrl(c.url) && contactOnlySentence(c.text)).map((c) => stripUrl(c.url)));
|
|
15009
15085
|
const onlineControl = captures.some(
|
|
15010
15086
|
(c) => !isFetchedDocument(c) && ownOnlineCancelControls(c, hosted).some((el) => !contactOnlyUrls.has(stripUrl(hrefOf(el) ?? "", c.url)))
|
|
15011
15087
|
);
|
|
@@ -16372,6 +16448,73 @@ var init_intro_terms = __esm({
|
|
|
16372
16448
|
}
|
|
16373
16449
|
});
|
|
16374
16450
|
|
|
16451
|
+
// ../rules/src/checks/charge-claims.ts
|
|
16452
|
+
function sentenceSpan(text, index) {
|
|
16453
|
+
const dot = text.lastIndexOf(". ", index);
|
|
16454
|
+
const start = Math.max(dot >= 0 ? dot + 2 : 0, text.lastIndexOf("\n", index) + 1);
|
|
16455
|
+
const ends = [text.indexOf(". ", index), text.indexOf("\n", index)].filter((n) => n >= 0);
|
|
16456
|
+
return { start: Math.min(start, index), end: ends.length ? Math.min(...ends) + 1 : text.length };
|
|
16457
|
+
}
|
|
16458
|
+
function misstatedRecurringCharge(text, prices, plans, others = []) {
|
|
16459
|
+
const usable = plans.filter((p) => p.amount > 0 && p.interval);
|
|
16460
|
+
if (!usable.length) return void 0;
|
|
16461
|
+
const known = [...usable.map((p) => p.amount), ...others].filter((n) => n > 0);
|
|
16462
|
+
const same = (a, b) => Math.abs(a - b) < 5e-3;
|
|
16463
|
+
const statesPlanPrice = prices.some((r) => r.money.amount !== null && known.some((k) => same(k, r.money.amount)));
|
|
16464
|
+
if (!statesPlanPrice) return void 0;
|
|
16465
|
+
return prices.find((r) => {
|
|
16466
|
+
const amount = r.money.amount;
|
|
16467
|
+
const unit2 = r.frequency.unit;
|
|
16468
|
+
if (amount === null || !(amount > 0) || !unit2) return false;
|
|
16469
|
+
if (known.some((k) => same(k, amount))) return false;
|
|
16470
|
+
const cur = (r.money.currency || "").toLowerCase();
|
|
16471
|
+
const plansHere = usable.filter((p) => !cur || !p.currency || p.currency.toLowerCase() === cur);
|
|
16472
|
+
if (!plansHere.some((p) => frequencyMatchesPlan(unit2, r.frequency.count, p))) return false;
|
|
16473
|
+
if (plansHere.some((p) => {
|
|
16474
|
+
const k = amount / p.amount;
|
|
16475
|
+
return k >= 2 && Math.abs(k - Math.round(k)) < 1e-3;
|
|
16476
|
+
})) return false;
|
|
16477
|
+
if (isPerPeriodEquivalent(r, plansHere)) return false;
|
|
16478
|
+
const at = r.money.index;
|
|
16479
|
+
if (!(at >= 0) || text.slice(at, at + r.money.text.length) !== r.money.text) return false;
|
|
16480
|
+
{
|
|
16481
|
+
const { start, end } = sentenceSpan(text, at);
|
|
16482
|
+
const sentence = text.slice(start, end);
|
|
16483
|
+
const around = text.slice(Math.max(start, at - 24), Math.min(end, at + r.money.text.length + 40));
|
|
16484
|
+
if (NOT_THE_CHARGE_AROUND_RE.test(around) || NOT_A_PRICE_AROUND_RE.test(around)) return false;
|
|
16485
|
+
if (INTRO_CUE_RE.test(sentence) || INTRO_SENTENCE_RE.test(sentence)) return false;
|
|
16486
|
+
const before = text.slice(Math.max(start, at - 32), at);
|
|
16487
|
+
const adjacent = prices.some((o) => {
|
|
16488
|
+
if (o === r || o.money.amount === null || o.money.index < 0 || !known.some((k) => same(k, o.money.amount))) return false;
|
|
16489
|
+
const [a, b] = o.money.index < at ? [o.money, r.money] : [r.money, o.money];
|
|
16490
|
+
return /^[\s()[\]/,:–—-]*$/.test(text.slice(a.index + a.text.length, b.index));
|
|
16491
|
+
});
|
|
16492
|
+
if (!adjacent && !CHARGE_CUE_BEFORE_RE.test(before)) return false;
|
|
16493
|
+
}
|
|
16494
|
+
return true;
|
|
16495
|
+
});
|
|
16496
|
+
}
|
|
16497
|
+
function isPerPeriodEquivalent(r, plans) {
|
|
16498
|
+
const amount = r.money.amount;
|
|
16499
|
+
const unit2 = r.frequency.unit;
|
|
16500
|
+
if (amount === null || !(amount > 0) || !unit2) return false;
|
|
16501
|
+
const cur = (r.money.currency || "").toLowerCase();
|
|
16502
|
+
return plans.some(
|
|
16503
|
+
(p) => p.amount > 0 && (!cur || !p.currency || p.currency.toLowerCase() === cur) && !frequencyMatchesPlan(unit2, r.frequency.count, p) && Math.abs(amount * periodDays(p.interval, p.intervalCount) / periodDays(unit2, r.frequency.count) - p.amount) <= Math.max(0.02 * p.amount, 0.05)
|
|
16504
|
+
);
|
|
16505
|
+
}
|
|
16506
|
+
var NOT_THE_CHARGE_AROUND_RE, INTRO_SENTENCE_RE, CHARGE_CUE_BEFORE_RE;
|
|
16507
|
+
var init_charge_claims = __esm({
|
|
16508
|
+
"../rules/src/checks/charge-claims.ts"() {
|
|
16509
|
+
"use strict";
|
|
16510
|
+
init_plan_terms();
|
|
16511
|
+
init_intro_terms();
|
|
16512
|
+
NOT_THE_CHARGE_AROUND_RE = /\b(?:off|save[sd]?|saving|savings|value[sd]?|worth|reward|bonus|max(?:imum)?|up\s+to|prorat\w*|earlier|previous(?:ly)?|used\s+to|last\s+(?:year|month|week|period|time)|from|was|were|previously|formerly|regular(?:ly)?|normally|usually|instead\s+of|compared|vs\.?|versus|old|reg\.|retail|list\s+price|add-?ons?|extra|additional|per\s+(?:seat|user|member|license|licence|unit))\b/i;
|
|
16513
|
+
INTRO_SENTENCE_RE = /\btrial\b|\bfree\b|\buntil\b[^.]*\bthen\b|\bfor\s+(?:the\s+|your\s+)?first\b|\bafter\s+(?:the\s+)?(?:first|trial|intro)/i;
|
|
16514
|
+
CHARGE_CUE_BEFORE_RE = /\b(?:charg\w*|bill(?:s|ed|ing)?|pay(?:s|ing|ment)?|renew(?:s|al|ing)?|costs?|total|due|amount|price)\b[^$€£\d]*$/i;
|
|
16515
|
+
}
|
|
16516
|
+
});
|
|
16517
|
+
|
|
16375
16518
|
// ../rules/src/checks/notice-content.ts
|
|
16376
16519
|
function offlineOnlyCancelIn(text, cancelMethods) {
|
|
16377
16520
|
const offline = contactOnlySentence(text);
|
|
@@ -16410,11 +16553,21 @@ function noticeContent(e, opts = {}) {
|
|
|
16410
16553
|
];
|
|
16411
16554
|
const amount = a.recurringPrices[0]?.money ?? positiveAmounts(a)[0];
|
|
16412
16555
|
const stated = positiveAmounts(a).filter((m) => m.amount !== null);
|
|
16556
|
+
const toMajor = (p, minor) => minor / 10 ** currencyDigitsOf(p.currency);
|
|
16557
|
+
const chargePlans = plans.map((p) => ({ amount: toMajor(p, p.amount), currency: p.currency, interval: p.interval, intervalCount: p.intervalCount }));
|
|
16558
|
+
const misstated = plans.length ? misstatedRecurringCharge(
|
|
16559
|
+
a.text,
|
|
16560
|
+
a.recurringPrices,
|
|
16561
|
+
chargePlans,
|
|
16562
|
+
[...expected, ...plans.flatMap((p) => typeof p.introAmount === "number" && p.introAmount > 0 ? [toMajor(p, p.introAmount)] : [])]
|
|
16563
|
+
) : void 0;
|
|
16413
16564
|
if (amount && expected.length && stated.length && !stated.some((m) => expected.some((x) => Math.abs(x - m.amount) < 5e-3))) wrong.amount = quote(amount);
|
|
16565
|
+
else if (misstated) wrong.amount = quote(misstated.money);
|
|
16414
16566
|
else if (amount) quotes.amount = quote(amount);
|
|
16415
16567
|
const freq = a.recurringPrices[0]?.frequency ?? a.frequencies[0];
|
|
16416
16568
|
const freqs = [...a.recurringPrices.map((r) => r.frequency), ...a.frequencies].filter((f) => f.unit);
|
|
16417
|
-
const
|
|
16569
|
+
const planPriceStated = stated.some((m) => chargePlans.some((p) => Math.abs(p.amount - m.amount) < 5e-3));
|
|
16570
|
+
const charged = a.recurringPrices.filter((r) => !!r.frequency?.unit && !(planPriceStated && isPerPeriodEquivalent(r, chargePlans))).map((r) => r.frequency);
|
|
16418
16571
|
const misses = (f) => !plans.some((p) => frequencyMatchesPlan(f.unit, f.count, p));
|
|
16419
16572
|
const wrongCharged = plans.length ? charged.find(misses) : void 0;
|
|
16420
16573
|
if (wrongCharged) wrong.frequency = quote(wrongCharged);
|
|
@@ -16481,6 +16634,7 @@ var init_notice_content = __esm({
|
|
|
16481
16634
|
init_context();
|
|
16482
16635
|
init_intro_terms();
|
|
16483
16636
|
init_result();
|
|
16637
|
+
init_charge_claims();
|
|
16484
16638
|
ELEMENT_LABELS = {
|
|
16485
16639
|
amount: "the amount charged",
|
|
16486
16640
|
frequency: "how often the subscription is charged",
|
|
@@ -16851,6 +17005,17 @@ function cancelBeforeTrialMatch(text) {
|
|
|
16851
17005
|
if (byDate && TRIAL_ENDS_DATE_RE.test(text)) return byDate[0];
|
|
16852
17006
|
return null;
|
|
16853
17007
|
}
|
|
17008
|
+
function statedTrialMismatch(a, p, offers) {
|
|
17009
|
+
const stated = a.trialLengths.filter((t) => typeof t.days === "number" && t.days > 0);
|
|
17010
|
+
if (!stated.length) return void 0;
|
|
17011
|
+
const own = p?.offer && p.offer.trialDays > 0 ? [p.offer] : p?.offer ? [] : offers.filter((o) => o.trialDays > 0);
|
|
17012
|
+
if (!own.length) return void 0;
|
|
17013
|
+
const fits = (days, trial) => days === trial || days % 30 === 0 && Math.abs(days - trial) <= Math.max(1, days / 30) * 2;
|
|
17014
|
+
if (stated.some((t) => own.some((o) => fits(t.days, o.trialDays)))) return void 0;
|
|
17015
|
+
const lengths = [...new Set(own.map((o) => o.trialDays))].sort((x, y) => x - y);
|
|
17016
|
+
const plan2 = p?.offer ? `${offerLabel(p.offer)} has a ${p.offer.trialDays}-day free trial` : `the plan${lengths.length > 1 ? "s give" : " gives"} a ${lengths.join("- or ")}-day free trial`;
|
|
17017
|
+
return { stated: `\u201C${stated[0].text}\u201D`, plan: plan2 };
|
|
17018
|
+
}
|
|
16854
17019
|
var ACK_MAX_DELAY_HOURS, NO_ACK_EMAIL_MESSAGE, ackCache, SIGNUP_LINK_TOLERANCE_MS, sent, terms, cancellationInstructions, CANCELLING_RE, CANCEL_BEFORE_TRIAL_RE, MONTH, DATE, CANCEL_BEFORE_NO_CHARGE_RE, CANCEL_BEFORE_DATE_RE, TRIAL_ENDS_DATE_RE, trialTerms, CONFIRMATION_CHECKS;
|
|
16855
17020
|
var init_confirmation = __esm({
|
|
16856
17021
|
"../rules/src/checks/confirmation.ts"() {
|
|
@@ -17160,6 +17325,15 @@ ${v.pageText}`);
|
|
|
17160
17325
|
);
|
|
17161
17326
|
const units = trialDocs.map((doc) => {
|
|
17162
17327
|
const a = doc.analysis;
|
|
17328
|
+
const wrongTrial = statedTrialMismatch(a, ackPlan(doc.email, input, offers), offers);
|
|
17329
|
+
if (wrongTrial) {
|
|
17330
|
+
return docUnit(doc, {
|
|
17331
|
+
status: "fail",
|
|
17332
|
+
detail: `says ${wrongTrial.stated}, but ${wrongTrial.plan}: state the trial the subscriber really gets`,
|
|
17333
|
+
confidence: 0.8,
|
|
17334
|
+
meta: { trialLengthMismatch: true }
|
|
17335
|
+
});
|
|
17336
|
+
}
|
|
17163
17337
|
const method = a.cancelMethod[0];
|
|
17164
17338
|
const timing = a.trialLengths[0] ?? a.postTrialCharge[0];
|
|
17165
17339
|
const beforeMatch = cancelBeforeTrialMatch(a.text);
|
|
@@ -19199,6 +19373,7 @@ var init_signup = __esm({
|
|
|
19199
19373
|
init_offer_view();
|
|
19200
19374
|
init_plan_terms();
|
|
19201
19375
|
init_cancellation();
|
|
19376
|
+
init_charge_claims();
|
|
19202
19377
|
init_intro_terms();
|
|
19203
19378
|
init_intro_terms();
|
|
19204
19379
|
init_result();
|
|
@@ -19364,6 +19539,29 @@ var init_signup = __esm({
|
|
|
19364
19539
|
const pl = view.plan?.plan;
|
|
19365
19540
|
return !!pl && hasIntro(pl) && r.money.amount !== null && Math.abs(r.money.amount - pl.introAmount / 10 ** currencyDigits(pl.currency || "usd")) < 5e-3;
|
|
19366
19541
|
};
|
|
19542
|
+
if (!tied && plans.length) {
|
|
19543
|
+
const sitePrices = withAccountPrices(referencePlans(input.ctx), input.ctx).map((p) => p.amount / 10 ** currencyDigits((p.currency || "usd").toLowerCase()));
|
|
19544
|
+
const intros = [...plans.map((p) => p.plan), ...view.plan ? [view.plan.plan] : []].flatMap(
|
|
19545
|
+
(pl) => hasIntro(pl) ? [pl.introAmount / 10 ** currencyDigits((pl.currency || "usd").toLowerCase())] : []
|
|
19546
|
+
);
|
|
19547
|
+
const misstated = misstatedRecurringCharge(
|
|
19548
|
+
near2.text,
|
|
19549
|
+
parsed,
|
|
19550
|
+
plans.map((p) => ({ amount: p.amount, currency: p.currency, interval: p.plan.interval, intervalCount: p.plan.intervalCount })),
|
|
19551
|
+
[...sitePrices, ...intros]
|
|
19552
|
+
);
|
|
19553
|
+
if (misstated && !NON_CHARGE_AMOUNT_RE.test(quote(misstated) ?? "") && !INTRO_PRICE_RE.test(quote(misstated) ?? "")) {
|
|
19554
|
+
const said = misstated.money.text;
|
|
19555
|
+
const own = quote(parsed.find(recurringMatches) ?? rp) ?? "";
|
|
19556
|
+
const firm = decisive && input.ctx.facts?.partialPlans !== true && !otherPlanCard(view, input);
|
|
19557
|
+
return unit(
|
|
19558
|
+
firm ? "fail" : "needs_review",
|
|
19559
|
+
`the terms state \u201C${own}\u201D but also \u201C${said}\u201D for the same billing period, and ${plans.length === 1 ? "the plan charges" : "the plans charge"} ${plans.slice(0, 4).map((p) => p.label).join(", ")} \u2014 state only the recurring charge subscribers will actually pay`,
|
|
19560
|
+
firm ? 0.85 : 0.6,
|
|
19561
|
+
{ quote: said, meta: { ...meta(misstated.money.amount, misstated.money.currency), priceMismatch: true, secondCharge: true } }
|
|
19562
|
+
);
|
|
19563
|
+
}
|
|
19564
|
+
}
|
|
19367
19565
|
const foreign = tied && plans.length ? parsed.find((r) => r.frequency.unit && !recurringMatches(r) && !equivalentOfPlan(r) && !ownIntro(r) && !INTRO_PRICE_RE.test(quote(r) ?? "")) : void 0;
|
|
19368
19566
|
if (foreign) {
|
|
19369
19567
|
const own = parsed.find(recurringMatches) ?? rp;
|
|
@@ -22221,7 +22419,7 @@ function classifyRaw(input) {
|
|
|
22221
22419
|
}
|
|
22222
22420
|
}
|
|
22223
22421
|
function fixFromFail(c) {
|
|
22224
|
-
return withPlanTerms(contentFixLine(String(c.checkId), c.data) ?? contentFixFromText(String(c.checkId), c.detail) ?? introFixFromText(c) ?? planFrequencyFix(c) ?? FAIL_FIX[String(c.checkId)] ?? "Apply the change in the task for this check.", c.data);
|
|
22422
|
+
return withPlanTerms(contentFixLine(String(c.checkId), c.data) ?? contentFixFromText(String(c.checkId), c.detail) ?? introFixFromText(c) ?? trialLengthFixFromText(c) ?? planFrequencyFix(c) ?? FAIL_FIX[String(c.checkId)] ?? "Apply the change in the task for this check.", c.data);
|
|
22225
22423
|
}
|
|
22226
22424
|
function withPlanTerms(fix, data) {
|
|
22227
22425
|
if (!/<(?:price|interval)>/.test(fix)) return fix;
|
|
@@ -22241,6 +22439,11 @@ function introFixFromText(c) {
|
|
|
22241
22439
|
const m = /introductory price of (\S+?)(?: \(\d+% off\))? for (the first [^—,]+?)\s*(?:,|—)/i.exec(c.detail);
|
|
22242
22440
|
return m ? `State the introductory price (${m[1]} for ${m[2]}) and when it ends, then the regular price from the next charge, in the renewal line.` : "State the introductory price and how long it lasts, then the regular price from the next charge, in the renewal line.";
|
|
22243
22441
|
}
|
|
22442
|
+
function trialLengthFixFromText(c) {
|
|
22443
|
+
if (c.checkId !== "confirmation.email.trial_terms") return void 0;
|
|
22444
|
+
const m = /says “([^”]+)”, but .*?\b(?:has|gives?) a ([\d -]+?or [\d]+|\d+)-day free trial/.exec(c.detail);
|
|
22445
|
+
return m ? `The acknowledgment says \u201C${m[1]}\u201D, but the plan has a ${m[2]}-day free trial: state the trial the subscriber really gets, and how to cancel before it ends.` : void 0;
|
|
22446
|
+
}
|
|
22244
22447
|
function planFrequencyFix(c) {
|
|
22245
22448
|
if (c.checkId !== "signup.disclosure.frequency") return void 0;
|
|
22246
22449
|
const every = /\bplans? (?:is|are) billed (every [a-z0-9 ]+?)(?= \/ | —|[.;,]|$)/i.exec(c.detail)?.[1];
|
|
@@ -22326,7 +22529,7 @@ function classifyFindings(findings) {
|
|
|
22326
22529
|
info: classify({ checkId: f.checkId, status: f.status, detail: f.detail, missing: f.evidence?.missing, data: f.data, origin: "evaluation", siblings })
|
|
22327
22530
|
}));
|
|
22328
22531
|
}
|
|
22329
|
-
var STATUS_LABELS, CHECK_TIERS, LIVE_CHECKS, has, isStripeInput, HOSTED_BEFORE_LINE_NOTE, HOSTED_CHECKOUT_NOTE, family, isEmailCheck, ask, PLAN_FACTS_FIX, STRIPE_CANCEL_UNREAD_FIX, BILLING_CONTRADICTS_FIX, VERIFIED_FROM_STRIPE_NOTE, VERIFIED_FROM_HOSTED_NOTE, HOSTED_UNLINKED_FIX, NOT_CHECKED_CANCEL_FIX, EMAIL_EXISTS, RULES, STATUS_RULES, NY_EMAIL_ONLY_LABEL, NY_EMAIL_ONLY_NOTE, STRIPE_HOSTED_UNITS, PLATFORM_NAMES2, PORTAL_REQUIRED_FIX, PORTAL_IN_APP_BROKEN_FIX, PORTAL_OFFLINE_FIX, PAYMENT_LINK_PORTAL_FIX, PAYMENT_LINK_OFFLINE_FIX, FAIL_FIX, CHECKS_PASS_UNREAD_LABEL, STATUS_RANK;
|
|
22532
|
+
var STATUS_LABELS, CHECK_TIERS, LIVE_CHECKS, has, isStripeInput, HOSTED_BEFORE_LINE_NOTE, HOSTED_CHECKOUT_NOTE, family, isEmailCheck, ask, PLAN_FACTS_FIX, STRIPE_CANCEL_UNREAD_FIX, BILLING_CONTRADICTS_FIX, VERIFIED_FROM_STRIPE_NOTE, VERIFIED_FROM_HOSTED_NOTE, HOSTED_UNLINKED_FIX, NOT_CHECKED_CANCEL_FIX, EMAIL_EXISTS, RULES, STATUS_RULES, NY_EMAIL_ONLY_LABEL, NY_EMAIL_ONLY_NOTE, STRIPE_HOSTED_UNITS, PLATFORM_NAMES2, PORTAL_REQUIRED_FIX, PORTAL_IN_APP_BROKEN_FIX, PORTAL_OFFLINE_FIX, PAYMENT_LINK_PORTAL_FIX, PAYMENT_LINK_OFFLINE_FIX, RENEWAL_LINE_EXAMPLE, FAIL_FIX, CHECKS_PASS_UNREAD_LABEL, STATUS_RANK;
|
|
22330
22533
|
var init_status = __esm({
|
|
22331
22534
|
"../rules/src/status.ts"() {
|
|
22332
22535
|
"use strict";
|
|
@@ -22522,7 +22725,7 @@ var init_status = __esm({
|
|
|
22522
22725
|
},
|
|
22523
22726
|
{ code: "receipt_without_terms", category: "content", when: has(/only a receipt without subscription terms/), build: () => ({ status: "needs_fix", fix: "Send an acknowledgment that restates the renewal terms (price, frequency, renews until cancelled) and how to cancel; a plain receipt is not enough." }) },
|
|
22524
22727
|
// Audit P2: Stripe wording (custom_text) only for Stripe inputs; any other checkout gets the same line, platform-neutral.
|
|
22525
|
-
{ code: "processor_default_line", category: "content", when: has(/payment processor.s default line|authorization line is not the renewal terms|no automatic-renewal statement was found/), build: (c) => ({ status: "needs_fix", fix: withPlanTerms(`Add your own renewal line next to the pay button${isStripeInput(c) ? " (Stripe custom_text.submit)" : ""}: "
|
|
22728
|
+
{ code: "processor_default_line", category: "content", when: has(/payment processor.s default line|authorization line is not the renewal terms|no automatic-renewal statement was found/), build: (c) => ({ status: "needs_fix", fix: withPlanTerms(`Add your own renewal line next to the pay button${isStripeInput(c) ? " (Stripe custom_text.submit)" : ""}: "${RENEWAL_LINE_EXAMPLE} Cancel anytime with your cancel link."`, c.data) }) },
|
|
22526
22729
|
{ code: "renewal_without_until_cancel", category: "content", when: has(/but not that it continues until the customer cancels/), build: () => ({ status: "needs_fix", fix: 'Add "until you cancel" to the renewal line.' }) },
|
|
22527
22730
|
{ code: "terms_hidden", category: "content", when: has(/collapsed <details>|\(nearly\) the colour of its background|nearly the colour of the background|hidden by a stylesheet|sr-only/), build: () => ({ status: "needs_fix", fix: "Show the renewal line as plain visible text next to the button (not collapsed, not faded into the background)." }) },
|
|
22528
22731
|
// Pilot round 11 (change-guard): an intro / discounted price the Stripe session does not apply.
|
|
@@ -22985,8 +23188,9 @@ var init_status = __esm({
|
|
|
22985
23188
|
PORTAL_OFFLINE_FIX = 'Turn on "Cancel subscriptions" in the Stripe customer portal and link it from your account page, then replace the "email / call us to cancel" line with "Cancel anytime online in your account".';
|
|
22986
23189
|
PAYMENT_LINK_PORTAL_FIX = `Turn on "Cancel subscriptions" in the Stripe customer portal (Stripe Dashboard \u2192 Settings \u2192 Billing \u2192 Customer portal), copy its billing.stripe.com login link and add it to your site's footer as a "Cancel subscription" link.`;
|
|
22987
23190
|
PAYMENT_LINK_OFFLINE_FIX = `${PAYMENT_LINK_PORTAL_FIX} Then replace the "email / call us to cancel" line with "Cancel anytime online" and that link.`;
|
|
23191
|
+
RENEWAL_LINE_EXAMPLE = "<price> every <interval> (plus applicable taxes). Renews automatically until you cancel.";
|
|
22988
23192
|
FAIL_FIX = {
|
|
22989
|
-
"signup.disclosure.autorenew_language":
|
|
23193
|
+
"signup.disclosure.autorenew_language": `Add one line under the price, before the card and email fields: "${RENEWAL_LINE_EXAMPLE}"`,
|
|
22990
23194
|
"signup.disclosure.price": "State the recurring price in the renewal line (the amount actually charged each time).",
|
|
22991
23195
|
"signup.disclosure.frequency": `State how often it is charged, in the plan's own interval ("every month", "every year").`,
|
|
22992
23196
|
"signup.disclosure.cancellation_policy": 'Add "Cancel anytime online" to the renewal line, with your cancel link.',
|
|
@@ -23835,6 +24039,10 @@ function fixTextsFor(t, style2) {
|
|
|
23835
24039
|
if (style2 === "full") return fixTexts(t);
|
|
23836
24040
|
return { disclosure: compactDisclosureText(t), consentLabel: COMPACT_CONSENT_LABEL, cta: compactCtaLabel(t) };
|
|
23837
24041
|
}
|
|
24042
|
+
function stripeCompactConsent(t) {
|
|
24043
|
+
const trial = t.trialDays && t.trialDays > 0 ? ` after the ${t.trialDays}-day free trial` : "";
|
|
24044
|
+
return `I agree that my subscription renews automatically at ${formatMoney(t.amount, t.currency)} every ${intervalPhrase(t.interval, t.intervalCount)}${trial} until I cancel.`;
|
|
24045
|
+
}
|
|
23838
24046
|
var COMPACT_CONSENT_LABEL;
|
|
23839
24047
|
var init_compact = __esm({
|
|
23840
24048
|
"../rules/src/fixes/compact.ts"() {
|
|
@@ -23851,9 +24059,10 @@ function shortDisclosureText(t) {
|
|
|
23851
24059
|
const trial = !!t.trialDays && t.trialDays > 0;
|
|
23852
24060
|
const intro = introPhrase(t);
|
|
23853
24061
|
const cancel = `Cancel anytime online at ${t.cancelUrl}`;
|
|
23854
|
-
|
|
23855
|
-
if (
|
|
23856
|
-
|
|
24062
|
+
const tax = " (plus applicable taxes)";
|
|
24063
|
+
if (trial) return `${t.trialDays} days free, then ${intro ?? `${price3} every ${every}`}${tax}. Renews automatically until you cancel. ${cancel} before the trial ends to avoid being charged.`;
|
|
24064
|
+
if (intro) return `${intro[0].toUpperCase()}${intro.slice(1)}${tax}. Renews automatically until you cancel. ${cancel}.`;
|
|
24065
|
+
return `Renews automatically every ${every} at ${price3}${tax} until you cancel. ${cancel}.`;
|
|
23857
24066
|
}
|
|
23858
24067
|
var init_short = __esm({
|
|
23859
24068
|
"../rules/src/fixes/short.ts"() {
|
|
@@ -24565,6 +24774,45 @@ var init_stripe = __esm({
|
|
|
24565
24774
|
}
|
|
24566
24775
|
});
|
|
24567
24776
|
|
|
24777
|
+
// ../rules/src/preflight/price-hint.ts
|
|
24778
|
+
function intervalHintOf(ref) {
|
|
24779
|
+
if (typeof ref !== "string" || !ref.trim() || ref.length > 200) return void 0;
|
|
24780
|
+
const s2 = ref.trim();
|
|
24781
|
+
if (GENERATED.test(s2)) return void 0;
|
|
24782
|
+
const tokens = s2.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
24783
|
+
const hits = /* @__PURE__ */ new Set();
|
|
24784
|
+
for (const t of tokens) for (const [k, re] of WORDS2) if (re.test(t)) hits.add(k);
|
|
24785
|
+
return hits.size === 1 ? [...hits][0] : void 0;
|
|
24786
|
+
}
|
|
24787
|
+
function priceIntervalConflict(ids, plan2) {
|
|
24788
|
+
const interval = plan2?.interval;
|
|
24789
|
+
if (!interval || !ids.length) return void 0;
|
|
24790
|
+
const count = Number(plan2?.interval_count ?? 1) || 1;
|
|
24791
|
+
for (const id of ids) {
|
|
24792
|
+
const hint = intervalHintOf(id);
|
|
24793
|
+
if (!hint) continue;
|
|
24794
|
+
const planUnit = interval === "month" && count === 12 ? "year" : interval;
|
|
24795
|
+
if (hint !== planUnit) {
|
|
24796
|
+
const name = plan2?.plan_name ?? plan2?.name;
|
|
24797
|
+
return `line_items use ${id}, whose name says it bills every ${hint}, but the plan terms given${name ? ` (plan "${name}")` : ""} bill every ${count > 1 ? `${count} ${interval}s` : interval}`;
|
|
24798
|
+
}
|
|
24799
|
+
}
|
|
24800
|
+
return void 0;
|
|
24801
|
+
}
|
|
24802
|
+
var WORDS2, GENERATED;
|
|
24803
|
+
var init_price_hint = __esm({
|
|
24804
|
+
"../rules/src/preflight/price-hint.ts"() {
|
|
24805
|
+
"use strict";
|
|
24806
|
+
WORDS2 = [
|
|
24807
|
+
["year", /^(?:annual|annually|yearly|year|years|yr|yrly|12mo|12m|1y|1yr)$/],
|
|
24808
|
+
["month", /^(?:monthly|month|months|mo|mth|1m|1mo)$/],
|
|
24809
|
+
["week", /^(?:weekly|week|weeks|wk)$/],
|
|
24810
|
+
["day", /^(?:daily|day|days)$/]
|
|
24811
|
+
];
|
|
24812
|
+
GENERATED = /^(?:price|plan|prod)_1[A-Za-z0-9]{12,}$/;
|
|
24813
|
+
}
|
|
24814
|
+
});
|
|
24815
|
+
|
|
24568
24816
|
// ../rules/src/preflight/stripe.ts
|
|
24569
24817
|
function clone(v) {
|
|
24570
24818
|
return JSON.parse(JSON.stringify(v));
|
|
@@ -24957,10 +25205,6 @@ function correct(params, shape, checks, forLink, style2 = "full") {
|
|
|
24957
25205
|
}
|
|
24958
25206
|
return { patch, corrected: out, texts, needText, needConsent };
|
|
24959
25207
|
}
|
|
24960
|
-
function stripeCompactConsent(t) {
|
|
24961
|
-
const trial = t.trialDays && t.trialDays > 0 ? ` after the ${t.trialDays}-day free trial` : "";
|
|
24962
|
-
return `I agree that my subscription renews automatically at ${formatMoney(t.amount, t.currency)} every ${intervalPhrase(t.interval, t.intervalCount)}${trial} until I cancel.`;
|
|
24963
|
-
}
|
|
24964
25208
|
async function bestCorrection(params, shape, checks, forLink, judgeAfter) {
|
|
24965
25209
|
const none = () => ({ fix: correct(params, shape, [], forLink), after: checks });
|
|
24966
25210
|
const first2 = correct(params, shape, checks, forLink, "compact");
|
|
@@ -25041,6 +25285,12 @@ function withResolvedPrices(input, knownPrices, amountSource = "merchant.price")
|
|
|
25041
25285
|
warnings: [{ code: "price_amount_unverified", message: `line_items use ${ids[0]}, which Checkbox could not read (no connected Stripe account has it), so the amount in the checks and in the corrected custom_text comes from ${amountSource} and is not verified against the Price. Connect Stripe on the site, or register the params with createStripeCheckoutConfig on a site whose Stripe account has this Price.`, param: "params.line_items" }]
|
|
25042
25286
|
};
|
|
25043
25287
|
}
|
|
25288
|
+
function pricePairingConflict(input, resolved) {
|
|
25289
|
+
if (!resolved.warnings.some((w) => w.code === "price_amount_unverified")) return void 0;
|
|
25290
|
+
const conflict = priceIntervalConflict(lineItemPriceIds(input.params), input.merchant);
|
|
25291
|
+
if (!conflict) return void 0;
|
|
25292
|
+
return { code: "price_plan_conflict", message: `${conflict}. Checkbox offers no corrected params for this pairing: they would state the other plan's price. Check which plan this checkout sells and pass that plan's terms (in checkbox.json: the entry's "plan"), or connect Stripe so Checkbox reads the Price.`, param: "params.line_items" };
|
|
25293
|
+
}
|
|
25044
25294
|
async function checkStripeCheckoutSession(input0, opts = {}) {
|
|
25045
25295
|
const { jurisdictions, asOf, now } = common(input0, opts.now);
|
|
25046
25296
|
const resolvedPrices = withResolvedPrices(input0, opts.knownPrices, opts.amountSource);
|
|
@@ -25056,7 +25306,8 @@ async function checkStripeCheckoutSession(input0, opts = {}) {
|
|
|
25056
25306
|
const shape = readParams(params, input.merchant, "session", asOf);
|
|
25057
25307
|
const url = "https://checkout.stripe.com/c/pay/cs_test_preflight";
|
|
25058
25308
|
const checks = await judge2(shape, url, jurisdictions, asOf, now, "preflight-stripe-before");
|
|
25059
|
-
const
|
|
25309
|
+
const pairing = pricePairingConflict(input0, resolvedPrices);
|
|
25310
|
+
const { fix, after } = pairing ? { fix: correct(params, shape, [], false), after: checks } : await bestCorrection(
|
|
25060
25311
|
params,
|
|
25061
25312
|
shape,
|
|
25062
25313
|
checks,
|
|
@@ -25071,12 +25322,12 @@ async function checkStripeCheckoutSession(input0, opts = {}) {
|
|
|
25071
25322
|
verification: { checks: d.checks, passes: d.passes, remaining_checks: d.remaining, note: HOSTED_STYLING_NOTE },
|
|
25072
25323
|
placeholders
|
|
25073
25324
|
};
|
|
25074
|
-
const warnings = [...resolvedPrices.warnings, ...shape.warnings];
|
|
25325
|
+
const warnings = [...resolvedPrices.warnings, ...pairing ? [pairing] : [], ...shape.warnings];
|
|
25075
25326
|
if (fix.needConsent) warnings.push({ code: "stripe_tos_url_required", message: 'consent_collection.terms_of_service = "required" needs a Terms of Service URL in Stripe Dashboard \u2192 Settings \u2192 Public details, or session creation fails.' });
|
|
25076
25327
|
const channel2 = noticeChannelField(params);
|
|
25077
25328
|
if (channel2) warnings.push(channel2);
|
|
25078
25329
|
return {
|
|
25079
|
-
...baseReport("stripe_checkout_session", checks, jurisdictions, asOf, warnings, nextAction("session", checks, correction), void 0, { cancelUrl: shape.plan?.cancelUrl }),
|
|
25330
|
+
...baseReport("stripe_checkout_session", checks, jurisdictions, asOf, warnings, pairing ? PAIRING_ACTION(pairing) : nextAction("session", checks, correction), void 0, { cancelUrl: shape.plan?.cancelUrl }),
|
|
25080
25331
|
kind: "stripe_checkout_session",
|
|
25081
25332
|
correction
|
|
25082
25333
|
};
|
|
@@ -25094,7 +25345,8 @@ async function checkStripePaymentLink(input0, opts = {}) {
|
|
|
25094
25345
|
const id = typeof params.id === "string" ? params.id : void 0;
|
|
25095
25346
|
const url = typeof params.url === "string" && /^https:\/\/buy\.stripe\.com\//.test(params.url) ? params.url : "https://buy.stripe.com/test_preflight";
|
|
25096
25347
|
const checks = await judge2(shape, url, jurisdictions, asOf, now, "preflight-link-before");
|
|
25097
|
-
const
|
|
25348
|
+
const pairing = pricePairingConflict(input0, resolvedPrices);
|
|
25349
|
+
const { fix, after } = pairing ? { fix: correct(params, shape, [], true), after: checks } : await bestCorrection(
|
|
25098
25350
|
params,
|
|
25099
25351
|
shape,
|
|
25100
25352
|
checks,
|
|
@@ -25133,11 +25385,11 @@ async function checkStripePaymentLink(input0, opts = {}) {
|
|
|
25133
25385
|
...update_params ? { update_params } : {},
|
|
25134
25386
|
needs_new_link_for_consent: needsNew
|
|
25135
25387
|
};
|
|
25136
|
-
const warnings = [...resolvedPrices.warnings, ...shape.warnings];
|
|
25388
|
+
const warnings = [...resolvedPrices.warnings, ...pairing ? [pairing] : [], ...shape.warnings];
|
|
25137
25389
|
if (fix.needConsent) warnings.push({ code: "stripe_tos_url_required", message: 'consent_collection.terms_of_service = "required" needs a Terms of Service URL in Stripe Dashboard \u2192 Settings \u2192 Public details.' });
|
|
25138
25390
|
const extra = needsNew && !id ? " Stripe cannot add the consent checkbox to an existing link (stripe.paymentLinks.update refuses consent_collection): create a new link with correction.corrected_params, replace the old URL everywhere, then deactivate the old link." : needsNew ? ` Stripe cannot add the consent checkbox to an existing link: send correction.update_params with stripe.paymentLinks.update("${id}", \u2026) for the text now, then create a new link with correction.corrected_params, replace the old URL everywhere, then deactivate the old link.` : id && update_params ? ` For the existing link use stripe.paymentLinks.update("${id}", correction.update_params) (same URL).` : "";
|
|
25139
25391
|
return {
|
|
25140
|
-
...baseReport("stripe_payment_link", checks, jurisdictions, asOf, warnings, nextAction("link", checks, correction, extra), void 0, { cancelUrl: shape.plan?.cancelUrl }),
|
|
25392
|
+
...baseReport("stripe_payment_link", checks, jurisdictions, asOf, warnings, pairing ? PAIRING_ACTION(pairing) : nextAction("link", checks, correction, extra), void 0, { cancelUrl: shape.plan?.cancelUrl }),
|
|
25141
25393
|
kind: "stripe_payment_link",
|
|
25142
25394
|
correction
|
|
25143
25395
|
};
|
|
@@ -25216,7 +25468,7 @@ function noticeChannelField(params) {
|
|
|
25216
25468
|
param: `params.custom_fields[${i}]`
|
|
25217
25469
|
};
|
|
25218
25470
|
}
|
|
25219
|
-
var TEXT_CHECKS, CONSENT_CHECKS2, HOSTED_STYLING_NOTE, INTRO_PROMISE_RE, isObj, msg, CUSTOM_TEXT_FIELDS, STRIPE_GREY, DAY_MS3, open, STRIPE_HOSTED_STYLING_NOTE;
|
|
25471
|
+
var TEXT_CHECKS, CONSENT_CHECKS2, HOSTED_STYLING_NOTE, INTRO_PROMISE_RE, isObj, msg, CUSTOM_TEXT_FIELDS, STRIPE_GREY, DAY_MS3, open, PAIRING_ACTION, STRIPE_HOSTED_STYLING_NOTE;
|
|
25220
25472
|
var init_stripe2 = __esm({
|
|
25221
25473
|
"../rules/src/preflight/stripe.ts"() {
|
|
25222
25474
|
"use strict";
|
|
@@ -25229,6 +25481,7 @@ var init_stripe2 = __esm({
|
|
|
25229
25481
|
init_common();
|
|
25230
25482
|
init_disclosure();
|
|
25231
25483
|
init_model();
|
|
25484
|
+
init_price_hint();
|
|
25232
25485
|
TEXT_CHECKS = [
|
|
25233
25486
|
"signup.disclosure.autorenew_language",
|
|
25234
25487
|
"signup.disclosure.price",
|
|
@@ -25247,10 +25500,31 @@ var init_stripe2 = __esm({
|
|
|
25247
25500
|
STRIPE_GREY = { fontSizePx: 13, fontWeight: 400, color: "rgba(26, 26, 26, 0.7)", backgroundColor: "rgb(255, 255, 255)" };
|
|
25248
25501
|
DAY_MS3 = 864e5;
|
|
25249
25502
|
open = (c) => !!c && (c.status === "fail" || c.status === "needs_review");
|
|
25503
|
+
PAIRING_ACTION = (w) => ({ type: "human_review", message: `${w.message}` });
|
|
25250
25504
|
STRIPE_HOSTED_STYLING_NOTE = HOSTED_STYLING_NOTE;
|
|
25251
25505
|
}
|
|
25252
25506
|
});
|
|
25253
25507
|
|
|
25508
|
+
// ../rules/src/preflight/stripe-verdict.ts
|
|
25509
|
+
function stripeParamsVerdict(r) {
|
|
25510
|
+
if (r.verdict === "not_applicable") return { verdict: "not_applicable", open: [] };
|
|
25511
|
+
const open2 = r.checks.filter((c) => c.status === "fail" || c.status === "needs_review" && !(c.compliance_status && SATISFIED_COMPLIANCE.has(c.compliance_status))).map((c) => ({ check_id: c.check_id, status: c.status, title: c.title ?? c.check_id, detail: c.explanation ?? "" })).sort((a, b) => (a.status === "fail" ? 0 : 1) - (b.status === "fail" ? 0 : 1));
|
|
25512
|
+
if (r.verdict === "fail" || r.compliance_status === "needs_fix" || open2.some((c) => c.status === "fail")) return { verdict: "fail", open: open2 };
|
|
25513
|
+
const pairing = (r.warnings ?? []).find((w) => w.code === "price_plan_conflict");
|
|
25514
|
+
if (pairing) return { verdict: "needs_review", open: open2, reason: pairing.message };
|
|
25515
|
+
const coupon = (r.warnings ?? []).find((w) => w.code === "coupon_unverified");
|
|
25516
|
+
if (open2.length) return { verdict: "needs_review", open: open2, ...coupon ? { reason: coupon.message } : {} };
|
|
25517
|
+
if (coupon) return { verdict: "needs_review", open: [], reason: coupon.message };
|
|
25518
|
+
return { verdict: "pass", open: [] };
|
|
25519
|
+
}
|
|
25520
|
+
var SATISFIED_COMPLIANCE;
|
|
25521
|
+
var init_stripe_verdict = __esm({
|
|
25522
|
+
"../rules/src/preflight/stripe-verdict.ts"() {
|
|
25523
|
+
"use strict";
|
|
25524
|
+
SATISFIED_COMPLIANCE = /* @__PURE__ */ new Set(["compliant", "pending_live_check", "verified_live", "not_applicable"]);
|
|
25525
|
+
}
|
|
25526
|
+
});
|
|
25527
|
+
|
|
25254
25528
|
// ../rules/src/preflight/email.ts
|
|
25255
25529
|
function linkify(escaped) {
|
|
25256
25530
|
return escaped.replace(/https?:\/\/[^\s<]+/g, (raw) => {
|
|
@@ -25434,6 +25708,10 @@ function contextFor(kind, occ, plan2, jurisdictions, asOf, email) {
|
|
|
25434
25708
|
function longDate2(d) {
|
|
25435
25709
|
return d.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric", timeZone: "UTC" });
|
|
25436
25710
|
}
|
|
25711
|
+
function supportContactLine(contact, opts = {}) {
|
|
25712
|
+
if (contact || opts.required) return `Questions? Reply to this email or contact us at ${contact || SUPPORT_CONTACT_PLACEHOLDER}.`;
|
|
25713
|
+
return "Questions? Reply to this email.";
|
|
25714
|
+
}
|
|
25437
25715
|
function emailTemplate(kind, plan2, opts = {}) {
|
|
25438
25716
|
const brand = plan2.merchantName ?? "";
|
|
25439
25717
|
const sub = `${brand ? `${brand} ` : ""}subscription`;
|
|
@@ -25441,7 +25719,7 @@ function emailTemplate(kind, plan2, opts = {}) {
|
|
|
25441
25719
|
const every = intervalPhrase(plan2.interval, plan2.intervalCount);
|
|
25442
25720
|
const cancel = plan2.cancelUrl ?? CANCEL_URL_PLACEHOLDER;
|
|
25443
25721
|
const effect = plan2.cancelTiming === "immediately" ? "cancellation takes effect immediately" : "cancellation takes effect at the end of your current billing period";
|
|
25444
|
-
const contact = opts.supportContact
|
|
25722
|
+
const contact = supportContactLine(opts.supportContact);
|
|
25445
25723
|
const lines = (...l) => l.filter(Boolean).join("\n\n");
|
|
25446
25724
|
const intro = plan2.introAmount !== void 0 && plan2.introAmount < plan2.amount ? `${formatMoney(plan2.introAmount, plan2.currency)} for ${plan2.introPeriods && plan2.introPeriods > 1 ? `your first ${plan2.introPeriods} billing periods` : `your first ${every}`}` : "";
|
|
25447
25725
|
const introLine = intro ? `Introductory price: ${intro}, then ${money3} every ${every}.` : "";
|
|
@@ -25449,11 +25727,13 @@ function emailTemplate(kind, plan2, opts = {}) {
|
|
|
25449
25727
|
case "acknowledgment":
|
|
25450
25728
|
return {
|
|
25451
25729
|
subject: `Your ${sub} is confirmed`,
|
|
25730
|
+
// Round 31 (pilot 30 item 22): the same sentences, in the same order, as the "You send" acknowledgment template
|
|
25731
|
+
// (apps/api notices/merchant-templates.ts), so the plan, checkEmail and the due notice give one email.
|
|
25452
25732
|
text: lines(
|
|
25453
|
-
`Thanks for subscribing${brand ? ` to ${brand}` : ""}
|
|
25454
|
-
introLine,
|
|
25733
|
+
`Thanks for subscribing${brand ? ` to ${brand}` : ""}. This email confirms your subscription terms.`,
|
|
25455
25734
|
plan2.trialDays ? `Your free trial lasts ${plan2.trialDays} days and ends on {{trial_end_date}}. To avoid being charged, cancel before your trial ends at ${cancel} \u2014 it takes less than a minute. If you do nothing, your subscription starts automatically on {{trial_end_date}} and you will be charged ${money3} every ${every}.` : "",
|
|
25456
|
-
`Your ${sub} automatically renews every ${every} at ${money3} until you cancel. Your next charge is on {{next_charge_date}}.`,
|
|
25735
|
+
`Your ${sub} automatically renews every ${every} at ${money3} until you cancel. Your ${plan2.trialDays ? "first" : "next"} charge is on {{next_charge_date}}.`,
|
|
25736
|
+
introLine,
|
|
25457
25737
|
`How to cancel: go to ${cancel} \u2014 you can cancel anytime online; ${effect}.`,
|
|
25458
25738
|
contact
|
|
25459
25739
|
),
|
|
@@ -25509,7 +25789,7 @@ function emailTemplate(kind, plan2, opts = {}) {
|
|
|
25509
25789
|
`To stop future charges, cancel online anytime at ${cancel}.`,
|
|
25510
25790
|
// Pilot round 11 (invariant test): Georgia's charge notice must give the business's contact information;
|
|
25511
25791
|
// "reply to this email" is not one, so without a support contact the template asks for it.
|
|
25512
|
-
opts.supportContact
|
|
25792
|
+
supportContactLine(opts.supportContact, { required: true })
|
|
25513
25793
|
),
|
|
25514
25794
|
// Round 12: the template used {{support_contact}} without listing it as a merge tag to fill.
|
|
25515
25795
|
tags: ["{{charge_date}}", "{{next_charge_date}}", ...opts.supportContact ? [] : [SUPPORT_CONTACT_PLACEHOLDER]]
|
|
@@ -25632,7 +25912,8 @@ ${html ?? ""}`, kind);
|
|
|
25632
25912
|
text: t.text,
|
|
25633
25913
|
html: htmlOf(t.text),
|
|
25634
25914
|
merge_tags: t.tags,
|
|
25635
|
-
|
|
25915
|
+
// Round 32: only the placeholders the template really contains (a cancellation confirmation has no {{cancel_url}}).
|
|
25916
|
+
placeholders: [CANCEL_URL_PLACEHOLDER, SUPPORT_CONTACT_PLACEHOLDER].filter((p) => t.text.includes(p) || t.subject.includes(p)),
|
|
25636
25917
|
verified: remaining.length === 0,
|
|
25637
25918
|
remaining_checks: remaining
|
|
25638
25919
|
};
|
|
@@ -26050,6 +26331,7 @@ __export(preflight_exports, {
|
|
|
26050
26331
|
PREFLIGHT_DISCLAIMER: () => PREFLIGHT_DISCLAIMER,
|
|
26051
26332
|
PREFLIGHT_DOCS_URL: () => PREFLIGHT_DOCS_URL,
|
|
26052
26333
|
PreflightInputError: () => PreflightInputError,
|
|
26334
|
+
SATISFIED_COMPLIANCE: () => SATISFIED_COMPLIANCE,
|
|
26053
26335
|
STRIPE_HOSTED_STYLING_NOTE: () => STRIPE_HOSTED_STYLING_NOTE,
|
|
26054
26336
|
checkDisclosure: () => checkDisclosure,
|
|
26055
26337
|
checkEmail: () => checkEmail,
|
|
@@ -26059,6 +26341,7 @@ __export(preflight_exports, {
|
|
|
26059
26341
|
checkoutSessionsOf: () => checkoutSessionsOf,
|
|
26060
26342
|
emailTemplate: () => emailTemplate,
|
|
26061
26343
|
extractStaticPage: () => extractStaticPage,
|
|
26344
|
+
intervalHintOf: () => intervalHintOf,
|
|
26062
26345
|
lineItemPriceIds: () => lineItemPriceIds,
|
|
26063
26346
|
modelCheckoutConfig: () => modelCheckoutConfig,
|
|
26064
26347
|
modelContext: () => modelContext,
|
|
@@ -26066,12 +26349,17 @@ __export(preflight_exports, {
|
|
|
26066
26349
|
parseHtml: () => parseHtml,
|
|
26067
26350
|
parseJurisdictionList: () => parseJurisdictionList,
|
|
26068
26351
|
paymentLinksOf: () => paymentLinksOf,
|
|
26352
|
+
priceIntervalConflict: () => priceIntervalConflict,
|
|
26069
26353
|
resolvePlan: () => resolvePlan,
|
|
26070
26354
|
resolvePriceIds: () => resolvePriceIds,
|
|
26071
26355
|
runChecks: () => runChecks,
|
|
26072
26356
|
shapeChecks: () => shapeChecks,
|
|
26073
26357
|
signupStripeObjects: () => signupStripeObjects,
|
|
26074
|
-
|
|
26358
|
+
stripeParamsVerdict: () => stripeParamsVerdict,
|
|
26359
|
+
supportContactLine: () => supportContactLine,
|
|
26360
|
+
testRunSessionIds: () => testRunSessionIds,
|
|
26361
|
+
withIntroOffers: () => withIntroOffers,
|
|
26362
|
+
withTestRunSessions: () => withTestRunSessions
|
|
26075
26363
|
});
|
|
26076
26364
|
var init_preflight = __esm({
|
|
26077
26365
|
"../rules/src/preflight/index.ts"() {
|
|
@@ -26079,6 +26367,8 @@ var init_preflight = __esm({
|
|
|
26079
26367
|
init_common();
|
|
26080
26368
|
init_disclosure();
|
|
26081
26369
|
init_stripe2();
|
|
26370
|
+
init_price_hint();
|
|
26371
|
+
init_stripe_verdict();
|
|
26082
26372
|
init_email();
|
|
26083
26373
|
init_html_page();
|
|
26084
26374
|
init_page();
|
|
@@ -26263,7 +26553,8 @@ var init_check = __esm({
|
|
|
26263
26553
|
}
|
|
26264
26554
|
const hf = str(f, "html-file");
|
|
26265
26555
|
if (hf) html = await readInput(ctx, hf);
|
|
26266
|
-
if (!subject
|
|
26556
|
+
if (!subject && html) subject = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html)?.[1]?.replace(/\s+/g, " ").trim() || void 0;
|
|
26557
|
+
if (!subject) throw new UsageError('--subject is required (or a "Subject: \u2026" first line in --file, or an HTML <title>)');
|
|
26267
26558
|
if (!text && !html) throw new UsageError("Give the email body with --text, --file or --html-file");
|
|
26268
26559
|
const terms2 = planFrom(f);
|
|
26269
26560
|
const pp = str(f, "previous-price");
|
|
@@ -26425,7 +26716,7 @@ function loadConfig(path) {
|
|
|
26425
26716
|
function validateConfig(value) {
|
|
26426
26717
|
const is = new Issues();
|
|
26427
26718
|
if (!isObj2(value)) return ["must be a JSON object (see https://checkbox.site/docs/guides/ci#config)"];
|
|
26428
|
-
is.keys("", value, ["$schema", "merchant", "jurisdictions", "plans", "notices", "checks", "coverage"]);
|
|
26719
|
+
is.keys("", value, ["$schema", "merchant", "jurisdictions", "plans", "notices", "merge_tags", "checks", "coverage"]);
|
|
26429
26720
|
const cov = value.coverage;
|
|
26430
26721
|
if (cov !== void 0 && cov !== false) {
|
|
26431
26722
|
if (!isObj2(cov)) is.add("coverage", `must be false or an object like { "ignore": ["scripts/**"] } (got ${show(cov)})`);
|
|
@@ -26493,6 +26784,15 @@ function validateConfig(value) {
|
|
|
26493
26784
|
}
|
|
26494
26785
|
}
|
|
26495
26786
|
}
|
|
26787
|
+
const mergeTags = (at, m2) => {
|
|
26788
|
+
if (m2 === void 0) return;
|
|
26789
|
+
if (!isObj2(m2) || !Object.keys(m2).length) return is.add(at, `must be an object mapping each merge tag to its value, like { "price": "plan.price", "interval": "plan.interval", "first_name": "Sam" } (got ${show(m2)})`);
|
|
26790
|
+
for (const [k, v] of Object.entries(m2)) {
|
|
26791
|
+
if (!/^[A-Za-z_][\w.-]{0,60}$/.test(k)) is.add(`${at}.${k}`, "is not a merge tag name (letters, digits, _ . -; the name inside {{ }})");
|
|
26792
|
+
else if (typeof v !== "string" || v.length > 500) is.add(`${at}.${k}`, `must be a string: a sample value, or one of ${MERGE_TAG_FIELDS.join(", ")} (got ${show(v)})`);
|
|
26793
|
+
}
|
|
26794
|
+
};
|
|
26795
|
+
mergeTags("merge_tags", value.merge_tags);
|
|
26496
26796
|
const checks = value.checks;
|
|
26497
26797
|
if (!isObj2(checks)) {
|
|
26498
26798
|
is.add("checks", checks === void 0 ? "is required (an object with stripe_checkout, disclosures, pages, emails and/or notice_schedule)" : `must be an object (got ${show(checks)})`);
|
|
@@ -26577,7 +26877,8 @@ function validateConfig(value) {
|
|
|
26577
26877
|
entries(
|
|
26578
26878
|
"emails",
|
|
26579
26879
|
(at, e) => {
|
|
26580
|
-
is.keys(at, e, ["name", "plan", "kind", "subject", "file", "text", "html", "module", "args", "previous_price"]);
|
|
26880
|
+
is.keys(at, e, ["name", "plan", "kind", "subject", "file", "text", "html", "module", "args", "previous_price", "merge_tags"]);
|
|
26881
|
+
mergeTags(`${at}.merge_tags`, e.merge_tags);
|
|
26581
26882
|
is.oneOf(`${at}.kind`, e.kind, EMAIL_KINDS2, true);
|
|
26582
26883
|
is.exactlyOne(at, e, ["file", "text", "html", "module"], [["text", "html"]]);
|
|
26583
26884
|
if (e.file !== void 0) {
|
|
@@ -26610,7 +26911,7 @@ function entryLine(raw, name) {
|
|
|
26610
26911
|
const i = lines.findIndex((l) => needle.test(l));
|
|
26611
26912
|
return i >= 0 ? i + 1 : 1;
|
|
26612
26913
|
}
|
|
26613
|
-
var CONFIG_FILES, SCHEMA_URL, INTERVALS4, EMAIL_KINDS2, PAGE_KINDS2, ConfigError, isObj2, show, TODO, Issues, PLAN_KEYS;
|
|
26914
|
+
var CONFIG_FILES, SCHEMA_URL, INTERVALS4, EMAIL_KINDS2, PAGE_KINDS2, MERGE_TAG_FIELDS, ConfigError, isObj2, show, TODO, Issues, PLAN_KEYS;
|
|
26614
26915
|
var init_config3 = __esm({
|
|
26615
26916
|
"src/ci/config.ts"() {
|
|
26616
26917
|
"use strict";
|
|
@@ -26619,6 +26920,7 @@ var init_config3 = __esm({
|
|
|
26619
26920
|
INTERVALS4 = ["day", "week", "month", "year"];
|
|
26620
26921
|
EMAIL_KINDS2 = ["acknowledgment", "trial_reminder", "renewal_reminder", "price_change", "annual_terms", "charge_notice", "cancellation_confirmation"];
|
|
26621
26922
|
PAGE_KINDS2 = ["checkout", "pricing"];
|
|
26923
|
+
MERGE_TAG_FIELDS = ["plan.price", "plan.amount", "plan.interval", "plan.name", "plan.trial_days", "plan.currency", "merchant.name", "merchant.cancel_url", "merchant.support_contact"];
|
|
26622
26924
|
ConfigError = class extends Error {
|
|
26623
26925
|
constructor(file, issues) {
|
|
26624
26926
|
super(`${file}: ${issues.length === 1 ? issues[0] : `${issues.length} problems:
|
|
@@ -26699,8 +27001,8 @@ var init_config3 = __esm({
|
|
|
26699
27001
|
});
|
|
26700
27002
|
|
|
26701
27003
|
// src/commands/init/literal.ts
|
|
26702
|
-
function parseObjectLiteral(source, env2 = {}) {
|
|
26703
|
-
const p = new Parser(source, env2);
|
|
27004
|
+
function parseObjectLiteral(source, env2 = {}, consts = {}) {
|
|
27005
|
+
const p = new Parser(source, env2, consts);
|
|
26704
27006
|
if (p.peek() !== "{") return { value: null, unresolved: ["<not an object literal>"] };
|
|
26705
27007
|
const v = p.object("");
|
|
26706
27008
|
return { value: v === UNKNOWN ? null : v, unresolved: [...new Set(p.unresolved)], ...Object.keys(p.fromEnv).length ? { fromEnv: p.fromEnv } : {} };
|
|
@@ -26710,10 +27012,11 @@ var init_literal = __esm({
|
|
|
26710
27012
|
"src/commands/init/literal.ts"() {
|
|
26711
27013
|
"use strict";
|
|
26712
27014
|
UNKNOWN = Symbol("unknown");
|
|
26713
|
-
Parser = class {
|
|
26714
|
-
constructor(s2, env2) {
|
|
27015
|
+
Parser = class _Parser {
|
|
27016
|
+
constructor(s2, env2, consts = {}) {
|
|
26715
27017
|
this.s = s2;
|
|
26716
27018
|
this.env = env2;
|
|
27019
|
+
this.consts = consts;
|
|
26717
27020
|
}
|
|
26718
27021
|
i = 0;
|
|
26719
27022
|
unresolved = [];
|
|
@@ -26803,6 +27106,12 @@ var init_literal = __esm({
|
|
|
26803
27106
|
if (name === "false") return false;
|
|
26804
27107
|
if (name === "null") return null;
|
|
26805
27108
|
if (name === "undefined") return void 0;
|
|
27109
|
+
if (Object.prototype.hasOwnProperty.call(this.consts, name)) {
|
|
27110
|
+
const sub = new _Parser(this.consts[name], this.env);
|
|
27111
|
+
const v = sub.value("");
|
|
27112
|
+
if (v !== UNKNOWN && v !== void 0 && path) this.fromEnv[path] = name;
|
|
27113
|
+
return v;
|
|
27114
|
+
}
|
|
26806
27115
|
const env2 = /^(?:process\.env|import\.meta\.env)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(name);
|
|
26807
27116
|
if (env2) {
|
|
26808
27117
|
if (this.env[env2[1]] !== void 0 && path) this.fromEnv[path] = name;
|
|
@@ -26965,6 +27274,12 @@ function detectBrand(root, layoutFile, pkgName) {
|
|
|
26965
27274
|
}
|
|
26966
27275
|
return void 0;
|
|
26967
27276
|
}
|
|
27277
|
+
function detectMailer(deps, framework, hasPackageJson) {
|
|
27278
|
+
const found = MAILER_PACKAGES.find((m) => m in deps);
|
|
27279
|
+
if (found) return found;
|
|
27280
|
+
if (!hasPackageJson && framework === "html") return "none";
|
|
27281
|
+
return null;
|
|
27282
|
+
}
|
|
26968
27283
|
function readJson(path) {
|
|
26969
27284
|
try {
|
|
26970
27285
|
return JSON.parse(readFileSync5(path, "utf8"));
|
|
@@ -27024,12 +27339,49 @@ function walk2(root, visit) {
|
|
|
27024
27339
|
}
|
|
27025
27340
|
}
|
|
27026
27341
|
}
|
|
27342
|
+
function hasAppRouteFile(dir, depth = 0) {
|
|
27343
|
+
if (depth > 3) return false;
|
|
27344
|
+
let names = [];
|
|
27345
|
+
try {
|
|
27346
|
+
names = readdirSync2(dir);
|
|
27347
|
+
} catch {
|
|
27348
|
+
return false;
|
|
27349
|
+
}
|
|
27350
|
+
if (names.some((n) => /^(?:page|route|layout)\.(?:tsx|jsx|ts|js|mdx)$/.test(n))) return true;
|
|
27351
|
+
return names.some((n) => !n.startsWith(".") && n !== "node_modules" && (() => {
|
|
27352
|
+
try {
|
|
27353
|
+
return statSync(join4(dir, n)).isDirectory();
|
|
27354
|
+
} catch {
|
|
27355
|
+
return false;
|
|
27356
|
+
}
|
|
27357
|
+
})() && hasAppRouteFile(join4(dir, n), depth + 1));
|
|
27358
|
+
}
|
|
27359
|
+
function hasTsFiles(dir, depth = 0) {
|
|
27360
|
+
if (depth > 3) return false;
|
|
27361
|
+
let names = [];
|
|
27362
|
+
try {
|
|
27363
|
+
names = readdirSync2(dir);
|
|
27364
|
+
} catch {
|
|
27365
|
+
return false;
|
|
27366
|
+
}
|
|
27367
|
+
if (names.some((n) => /\.tsx?$/.test(n))) return true;
|
|
27368
|
+
return names.some((n) => !n.startsWith(".") && (() => {
|
|
27369
|
+
try {
|
|
27370
|
+
return statSync(join4(dir, n)).isDirectory();
|
|
27371
|
+
} catch {
|
|
27372
|
+
return false;
|
|
27373
|
+
}
|
|
27374
|
+
})() && hasTsFiles(join4(dir, n), depth + 1));
|
|
27375
|
+
}
|
|
27027
27376
|
function detectFramework(root, deps) {
|
|
27028
27377
|
const has2 = (d) => d in deps;
|
|
27029
27378
|
if (existsSync4(join4(root, "layout", "theme.liquid")) && existsSync4(join4(root, "config", "settings_schema.json"))) return { framework: "shopify-theme", layoutFile: "layout/theme.liquid" };
|
|
27030
27379
|
if (has2("next")) {
|
|
27031
27380
|
const app = first(root, ["app/layout.tsx", "app/layout.jsx", "app/layout.js", "app/layout.ts", "src/app/layout.tsx", "src/app/layout.jsx", "src/app/layout.js"]);
|
|
27032
27381
|
if (app) return { framework: "nextjs-app", layoutFile: app };
|
|
27382
|
+
const appDir = ["src/app", "app"].find((d) => existsSync4(join4(root, d)) && hasAppRouteFile(join4(root, d)));
|
|
27383
|
+
const pagesExists = existsSync4(join4(root, "pages")) || existsSync4(join4(root, "src", "pages"));
|
|
27384
|
+
if (appDir && !pagesExists) return { framework: "nextjs-app", layoutFile: `${appDir}/layout.${existsSync4(join4(root, "tsconfig.json")) || hasTsFiles(join4(root, appDir)) ? "tsx" : "js"}` };
|
|
27033
27385
|
const doc = first(root, ["pages/_document.tsx", "pages/_document.jsx", "pages/_document.js", "src/pages/_document.tsx", "src/pages/_document.jsx", "src/pages/_document.js"]);
|
|
27034
27386
|
const pagesDir = existsSync4(join4(root, "src", "pages")) ? "src/pages" : "pages";
|
|
27035
27387
|
return { framework: "nextjs-pages", layoutFile: doc ?? `${pagesDir}/_document.${existsSync4(join4(root, "tsconfig.json")) ? "tsx" : "js"}` };
|
|
@@ -27116,11 +27468,11 @@ function routeOf(rel2) {
|
|
|
27116
27468
|
}
|
|
27117
27469
|
return null;
|
|
27118
27470
|
}
|
|
27119
|
-
function findStripeKey(env2, processEnv) {
|
|
27471
|
+
function findStripeKey(env2, processEnv, envFile = {}) {
|
|
27120
27472
|
const sources = [];
|
|
27121
27473
|
for (const k of ["CHECKBOX_STRIPE_TEST_KEY", "STRIPE_RESTRICTED_KEY", "STRIPE_TEST_RESTRICTED_KEY", "STRIPE_SECRET_KEY", "STRIPE_TEST_SECRET_KEY", "STRIPE_API_KEY", "STRIPE_SK"]) {
|
|
27122
27474
|
sources.push([`${k} (environment)`, processEnv[k]]);
|
|
27123
|
-
sources.push([`${k} (.env)`, env2[k]]);
|
|
27475
|
+
sources.push([`${k} (${envFile[k] ?? ".env"})`, env2[k]]);
|
|
27124
27476
|
}
|
|
27125
27477
|
const found = sources.filter((s2) => typeof s2[1] === "string" && /^(sk|rk)_(test|live)_[A-Za-z0-9]{10,}$/.test(s2[1].trim())).map(([src, v]) => [src, v.trim()]);
|
|
27126
27478
|
const hint = (key) => `${key.slice(0, 8)}\u2026${key.slice(-4)}`;
|
|
@@ -27147,7 +27499,13 @@ function sessionLiteral(text, index) {
|
|
|
27147
27499
|
else if (ch === quote2) quote2 = null;
|
|
27148
27500
|
continue;
|
|
27149
27501
|
}
|
|
27150
|
-
if (ch ===
|
|
27502
|
+
if (ch === "/" && text[i + 1] === "/") {
|
|
27503
|
+
const nl = text.indexOf("\n", i);
|
|
27504
|
+
i = nl === -1 ? text.length : nl;
|
|
27505
|
+
} else if (ch === "/" && text[i + 1] === "*") {
|
|
27506
|
+
const end = text.indexOf("*/", i + 2);
|
|
27507
|
+
i = end === -1 ? text.length : end + 1;
|
|
27508
|
+
} else if (ch === '"' || ch === "'" || ch === "`") quote2 = ch;
|
|
27151
27509
|
else if (ch === "{") depth++;
|
|
27152
27510
|
else if (ch === "}") {
|
|
27153
27511
|
depth--;
|
|
@@ -27156,17 +27514,73 @@ function sessionLiteral(text, index) {
|
|
|
27156
27514
|
}
|
|
27157
27515
|
return null;
|
|
27158
27516
|
}
|
|
27517
|
+
function declaredExpr(text, from) {
|
|
27518
|
+
let depth = 0;
|
|
27519
|
+
let quote2 = null;
|
|
27520
|
+
for (let i = from; i < text.length && i - from < 2e3; i++) {
|
|
27521
|
+
const ch = text[i];
|
|
27522
|
+
if (quote2) {
|
|
27523
|
+
if (ch === "\\") i++;
|
|
27524
|
+
else if (ch === quote2) quote2 = null;
|
|
27525
|
+
continue;
|
|
27526
|
+
}
|
|
27527
|
+
if (ch === '"' || ch === "'" || ch === "`") quote2 = ch;
|
|
27528
|
+
else if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
27529
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
27530
|
+
else if ((ch === ";" || ch === "\n") && depth <= 0) return text.slice(from, i).trim();
|
|
27531
|
+
}
|
|
27532
|
+
return text.slice(from, from + 2e3).trim();
|
|
27533
|
+
}
|
|
27534
|
+
function constDeclarations(text, exportedOnly) {
|
|
27535
|
+
const out = {};
|
|
27536
|
+
const re = exportedOnly ? /(?:^|\n)[ \t]*export\s+const\s+([A-Za-z_$][\w$]*)\s*(?::[^=\n]+)?=\s*/g : /(?:^|\n)[ \t]*(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*(?::[^=\n]+)?=\s*/g;
|
|
27537
|
+
for (const m of text.matchAll(re)) if (!(m[1] in out)) out[m[1]] = declaredExpr(text, m.index + m[0].length);
|
|
27538
|
+
return out;
|
|
27539
|
+
}
|
|
27540
|
+
function resolveImport(root, fromFile, spec) {
|
|
27541
|
+
const dir = fromFile.includes("/") ? fromFile.slice(0, fromFile.lastIndexOf("/")) : "";
|
|
27542
|
+
const bases = spec.startsWith(".") ? [join4(dir, spec)] : /^[@~]\//.test(spec) ? [spec.slice(2), join4("src", spec.slice(2))] : [];
|
|
27543
|
+
for (const b of bases) {
|
|
27544
|
+
const base = b.split("\\").join("/").replace(/\.(?:[cm]?js|jsx)$/, "");
|
|
27545
|
+
for (const f of [b, ...[".ts", ".tsx", ".js", ".mjs", ".jsx", "/index.ts", "/index.js"].map((e) => `${base}${e}`)]) {
|
|
27546
|
+
if (/\.[cm]?[jt]sx?$/.test(f) && existsSync4(join4(root, f))) return f.split("\\").join("/");
|
|
27547
|
+
}
|
|
27548
|
+
}
|
|
27549
|
+
return void 0;
|
|
27550
|
+
}
|
|
27551
|
+
function constantsFor(root, relFile, text) {
|
|
27552
|
+
const out = {};
|
|
27553
|
+
for (const m of text.matchAll(/import\s*(?:type\s+)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
27554
|
+
const target = resolveImport(root, relFile, m[2]);
|
|
27555
|
+
if (!target) continue;
|
|
27556
|
+
let exported;
|
|
27557
|
+
try {
|
|
27558
|
+
exported = constDeclarations(readFileSync5(join4(root, target), "utf8"), true);
|
|
27559
|
+
} catch {
|
|
27560
|
+
continue;
|
|
27561
|
+
}
|
|
27562
|
+
for (const part of m[1].split(",")) {
|
|
27563
|
+
const [name, alias] = part.trim().split(/\s+as\s+/).map((x) => x.trim());
|
|
27564
|
+
if (name && exported[name] !== void 0) out[alias || name] = exported[name];
|
|
27565
|
+
}
|
|
27566
|
+
}
|
|
27567
|
+
return { ...out, ...constDeclarations(text, false) };
|
|
27568
|
+
}
|
|
27159
27569
|
function detectProject(root, processEnv = {}) {
|
|
27160
27570
|
const pkg = readJson(join4(root, "package.json"));
|
|
27161
27571
|
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
27162
27572
|
const { framework, layoutFile } = detectFramework(root, deps);
|
|
27163
27573
|
const packageManager = existsSync4(join4(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync4(join4(root, "yarn.lock")) ? "yarn" : existsSync4(join4(root, "bun.lockb")) || existsSync4(join4(root, "bun.lock")) ? "bun" : pkg ? "npm" : null;
|
|
27164
27574
|
const env2 = {};
|
|
27575
|
+
const envFile = {};
|
|
27165
27576
|
for (const f of [".env", ".env.production", ".env.local", ".env.development", ".env.development.local"]) {
|
|
27166
27577
|
const t = readText(join4(root, f));
|
|
27167
|
-
if (t)
|
|
27578
|
+
if (!t) continue;
|
|
27579
|
+
const vars = parseDotenv(t);
|
|
27580
|
+
Object.assign(env2, vars);
|
|
27581
|
+
for (const k of Object.keys(vars)) envFile[k] = f;
|
|
27168
27582
|
}
|
|
27169
|
-
|
|
27583
|
+
let billingPlatform = "stripe" in deps || "@stripe/stripe-js" in deps || "@stripe/react-stripe-js" in deps ? "stripe" : framework === "shopify-theme" || "@shopify/hydrogen" in deps || "@shopify/shopify-api" in deps ? "shopify" : "@rechargeapps/storefront-client" in deps ? "recharge" : "@paddle/paddle-js" in deps ? "paddle" : "chargebee" in deps ? "chargebee" : "custom";
|
|
27170
27584
|
const checkoutCode = [];
|
|
27171
27585
|
const payButtons = [];
|
|
27172
27586
|
const checkoutSessions = [];
|
|
@@ -27192,12 +27606,13 @@ function detectProject(root, processEnv = {}) {
|
|
|
27192
27606
|
checkoutSessions.push({ file: rel2, line: line2, unresolved: ["<params are not an object literal>"] });
|
|
27193
27607
|
continue;
|
|
27194
27608
|
}
|
|
27195
|
-
const parsed = parseObjectLiteral(lit, { ...env2, ...Object.fromEntries(Object.entries(processEnv).filter((e) => typeof e[1] === "string" && /^(STRIPE_|NEXT_PUBLIC_STRIPE_|PRICE_)/.test(e[0]))) });
|
|
27609
|
+
const parsed = parseObjectLiteral(lit, { ...env2, ...Object.fromEntries(Object.entries(processEnv).filter((e) => typeof e[1] === "string" && /^(STRIPE_|NEXT_PUBLIC_STRIPE_|PRICE_)/.test(e[0]))) }, constantsFor(root, rel2, text));
|
|
27196
27610
|
checkoutSessions.push({ file: rel2, line: line2, ...parsed.value ? { params: redactSecretsDeep(parsed.value) } : {}, unresolved: parsed.unresolved });
|
|
27197
27611
|
}
|
|
27198
27612
|
});
|
|
27613
|
+
if (billingPlatform === "custom" && checkoutCode.some((c) => c.kind.startsWith("stripe_"))) billingPlatform = "stripe";
|
|
27199
27614
|
const site = detectSiteUrl(root, env2, pkg);
|
|
27200
|
-
const stripeKey = findStripeKey(env2, processEnv);
|
|
27615
|
+
const stripeKey = findStripeKey(env2, processEnv, envFile);
|
|
27201
27616
|
const brand = detectBrand(root, layoutFile, typeof pkg?.name === "string" ? pkg.name : void 0);
|
|
27202
27617
|
return {
|
|
27203
27618
|
root,
|
|
@@ -27218,16 +27633,18 @@ function detectProject(root, processEnv = {}) {
|
|
|
27218
27633
|
...stripeKey.refused ? { stripeKeyRefused: stripeKey.refused } : {},
|
|
27219
27634
|
...brand ? { brand } : {},
|
|
27220
27635
|
plans: plansFromSessions(checkoutSessions),
|
|
27221
|
-
env: env2
|
|
27636
|
+
env: env2,
|
|
27637
|
+
mailer: detectMailer(deps, framework, !!pkg)
|
|
27222
27638
|
};
|
|
27223
27639
|
}
|
|
27224
|
-
var GENERIC_NAMES, IGNORE_DIRS, CODE_EXT, MAX_FILES, MAX_FILE_BYTES, first, SITE_URL_KEYS, POLICY_ROUTES, CHECKOUT_PATTERNS, PAY_BUTTON;
|
|
27640
|
+
var GENERIC_NAMES, MAILER_PACKAGES, IGNORE_DIRS, CODE_EXT, MAX_FILES, MAX_FILE_BYTES, first, SITE_URL_KEYS, POLICY_ROUTES, CHECKOUT_PATTERNS, PAY_BUTTON;
|
|
27225
27641
|
var init_detect = __esm({
|
|
27226
27642
|
"src/commands/init/detect.ts"() {
|
|
27227
27643
|
"use strict";
|
|
27228
27644
|
init_src2();
|
|
27229
27645
|
init_literal();
|
|
27230
27646
|
GENERIC_NAMES = /^(?:web|app|site|website|frontend|client|my-app|my-site|demo|example|store|shop|starter|template|project|monorepo|root)$/i;
|
|
27647
|
+
MAILER_PACKAGES = ["nodemailer", "resend", "@sendgrid/mail", "postmark", "mailgun.js", "mailgun-js", "@aws-sdk/client-ses", "@aws-sdk/client-sesv2", "@react-email/components", "react-email", "@mailchimp/mailchimp_transactional", "mailersend", "@sparkpost/node", "sparkpost", "emailjs", "@emailjs/browser", "loops", "plunk", "@plunk/node", "brevo", "@getbrevo/brevo", "sib-api-v3-sdk", "nodemailer-mailgun-transport", "@vercel/email"];
|
|
27231
27648
|
IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".next", ".nuxt", ".svelte-kit", ".output", ".vercel", ".netlify", ".turbo", ".cache", "dist", "build", "out", "coverage", ".astro", "vendor", ".venv", "__pycache__"]);
|
|
27232
27649
|
CODE_EXT = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|liquid|html|ejs|hbs|erb)$/i;
|
|
27233
27650
|
MAX_FILES = 4e3;
|
|
@@ -27243,6 +27660,7 @@ var init_detect = __esm({
|
|
|
27243
27660
|
CHECKOUT_PATTERNS = [
|
|
27244
27661
|
{ kind: "stripe_checkout_session", re: /checkout\.sessions\.create\s*\(/ },
|
|
27245
27662
|
{ kind: "stripe_payment_link", re: /paymentLinks\.create\s*\(|https:\/\/buy\.stripe\.com\// },
|
|
27663
|
+
{ kind: "stripe_checkout_link", re: /https:\/\/checkout\.stripe\.com\// },
|
|
27246
27664
|
{ kind: "stripe_pricing_table", re: /<stripe-pricing-table\b/ },
|
|
27247
27665
|
{ kind: "stripe_subscription_api", re: /subscriptions\.create\s*\(/ },
|
|
27248
27666
|
{ kind: "stripe_elements", re: /<(PaymentElement|CardElement|ExpressCheckoutElement)\b|elements\.create\(\s*['"]payment['"]/ },
|
|
@@ -27257,6 +27675,47 @@ var init_detect = __esm({
|
|
|
27257
27675
|
// src/ci/scaffold.ts
|
|
27258
27676
|
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
|
|
27259
27677
|
import { basename, dirname as dirname4, join as join5, relative as relative2 } from "node:path";
|
|
27678
|
+
function countTodoValues(v) {
|
|
27679
|
+
if (typeof v === "string") return /^TODO\b/.test(v) ? 1 : 0;
|
|
27680
|
+
if (Array.isArray(v)) return v.reduce((n, x) => n + countTodoValues(x), 0);
|
|
27681
|
+
if (v && typeof v === "object") return Object.values(v).reduce((n, x) => n + countTodoValues(x), 0);
|
|
27682
|
+
return 0;
|
|
27683
|
+
}
|
|
27684
|
+
function mergeTagsIn(text) {
|
|
27685
|
+
const out = [];
|
|
27686
|
+
for (const m of text.matchAll(/\{\{\{?\s*([A-Za-z_][\w.-]*)\s*\}?\}\}|\*\|([A-Za-z_][\w.-]*)\|\*/g)) {
|
|
27687
|
+
const t = m[1] ?? m[2];
|
|
27688
|
+
if (!out.includes(t)) out.push(t);
|
|
27689
|
+
}
|
|
27690
|
+
return out;
|
|
27691
|
+
}
|
|
27692
|
+
function mergeTagField(tag) {
|
|
27693
|
+
const t = tag.toLowerCase().replace(/[.-]/g, "_").replace(/^(?:subscription|sub)_/, "");
|
|
27694
|
+
if (/^(?:plan_)?(?:price|amount|cost|recurring_price|price_formatted|formatted_price|renewal_price|renewal_amount)$/.test(t)) return "plan.price";
|
|
27695
|
+
if (/^(?:plan_)?(?:interval|billing_interval|period|billing_period|frequency|billing_cycle|cycle|interval_name)$/.test(t)) return "plan.interval";
|
|
27696
|
+
if (/^(?:plan|plan_name|product|product_name|plan_title|tier)$/.test(t)) return "plan.name";
|
|
27697
|
+
if (/^(?:plan_)?(?:trial_days|trial_length|trial_period_days)$/.test(t)) return "plan.trial_days";
|
|
27698
|
+
if (/^(?:cancel_url|cancellation_url|manage_url|billing_url|account_url|cancel_link)$/.test(t)) return "merchant.cancel_url";
|
|
27699
|
+
if (/^(?:company|company_name|merchant|merchant_name|brand|brand_name|business_name|app_name)$/.test(t)) return "merchant.name";
|
|
27700
|
+
if (/^(?:support_email|support_contact|support|help_email|contact_email)$/.test(t)) return "merchant.support_contact";
|
|
27701
|
+
return void 0;
|
|
27702
|
+
}
|
|
27703
|
+
function pageSources(root) {
|
|
27704
|
+
const out = [];
|
|
27705
|
+
walk3(root, (rel2) => {
|
|
27706
|
+
if (out.length >= 4) return;
|
|
27707
|
+
let m = /^(?:src\/)?app\/((?:\([^/]+\)\/)*)([^/()]+)\/page\.(?:tsx|jsx|ts|js|mdx)$/.exec(rel2);
|
|
27708
|
+
if (m && PAGE_ROUTE.test(m[2])) {
|
|
27709
|
+
out.push({ source: rel2, file: `.next/server/app/${m[2]}.html`, route: m[2] });
|
|
27710
|
+
return;
|
|
27711
|
+
}
|
|
27712
|
+
m = /^(?:src\/)?pages\/([^/]+?)(?:\/index)?\.(tsx|jsx|ts|js|astro)$/.exec(rel2);
|
|
27713
|
+
if (m && PAGE_ROUTE.test(m[1])) {
|
|
27714
|
+
out.push({ source: rel2, file: m[2] === "astro" ? `dist/${m[1]}/index.html` : `.next/server/pages/${m[1]}.html`, route: m[1] });
|
|
27715
|
+
}
|
|
27716
|
+
});
|
|
27717
|
+
return out;
|
|
27718
|
+
}
|
|
27260
27719
|
function walk3(root, visit) {
|
|
27261
27720
|
let n = 0;
|
|
27262
27721
|
const go = (dir, depth) => {
|
|
@@ -27306,7 +27765,8 @@ function plansFromLiterals(text, file) {
|
|
|
27306
27765
|
const trial = /\btrial(?:_period)?_?days\s*:\s*(\d+)/i.exec(body)?.[1];
|
|
27307
27766
|
if (out.some((p) => p.name === name)) continue;
|
|
27308
27767
|
const key = m[1]?.trim() && /^[A-Za-z_$][\w$]*$/.test(m[1].trim()) ? m[1].trim() : void 0;
|
|
27309
|
-
|
|
27768
|
+
const ids = [...new Set([...body.matchAll(/['"`](price_[A-Za-z0-9_]+)['"`]/g)].map((x) => x[1]))];
|
|
27769
|
+
out.push({ name, ...key ? { key } : {}, price: major, currency, interval, ...count && Number(count) > 1 ? { interval_count: Number(count) } : {}, ...trial && Number(trial) > 0 ? { trial_days: Number(trial) } : {}, ...ids.length ? { price_ids: ids } : {}, source: file });
|
|
27310
27770
|
}
|
|
27311
27771
|
return out;
|
|
27312
27772
|
}
|
|
@@ -27330,10 +27790,111 @@ function paramsBuilder(root, file, line2) {
|
|
|
27330
27790
|
} else if (new RegExp(`export\\s+(?:async\\s+)?function\\s+${fn}\\b|export\\s+const\\s+${fn}\\b`).test(text)) target = file;
|
|
27331
27791
|
return target ? { module: `${target}#${fn}`, takesArgs } : void 0;
|
|
27332
27792
|
}
|
|
27793
|
+
function plansSoldBy(params, plans) {
|
|
27794
|
+
const items = Array.isArray(params.line_items) ? params.line_items : [];
|
|
27795
|
+
const out = [];
|
|
27796
|
+
for (const li of items) {
|
|
27797
|
+
const pd = li?.price_data;
|
|
27798
|
+
if (!pd || typeof pd.unit_amount !== "number") continue;
|
|
27799
|
+
const match = plans.find((p) => (typeof p.amount === "number" ? p.amount : typeof p.price === "number" ? Math.round(p.price * 100) : NaN) === pd.unit_amount && p.interval === pd.recurring?.interval);
|
|
27800
|
+
if (match && !out.includes(match)) out.push(match);
|
|
27801
|
+
}
|
|
27802
|
+
return out;
|
|
27803
|
+
}
|
|
27804
|
+
function cancelUrlInCode(sessions) {
|
|
27805
|
+
const urls = /* @__PURE__ */ new Set();
|
|
27806
|
+
for (const s2 of sessions) {
|
|
27807
|
+
const text = JSON.stringify(s2.params?.custom_text ?? "");
|
|
27808
|
+
for (const m of text.matchAll(/cancel[^"]{0,80}?(https:\/\/[^\s"'<>)]+?)(?=[.,;:!?]?(?:[\s"'<>)]|$))/gi)) urls.add(m[1]);
|
|
27809
|
+
}
|
|
27810
|
+
return urls.size === 1 ? [...urls][0] : void 0;
|
|
27811
|
+
}
|
|
27812
|
+
function pageSource(root, built) {
|
|
27813
|
+
const route = /(?:^|\/)(?:public|dist|out|build|static|_site|www)\/(?:(.*)\/)?([^/]+?)(?:\/index)?\.html?$/i.exec(built);
|
|
27814
|
+
if (!route) return void 0;
|
|
27815
|
+
const path = [route[1], route[2]].filter(Boolean).join("/");
|
|
27816
|
+
const bases = [`app/${path}/page`, `src/app/${path}/page`, `app/(marketing)/${path}/page`, `pages/${path}`, `src/pages/${path}`, `src/routes/${path}/+page`, `pages/${path}/index`, `src/pages/${path}/index`];
|
|
27817
|
+
const hits = bases.flatMap((b) => [".tsx", ".jsx", ".ts", ".js", ".svelte", ".vue", ".astro", ".mdx"].map((e) => `${b}${e}`)).filter((f) => existsSync5(join5(root, f)));
|
|
27818
|
+
return hits.length === 1 ? hits[0] : void 0;
|
|
27819
|
+
}
|
|
27820
|
+
function priceExpressions(root, file, line2) {
|
|
27821
|
+
let text;
|
|
27822
|
+
try {
|
|
27823
|
+
text = readFileSync6(join5(root, file), "utf8");
|
|
27824
|
+
} catch {
|
|
27825
|
+
return [];
|
|
27826
|
+
}
|
|
27827
|
+
const from = text.split("\n").slice(line2 - 1).join("\n");
|
|
27828
|
+
const at = from.search(/checkout\.sessions\.create|paymentLinks\.create/);
|
|
27829
|
+
const open2 = at < 0 ? -1 : from.indexOf("(", at);
|
|
27830
|
+
if (open2 < 0) return [];
|
|
27831
|
+
let depth = 0;
|
|
27832
|
+
let end = from.length;
|
|
27833
|
+
for (let i = open2; i < from.length; i++) {
|
|
27834
|
+
const c = from[i];
|
|
27835
|
+
if (c === "(" || c === "{" || c === "[") depth++;
|
|
27836
|
+
else if (c === ")" || c === "}" || c === "]") {
|
|
27837
|
+
depth--;
|
|
27838
|
+
if (depth === 0) {
|
|
27839
|
+
end = i;
|
|
27840
|
+
break;
|
|
27841
|
+
}
|
|
27842
|
+
}
|
|
27843
|
+
}
|
|
27844
|
+
const call = from.slice(open2, end);
|
|
27845
|
+
const li = call.indexOf("line_items");
|
|
27846
|
+
if (li < 0) return [];
|
|
27847
|
+
const items = call.slice(li);
|
|
27848
|
+
const out = [];
|
|
27849
|
+
for (const m of items.matchAll(/\bprice\s*:\s*/g)) {
|
|
27850
|
+
let d = 0;
|
|
27851
|
+
let j = m.index + m[0].length;
|
|
27852
|
+
const start = j;
|
|
27853
|
+
for (; j < items.length; j++) {
|
|
27854
|
+
const c = items[j];
|
|
27855
|
+
if (c === "(" || c === "{" || c === "[") d++;
|
|
27856
|
+
else if (c === ")" || c === "}" || c === "]") {
|
|
27857
|
+
if (d === 0) break;
|
|
27858
|
+
d--;
|
|
27859
|
+
} else if ((c === "," || c === "\n") && d === 0) break;
|
|
27860
|
+
}
|
|
27861
|
+
const expr = items.slice(start, j).trim();
|
|
27862
|
+
if (expr) out.push(expr);
|
|
27863
|
+
if (out.length >= 5) break;
|
|
27864
|
+
}
|
|
27865
|
+
return out;
|
|
27866
|
+
}
|
|
27867
|
+
function pairCall(root, s2, plans, priceIds, calls) {
|
|
27868
|
+
if (plans.length === 1) return { plan: String(plans[0].name) };
|
|
27869
|
+
if (!plans.length || plans.some((p) => String(p.name).startsWith("TODO"))) return {};
|
|
27870
|
+
const sold = s2.params ? plansSoldBy(s2.params, plans) : [];
|
|
27871
|
+
if (sold.length === 1) return { plan: String(sold[0].name) };
|
|
27872
|
+
const items = Array.isArray(s2.params?.line_items) ? s2.params.line_items : [];
|
|
27873
|
+
const literalIds = items.map((li) => li && typeof li.price === "string" ? li.price : void 0).filter((x) => !!x);
|
|
27874
|
+
const exprs = priceExpressions(root, s2.file, s2.line).filter((e) => !/^['"`][^'"`]*['"`]$/.test(e));
|
|
27875
|
+
for (const id of literalIds) {
|
|
27876
|
+
const owner = plans.filter((p) => priceIds.get(String(p.name))?.includes(id));
|
|
27877
|
+
if (owner.length === 1) return { plan: String(owner[0].name), why: `${id} is the Price id of that plan in the plans module` };
|
|
27878
|
+
}
|
|
27879
|
+
const runtime = exprs.find((e) => /\?|\|\||&&|\[[^\]'"`]*\]|\(/.test(e));
|
|
27880
|
+
if (runtime && !literalIds.length) return calls === 1 ? { several: true } : { ref: runtime };
|
|
27881
|
+
for (const ref of [...literalIds, ...exprs]) {
|
|
27882
|
+
const hint = intervalHintOf(ref);
|
|
27883
|
+
if (!hint) continue;
|
|
27884
|
+
const match = plans.filter((p) => {
|
|
27885
|
+
const count = Number(p.interval_count ?? 1) || 1;
|
|
27886
|
+
const unit2 = p.interval === "month" && count === 12 ? "year" : count === 1 ? p.interval : void 0;
|
|
27887
|
+
return unit2 === hint;
|
|
27888
|
+
});
|
|
27889
|
+
if (match.length === 1) return { plan: String(match[0].name), why: `its Price ${ref} is named for a ${hint === "day" ? "dai" : hint}ly price; check this is the plan it sells` };
|
|
27890
|
+
}
|
|
27891
|
+
return { ref: literalIds[0] ?? exprs[0] };
|
|
27892
|
+
}
|
|
27333
27893
|
function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
27334
27894
|
const det = detectProject(root, env2);
|
|
27335
27895
|
const todos = [];
|
|
27336
27896
|
const found = [];
|
|
27897
|
+
const notes2 = [];
|
|
27337
27898
|
const names = /* @__PURE__ */ new Set();
|
|
27338
27899
|
const merchant = {};
|
|
27339
27900
|
const fact = (k) => typeof facts[k] === "string" && facts[k].trim() ? facts[k].trim() : void 0;
|
|
@@ -27348,13 +27909,34 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27348
27909
|
merchant.cancel_url = cancelFact;
|
|
27349
27910
|
found.push(`cancel URL ${cancelFact} (from checkbox init)`);
|
|
27350
27911
|
} else if (siteUrl && cancelPath) merchant.cancel_url = `${originOf(siteUrl)}${cancelPath}`;
|
|
27351
|
-
else {
|
|
27912
|
+
else if (cancelUrlInCode(det.checkoutSessions)) {
|
|
27913
|
+
merchant.cancel_url = cancelUrlInCode(det.checkoutSessions);
|
|
27914
|
+
found.push(`cancel URL ${String(merchant.cancel_url)} (from the Checkout custom_text)`);
|
|
27915
|
+
} else {
|
|
27352
27916
|
merchant.cancel_url = "TODO: https://\u2026 page where subscribers cancel online";
|
|
27353
27917
|
todos.push("merchant.cancel_url: the page where subscribers cancel online");
|
|
27354
27918
|
}
|
|
27355
27919
|
if (fact("support_email")) merchant.support_contact = fact("support_email");
|
|
27920
|
+
const initPlans = Array.isArray(facts.plans) ? facts.plans : [];
|
|
27921
|
+
const initName = (p) => {
|
|
27922
|
+
const m = initPlans.find((x) => typeof x.name === "string" && x.amount === p.amount && x.interval === p.interval && (Number(x.interval_count ?? 1) || 1) === (p.interval_count ?? 1));
|
|
27923
|
+
return m ? String(m.name) : void 0;
|
|
27924
|
+
};
|
|
27925
|
+
const literals = [];
|
|
27926
|
+
walk3(root, (rel2) => {
|
|
27927
|
+
if (!PLAN_FILE.test(rel2) || literals.length >= 8) return;
|
|
27928
|
+
let text = "";
|
|
27929
|
+
try {
|
|
27930
|
+
text = readFileSync6(join5(root, rel2), "utf8");
|
|
27931
|
+
} catch {
|
|
27932
|
+
return;
|
|
27933
|
+
}
|
|
27934
|
+
for (const p of plansFromLiterals(text, rel2)) if (!literals.some((x) => x.name === p.name)) literals.push(p);
|
|
27935
|
+
});
|
|
27936
|
+
const minorOf = (p) => typeof p.amount === "number" ? p.amount : typeof p.price === "number" ? Math.round(p.price * 100) : NaN;
|
|
27937
|
+
const literalFor = (p) => literals.find((x) => Math.round(x.price * 100) === minorOf(p) && x.interval === p.interval && (x.interval_count ?? 1) === (Number(p.interval_count ?? 1) || 1));
|
|
27356
27938
|
const plans = det.plans.map((p, i) => ({
|
|
27357
|
-
name: p.name ?? `Plan ${i + 1}`,
|
|
27939
|
+
name: p.name ?? initName(p) ?? literalFor(p)?.name ?? `Plan ${i + 1}`,
|
|
27358
27940
|
amount: p.amount,
|
|
27359
27941
|
currency: p.currency,
|
|
27360
27942
|
interval: p.interval,
|
|
@@ -27362,39 +27944,41 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27362
27944
|
...p.trial_days ? { trial_days: p.trial_days } : {}
|
|
27363
27945
|
}));
|
|
27364
27946
|
for (const p of det.plans) found.push(`plan ${p.name ?? ""} ${p.amount / 100} ${p.currency}/${p.interval} (${p.source})`.replace(/\s+/g, " "));
|
|
27365
|
-
const planKeys = /* @__PURE__ */ new Map();
|
|
27366
27947
|
if (!plans.length && Array.isArray(facts.plans)) {
|
|
27367
27948
|
for (const [i, p] of facts.plans.entries()) {
|
|
27368
27949
|
if (typeof p.amount !== "number" || typeof p.interval !== "string") continue;
|
|
27369
|
-
plans.push({ name: typeof p.name === "string" ? p.name : `Plan ${i + 1}`, amount: p.amount, currency: typeof p.currency === "string" ? p.currency : "usd", interval: p.interval, ...typeof p.interval_count === "number" ? { interval_count: p.interval_count } : {}, ...typeof p.trial_days === "number" ? { trial_days: p.trial_days } : {} });
|
|
27950
|
+
plans.push({ name: typeof p.name === "string" && p.name.trim() ? p.name : literalFor(p)?.name ?? `Plan ${i + 1}`, amount: p.amount, currency: typeof p.currency === "string" ? p.currency : "usd", interval: p.interval, ...typeof p.interval_count === "number" ? { interval_count: p.interval_count } : {}, ...typeof p.trial_days === "number" ? { trial_days: p.trial_days } : {} });
|
|
27370
27951
|
found.push(`plan ${String(plans[plans.length - 1].name)} (from checkbox init --plan)`);
|
|
27371
27952
|
}
|
|
27372
27953
|
}
|
|
27373
27954
|
if (!plans.length) {
|
|
27374
|
-
|
|
27375
|
-
|
|
27376
|
-
|
|
27377
|
-
|
|
27378
|
-
|
|
27379
|
-
|
|
27380
|
-
|
|
27381
|
-
|
|
27382
|
-
|
|
27383
|
-
|
|
27384
|
-
|
|
27385
|
-
|
|
27386
|
-
|
|
27387
|
-
|
|
27388
|
-
}
|
|
27389
|
-
});
|
|
27955
|
+
for (const p of literals) {
|
|
27956
|
+
const { source, key: _key, price_ids: _ids, ...plan2 } = p;
|
|
27957
|
+
plans.push(plan2);
|
|
27958
|
+
found.push(`plan ${p.name} ${p.price} ${p.currency}/${p.interval} (${source})`);
|
|
27959
|
+
}
|
|
27960
|
+
}
|
|
27961
|
+
const planKeys = /* @__PURE__ */ new Map();
|
|
27962
|
+
const planPriceIds = /* @__PURE__ */ new Map();
|
|
27963
|
+
for (const p of plans) {
|
|
27964
|
+
const lit = literals.find((x) => x.name === p.name) ?? literalFor(p);
|
|
27965
|
+
if (lit?.key) planKeys.set(String(p.name), lit.key);
|
|
27966
|
+
if (lit?.price_ids?.length) planPriceIds.set(String(p.name), lit.price_ids);
|
|
27967
|
+
const known = initPlans.find((x) => Array.isArray(x.stripe_price_ids) && minorOf(x) === minorOf(p) && x.interval === p.interval && (Number(x.interval_count ?? 1) || 1) === (Number(p.interval_count ?? 1) || 1));
|
|
27968
|
+
if (known) planPriceIds.set(String(p.name), [...planPriceIds.get(String(p.name)) ?? [], ...known.stripe_price_ids]);
|
|
27390
27969
|
}
|
|
27391
27970
|
if (!plans.length) {
|
|
27392
27971
|
plans.push({ name: "TODO: plan name", price: "TODO: e.g. 19.99", currency: "usd", interval: "month" });
|
|
27393
27972
|
todos.push("plans: the price, interval and trial of each plan (the checkout uses Price ids, which cannot be read from code)");
|
|
27394
27973
|
}
|
|
27395
27974
|
const planNames = plans.map((p) => String(p.name));
|
|
27396
|
-
const
|
|
27397
|
-
|
|
27975
|
+
const oneOf = planNames.map((n) => JSON.stringify(n)).join(", ");
|
|
27976
|
+
let planTodo = false;
|
|
27977
|
+
const planFor = (kind = "entry") => {
|
|
27978
|
+
if (plans.length <= 1) return {};
|
|
27979
|
+
planTodo = true;
|
|
27980
|
+
return { plan: kind === "page" ? `TODO: "all" (the page shows every plan) or one of ${oneOf}` : `TODO: one of ${oneOf}` };
|
|
27981
|
+
};
|
|
27398
27982
|
const stripe = [];
|
|
27399
27983
|
const perFile = /* @__PURE__ */ new Map();
|
|
27400
27984
|
for (const s2 of det.checkoutSessions) {
|
|
@@ -27425,7 +28009,21 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27425
28009
|
} else if (s2.unresolved.length) found.push(`${s2.file}:${s2.line}: Checkout Session (${s2.unresolved.length} value(s) are expressions, read as-is)`);
|
|
27426
28010
|
else found.push(`${s2.file}:${s2.line}: Checkout Session`);
|
|
27427
28011
|
entry.strict = true;
|
|
27428
|
-
|
|
28012
|
+
const pair = pairCall(root, s2, plans, planPriceIds, det.checkoutSessions.length);
|
|
28013
|
+
if (pair.plan) {
|
|
28014
|
+
if (plans.length > 1) {
|
|
28015
|
+
names.delete(String(entry.name));
|
|
28016
|
+
entry.name = unique(names, `${String(entry.name).replace(/\)$/, "")}: ${pair.plan})`);
|
|
28017
|
+
}
|
|
28018
|
+
stripe.push({ ...entry, plan: pair.plan });
|
|
28019
|
+
if (pair.why) found.push(`${s2.file}:${s2.line}: sells plan "${pair.plan}" (${pair.why})`);
|
|
28020
|
+
} else if (pair.several) {
|
|
28021
|
+
for (const p of plans) stripe.push({ ...entry, name: unique(names, `${String(entry.name).replace(/\)$/, "")}: ${String(p.name)})`), plan: String(p.name) });
|
|
28022
|
+
names.delete(String(entry.name));
|
|
28023
|
+
} else if (plans.length > 1 && !plans.some((p) => String(p.name).startsWith("TODO"))) {
|
|
28024
|
+
stripe.push({ ...entry, plan: `TODO: which plan does this call sell? one of ${oneOf}` });
|
|
28025
|
+
todos.push(`${s2.file}:${s2.line}: which plan this Checkout call sells ("plan"); Checkbox could not tell from its Price id${pair.ref ? ` (${pair.ref})` : ""}`);
|
|
28026
|
+
} else stripe.push(entry);
|
|
27429
28027
|
}
|
|
27430
28028
|
const emails = [];
|
|
27431
28029
|
const pages = [];
|
|
@@ -27434,18 +28032,29 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27434
28032
|
if (IN_EMAIL_DIR.test(rel2)) {
|
|
27435
28033
|
const kind = emailKindOf(rel2);
|
|
27436
28034
|
if (/\.(?:html?|txt|md)$/i.test(rel2) && kind) {
|
|
27437
|
-
|
|
27438
|
-
|
|
28035
|
+
let text = "";
|
|
28036
|
+
try {
|
|
28037
|
+
text = readFileSync6(join5(root, rel2), "utf8");
|
|
28038
|
+
} catch {
|
|
28039
|
+
}
|
|
28040
|
+
const tags = Object.fromEntries(mergeTagsIn(text).flatMap((t) => mergeTagField(t) ? [[t, mergeTagField(t)]] : []));
|
|
28041
|
+
const planTagged = Object.values(tags).some((v) => v.startsWith("plan."));
|
|
28042
|
+
const hint = intervalHintOf(basename(rel2).replace(/\.[^.]+$/, ""));
|
|
28043
|
+
const byName = hint && plans.length > 1 ? plans.filter((p) => p.interval === hint && (Number(p.interval_count ?? 1) || 1) === 1) : [];
|
|
28044
|
+
const plan2 = plans.length > 1 && planTagged ? { plan: "all" } : byName.length === 1 ? { plan: String(byName[0].name) } : planFor();
|
|
28045
|
+
emails.push({ name: unique(names, `Email: ${basename(rel2)}`), kind, file: rel2, ...plan2, ...Object.keys(tags).length ? { merge_tags: tags } : {} });
|
|
28046
|
+
found.push(`${rel2}: ${kind} email${Object.keys(tags).length ? ` (its merge tags ${Object.keys(tags).map((t) => `{{${t}}}`).join(", ")} are filled from checkbox.json: see "merge_tags")` : ""}`);
|
|
27439
28047
|
return;
|
|
27440
28048
|
}
|
|
27441
28049
|
if (/\.[jt]sx$|\.mjml$/i.test(rel2) && kind) {
|
|
27442
|
-
|
|
28050
|
+
notes2.push(`${rel2}: a ${kind} email component; add it with "module": "<file>#<export>" returning the rendered { subject, html } (React Email: render() with sample props)`);
|
|
27443
28051
|
return;
|
|
27444
28052
|
}
|
|
27445
28053
|
}
|
|
27446
28054
|
if (PAGE_FILE.test(rel2)) {
|
|
27447
28055
|
const kind = /checkout|subscribe/i.test(rel2) ? "checkout" : "pricing";
|
|
27448
|
-
|
|
28056
|
+
const src = pageSource(root, rel2);
|
|
28057
|
+
pages.push({ name: unique(names, `Page: ${rel2}`), file: rel2, kind, ...planFor("page"), ...src ? { source: src } : {} });
|
|
27449
28058
|
found.push(`${rel2}: ${kind} page`);
|
|
27450
28059
|
return;
|
|
27451
28060
|
}
|
|
@@ -27462,9 +28071,24 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27462
28071
|
}
|
|
27463
28072
|
}
|
|
27464
28073
|
});
|
|
27465
|
-
|
|
27466
|
-
|
|
28074
|
+
for (const ps of pageSources(root)) {
|
|
28075
|
+
if (pages.some((p) => p.source === ps.source || p.file === ps.file)) continue;
|
|
28076
|
+
let text = "";
|
|
28077
|
+
try {
|
|
28078
|
+
text = readFileSync6(join5(root, ps.source), "utf8");
|
|
28079
|
+
} catch {
|
|
28080
|
+
}
|
|
28081
|
+
const kind = /checkout|subscribe/i.test(ps.route) ? "checkout" : "pricing";
|
|
28082
|
+
const shows = (p) => text.includes(String(p.name)) || typeof p.price === "number" && text.includes(`$${p.price}`) || typeof p.amount === "number" && text.includes(`$${p.amount / 100}`);
|
|
28083
|
+
const showsAll = plans.length > 1 && plans.every(shows);
|
|
28084
|
+
pages.push({ name: unique(names, `Page: ${ps.route}`), file: ps.file, source: ps.source, kind, ...showsAll ? { plan: "all" } : planFor("page") });
|
|
28085
|
+
found.push(`${ps.source}: ${kind} page (checked as the built ${ps.file}; the workflow builds it first)`);
|
|
28086
|
+
}
|
|
28087
|
+
const inPages = (f) => pages.some((p) => p.source === f);
|
|
28088
|
+
if (!disclosures.length && det.payButtons.length && !det.payButtons.some((b) => inPages(b.file))) {
|
|
28089
|
+
notes2.push(`The renewal disclosure lives in code (${det.payButtons[0].file}); keep its text in a .txt file the component imports and add it under checks.disclosures, or check the rendered page under checks.pages`);
|
|
27467
28090
|
}
|
|
28091
|
+
if (planTodo) todos.push('plan: pick the plan of each disclosure, page and email entry marked TODO ("all" for a page that shows every plan)');
|
|
27468
28092
|
const checks = {};
|
|
27469
28093
|
if (stripe.length) checks.stripe_checkout = stripe;
|
|
27470
28094
|
if (disclosures.length) checks.disclosures = disclosures;
|
|
@@ -27472,20 +28096,24 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27472
28096
|
if (emails.length) checks.emails = emails;
|
|
27473
28097
|
checks.notice_schedule = true;
|
|
27474
28098
|
if (!stripe.length && !disclosures.length && !pages.length && !emails.length) {
|
|
27475
|
-
|
|
28099
|
+
notes2.push('No checkout code, email templates or pages were found: add entries under "checks" (https://checkbox.site/docs/guides/ci#config)');
|
|
27476
28100
|
}
|
|
27477
28101
|
return {
|
|
27478
28102
|
config: { $schema: SCHEMA_URL, merchant, jurisdictions: "all", plans, checks },
|
|
27479
28103
|
todos,
|
|
27480
|
-
found
|
|
28104
|
+
found,
|
|
28105
|
+
notes: notes2,
|
|
28106
|
+
todoValues: countTodoValues({ merchant, plans, checks })
|
|
27481
28107
|
};
|
|
27482
28108
|
}
|
|
27483
|
-
var IGNORE, MAX_FILES2, EMAIL_NAMES, IN_EMAIL_DIR, PAGE_FILE, DISCLOSURE_TEXT, unique, PLAN_FILE;
|
|
28109
|
+
var PAGE_ROUTE, IGNORE, MAX_FILES2, EMAIL_NAMES, IN_EMAIL_DIR, PAGE_FILE, DISCLOSURE_TEXT, unique, PLAN_FILE;
|
|
27484
28110
|
var init_scaffold = __esm({
|
|
27485
28111
|
"src/ci/scaffold.ts"() {
|
|
27486
28112
|
"use strict";
|
|
27487
28113
|
init_detect();
|
|
28114
|
+
init_preflight();
|
|
27488
28115
|
init_config3();
|
|
28116
|
+
PAGE_ROUTE = /^(pricing|checkout|subscribe|plans)$/i;
|
|
27489
28117
|
IGNORE = /* @__PURE__ */ new Set(["node_modules", ".git", ".next", ".nuxt", ".svelte-kit", ".output", ".vercel", ".netlify", ".turbo", ".cache", "coverage", ".astro", "vendor", ".venv", "__pycache__", ".github"]);
|
|
27490
28118
|
MAX_FILES2 = 5e3;
|
|
27491
28119
|
EMAIL_NAMES = [
|
|
@@ -27737,7 +28365,7 @@ function moduleErrorMessage(stderr, dir, flags = []) {
|
|
|
27737
28365
|
const first2 = lines.find((l) => /^(?:[A-Z]\w*)?Error\b.*:/.test(l)) ?? lines.find((l) => /error/i.test(l)) ?? lines[lines.length - 1] ?? "";
|
|
27738
28366
|
return first2.replace(/file:\/\/\S+/g, (u) => rel2(u)).replace(/\s+/g, " ").slice(0, 400);
|
|
27739
28367
|
}
|
|
27740
|
-
function paramsFromSource(text, file, call = 1, env2 = {}) {
|
|
28368
|
+
function paramsFromSource(text, file, call = 1, env2 = {}, consts = {}) {
|
|
27741
28369
|
const calls = [...text.matchAll(/checkout\.sessions\.create\s*\(/g)];
|
|
27742
28370
|
if (!calls.length) throw new InputError(`${file} has no stripe.checkout.sessions.create( call`);
|
|
27743
28371
|
const m = calls[call - 1];
|
|
@@ -27745,7 +28373,7 @@ function paramsFromSource(text, file, call = 1, env2 = {}) {
|
|
|
27745
28373
|
const line2 = text.slice(0, m.index).split("\n").length;
|
|
27746
28374
|
const lit = sessionLiteral(text, m.index + m[0].length);
|
|
27747
28375
|
if (!lit) throw new InputError(`${file}:${line2}: the params are not an object literal; use "module" (a function returning the params) or "file" instead`);
|
|
27748
|
-
const parsed = parseObjectLiteral(lit, env2);
|
|
28376
|
+
const parsed = parseObjectLiteral(lit, env2, consts);
|
|
27749
28377
|
if (!parsed.value) throw new InputError(`${file}:${line2}: could not read the params object`);
|
|
27750
28378
|
return { params: parsed.value, line: line2, unresolved: parsed.unresolved, ...parsed.fromEnv ? { fromEnv: parsed.fromEnv } : {} };
|
|
27751
28379
|
}
|
|
@@ -27849,7 +28477,8 @@ async function stripeJob(loaded, e, o, root) {
|
|
|
27849
28477
|
} else {
|
|
27850
28478
|
const src = e.source;
|
|
27851
28479
|
job.inputs.push(src);
|
|
27852
|
-
const
|
|
28480
|
+
const srcText = readRel(dir, src);
|
|
28481
|
+
const r = paramsFromSource(srcText, src, e.call ?? 1, projectEnv(dir), constantsFor(dir, src, srcText));
|
|
27853
28482
|
job.target = { file: rel(loaded, root, src), line: r.line };
|
|
27854
28483
|
job.fromSource = true;
|
|
27855
28484
|
params = r.params;
|
|
@@ -27861,9 +28490,16 @@ async function stripeJob(loaded, e, o, root) {
|
|
|
27861
28490
|
}
|
|
27862
28491
|
}
|
|
27863
28492
|
if (!params || typeof params !== "object" || Array.isArray(params)) throw new InputError("the Checkout Session params must be a JSON object");
|
|
28493
|
+
const items0 = Array.isArray(params.line_items) ? params.line_items : [];
|
|
28494
|
+
if (!e.plan && (config.plans?.length ?? 0) > 1 && !items0.some((li) => li && typeof li === "object" && li.price_data)) {
|
|
28495
|
+
throw new InputError(`"${e.name}" does not say which plan it sells, and its price is a Price id or an expression Checkbox cannot read: add "plan": one of ${(config.plans ?? []).map((p) => JSON.stringify(p.name)).join(", ")} to this entry (one entry per plan when one call serves several plans).`);
|
|
28496
|
+
}
|
|
27864
28497
|
const merchant = planTerms(config, pickPlan(config, e.plan));
|
|
27865
28498
|
const drift = planDrift(params, pickPlan(config, e.plan));
|
|
27866
28499
|
if (drift) job.warnings.push(drift);
|
|
28500
|
+
const soldPlan = pickPlan(config, e.plan);
|
|
28501
|
+
const overrides = soldPlan ? planAsSold(params, soldPlan) : void 0;
|
|
28502
|
+
if (soldPlan && overrides) job.sold = { plan: soldPlan.name, overrides };
|
|
27867
28503
|
const items = Array.isArray(params.line_items) ? params.line_items : [];
|
|
27868
28504
|
if (items.length && items.every((li) => li && typeof li === "object" && li.price_data)) for (const k of ["price", "amount", "currency", "interval", "interval_count"]) delete merchant[k];
|
|
27869
28505
|
job.body = { params, ...Object.keys(merchant).length ? { merchant } : {}, ...jurisdictionsOf(config) };
|
|
@@ -27872,6 +28508,29 @@ async function stripeJob(loaded, e, o, root) {
|
|
|
27872
28508
|
}
|
|
27873
28509
|
return job;
|
|
27874
28510
|
}
|
|
28511
|
+
function planAsSold(params, plan2) {
|
|
28512
|
+
const sd = params.subscription_data ?? {};
|
|
28513
|
+
const trial = typeof sd.trial_period_days === "number" ? sd.trial_period_days : void 0;
|
|
28514
|
+
const out = {};
|
|
28515
|
+
if (trial !== void 0 && trial !== (plan2.trial_days ?? 0)) out.trial_days = trial;
|
|
28516
|
+
if (trial === void 0 && plan2.trial_days && plan2.trial_days > 0 && sd.trial_end === void 0) out.trial_days = 0;
|
|
28517
|
+
const items = Array.isArray(params.line_items) ? params.line_items : [];
|
|
28518
|
+
const pd = items.map((li) => li && typeof li === "object" ? li.price_data : void 0).find((x) => x && typeof x === "object");
|
|
28519
|
+
const rec = pd && typeof pd.recurring === "object" ? pd.recurring : void 0;
|
|
28520
|
+
if (rec && typeof rec.interval === "string" && rec.interval !== plan2.interval && ["day", "week", "month", "year"].includes(rec.interval)) out.interval = rec.interval;
|
|
28521
|
+
const price3 = typeof plan2.price === "number" ? plan2.price : typeof plan2.price === "string" && /^[\d.]+$/.test(plan2.price) ? Number(plan2.price) : void 0;
|
|
28522
|
+
if (pd && typeof pd.unit_amount === "number" && price3 !== void 0 && /^(?:usd|eur|gbp|cad|aud|nzd)$/i.test(String(pd.currency ?? plan2.currency ?? "usd")) && (!pd.currency || pd.currency === (plan2.currency ?? "usd")) && Math.abs(pd.unit_amount - Math.round(price3 * 100)) >= 1) {
|
|
28523
|
+
out.price = pd.unit_amount / 100;
|
|
28524
|
+
}
|
|
28525
|
+
return Object.keys(out).length ? out : void 0;
|
|
28526
|
+
}
|
|
28527
|
+
function describeSold(o) {
|
|
28528
|
+
const parts = [];
|
|
28529
|
+
if (o.trial_days !== void 0) parts.push(o.trial_days ? `a ${o.trial_days}-day trial` : "no trial");
|
|
28530
|
+
const charge = [o.price !== void 0 ? Number(o.price).toFixed(2) : "", o.interval ? `every ${o.interval}` : ""].filter(Boolean).join(" ");
|
|
28531
|
+
if (charge) parts.push(charge);
|
|
28532
|
+
return parts.join(", ");
|
|
28533
|
+
}
|
|
27875
28534
|
function planDrift(params, plan2) {
|
|
27876
28535
|
if (!plan2) return void 0;
|
|
27877
28536
|
const sd = params.subscription_data ?? {};
|
|
@@ -27887,7 +28546,7 @@ function planDrift(params, plan2) {
|
|
|
27887
28546
|
if (pd && typeof pd.unit_amount === "number" && price3 !== void 0 && /^(?:usd|eur|gbp|cad|aud|nzd)$/i.test(String(pd.currency ?? plan2.currency ?? "usd")) && (!pd.currency || pd.currency === (plan2.currency ?? "usd")) && Math.abs(pd.unit_amount - Math.round(price3 * 100)) >= 1) {
|
|
27888
28547
|
out.push(`the params charge ${(pd.unit_amount / 100).toFixed(2)}, plan "${plan2.name}" ${price3.toFixed(2)}`);
|
|
27889
28548
|
}
|
|
27890
|
-
return out.length ? `The params and checkbox.json disagree: ${out.join("; ")}. The
|
|
28549
|
+
return out.length ? `The params and checkbox.json disagree: ${out.join("; ")}. The Checkout check uses the params, and the emails, pages and disclosures of plan "${plan2.name}" are checked against both until they agree: update "plans" in checkbox.json to the plan you really sell.` : void 0;
|
|
27891
28550
|
}
|
|
27892
28551
|
function disclosureJob(loaded, e, root) {
|
|
27893
28552
|
const { config, dir } = loaded;
|
|
@@ -27960,6 +28619,53 @@ function pageJob(loaded, e, o, root) {
|
|
|
27960
28619
|
}
|
|
27961
28620
|
return job;
|
|
27962
28621
|
}
|
|
28622
|
+
function planMoney(plan2) {
|
|
28623
|
+
if (!plan2) return void 0;
|
|
28624
|
+
const cur = (plan2.currency ?? "usd").toUpperCase();
|
|
28625
|
+
const major = typeof plan2.amount === "number" ? plan2.amount / 100 : typeof plan2.price === "number" ? plan2.price : typeof plan2.price === "string" && /^[\d.]+$/.test(plan2.price) ? Number(plan2.price) : void 0;
|
|
28626
|
+
if (major === void 0) return void 0;
|
|
28627
|
+
try {
|
|
28628
|
+
return new Intl.NumberFormat("en-US", { style: "currency", currency: cur }).format(major);
|
|
28629
|
+
} catch {
|
|
28630
|
+
return `${major.toFixed(2)} ${cur}`;
|
|
28631
|
+
}
|
|
28632
|
+
}
|
|
28633
|
+
function fillMergeTags(msg2, tags, config, plan2) {
|
|
28634
|
+
const field = (v) => {
|
|
28635
|
+
if (!MERGE_TAG_FIELDS.includes(v)) return v;
|
|
28636
|
+
switch (v) {
|
|
28637
|
+
case "plan.price":
|
|
28638
|
+
case "plan.amount":
|
|
28639
|
+
return planMoney(plan2);
|
|
28640
|
+
case "plan.interval":
|
|
28641
|
+
return plan2 ? (plan2.interval_count ?? 1) > 1 ? `${plan2.interval_count} ${plan2.interval}s` : plan2.interval : void 0;
|
|
28642
|
+
case "plan.name":
|
|
28643
|
+
return plan2?.name;
|
|
28644
|
+
case "plan.trial_days":
|
|
28645
|
+
return plan2?.trial_days !== void 0 ? String(plan2.trial_days) : void 0;
|
|
28646
|
+
case "plan.currency":
|
|
28647
|
+
return (plan2?.currency ?? "usd").toUpperCase();
|
|
28648
|
+
case "merchant.name":
|
|
28649
|
+
return config.merchant?.name;
|
|
28650
|
+
case "merchant.cancel_url":
|
|
28651
|
+
return config.merchant?.cancel_url;
|
|
28652
|
+
case "merchant.support_contact":
|
|
28653
|
+
return config.merchant?.support_contact;
|
|
28654
|
+
}
|
|
28655
|
+
return void 0;
|
|
28656
|
+
};
|
|
28657
|
+
const values = /* @__PURE__ */ new Map();
|
|
28658
|
+
for (const [k, v] of Object.entries(tags)) {
|
|
28659
|
+
const val = field(v);
|
|
28660
|
+
if (val !== void 0) values.set(k.toLowerCase(), val);
|
|
28661
|
+
}
|
|
28662
|
+
const esc3 = (t) => t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
28663
|
+
const fill2 = (t, html) => t?.replace(/\{\{\{\s*([\w.-]+)\s*\}\}\}|\{\{\s*([\w.-]+)\s*\}\}|\*\|([\w.-]+)\|\*/g, (m, a, b, c) => {
|
|
28664
|
+
const v = values.get(String(a ?? b ?? c).toLowerCase());
|
|
28665
|
+
return v === void 0 ? m : html ? esc3(v) : v;
|
|
28666
|
+
});
|
|
28667
|
+
return { ...msg2.subject !== void 0 ? { subject: fill2(msg2.subject, false) } : {}, ...msg2.text !== void 0 ? { text: fill2(msg2.text, false) } : {}, ...msg2.html !== void 0 ? { html: fill2(msg2.html, true) } : {} };
|
|
28668
|
+
}
|
|
27963
28669
|
async function emailJob(loaded, e, o, root, planName = e.plan, name = e.name) {
|
|
27964
28670
|
const { config, dir } = loaded;
|
|
27965
28671
|
const job = { name, type: "email", ...planName ? { plan: planName } : {}, target: { file: rel(loaded, root, loaded.path), line: entryLine(loaded.raw, e.name) }, inputs: [], warnings: [] };
|
|
@@ -27980,6 +28686,8 @@ async function emailJob(loaded, e, o, root, planName = e.plan, name = e.name) {
|
|
|
27980
28686
|
msg2 = { ...typeof r.subject === "string" ? { subject: r.subject } : e.subject ? { subject: e.subject } : {}, ...typeof r.text === "string" ? { text: r.text } : {}, ...typeof r.html === "string" ? { html: r.html } : {} };
|
|
27981
28687
|
} else throw new InputError(`${e.module} must return { subject, text | html } or a string`);
|
|
27982
28688
|
} else msg2 = { subject: e.subject, ...e.text ? { text: e.text } : {}, ...e.html ? { html: e.html } : {} };
|
|
28689
|
+
const tags = { ...config.merge_tags ?? {}, ...e.merge_tags ?? {} };
|
|
28690
|
+
if (Object.keys(tags).length) msg2 = fillMergeTags(msg2, tags, config, pickPlan(config, planName));
|
|
27983
28691
|
if (!msg2.subject) throw new InputError(`no subject: add "subject" to the entry, a first line "Subject: \u2026" to the file, or an HTML <title>`);
|
|
27984
28692
|
if (!msg2.text && !msg2.html) throw new InputError("the email body is empty");
|
|
27985
28693
|
const terms2 = { ...planTerms(config, pickPlan(config, planName)) };
|
|
@@ -27993,9 +28701,35 @@ async function emailJob(loaded, e, o, root, planName = e.plan, name = e.name) {
|
|
|
27993
28701
|
}
|
|
27994
28702
|
async function resolveJobs(loaded, o) {
|
|
27995
28703
|
const root = o.root ?? loaded.dir;
|
|
28704
|
+
const jobs = [];
|
|
28705
|
+
for (const e of loaded.config.checks.stripe_checkout ?? []) jobs.push(await stripeJob(loaded, e, o, root));
|
|
28706
|
+
const dependent = await planJobs(loaded, o, root);
|
|
28707
|
+
jobs.push(...dependent);
|
|
28708
|
+
const seen = /* @__PURE__ */ new Set();
|
|
28709
|
+
for (const sj of jobs.filter((j) => j.type === "stripe_checkout" && j.sold)) {
|
|
28710
|
+
const key = JSON.stringify(sj.sold);
|
|
28711
|
+
if (seen.has(key)) continue;
|
|
28712
|
+
seen.add(key);
|
|
28713
|
+
const { plan: planName, overrides } = sj.sold;
|
|
28714
|
+
const config = { ...loaded.config, plans: (loaded.config.plans ?? []).map((p) => p.name === planName ? { ...p, ...overrides } : p) };
|
|
28715
|
+
const variant = await planJobs({ ...loaded, config }, o, root);
|
|
28716
|
+
const sold = describeSold(overrides);
|
|
28717
|
+
variant.forEach((v, i) => {
|
|
28718
|
+
const orig = dependent[i];
|
|
28719
|
+
if (orig.error || orig.skip || !v.body || JSON.stringify(v.body) === JSON.stringify(orig.body)) return;
|
|
28720
|
+
jobs.push({
|
|
28721
|
+
...v,
|
|
28722
|
+
name: `${orig.name} (as "${sj.name}" sells it: ${sold})`,
|
|
28723
|
+
inputs: [.../* @__PURE__ */ new Set([...v.inputs, ...sj.inputs])],
|
|
28724
|
+
warnings: [`The params and checkbox.json disagree: "${sj.name}" sells plan "${planName}" with ${sold}, which checkbox.json does not say. This check judges ${orig.name} against what the params sell; the same check against checkbox.json runs too, and both must pass. Update "plans" in checkbox.json (or the params) so they agree.`, ...v.warnings]
|
|
28725
|
+
});
|
|
28726
|
+
});
|
|
28727
|
+
}
|
|
28728
|
+
return jobs;
|
|
28729
|
+
}
|
|
28730
|
+
async function planJobs(loaded, o, root) {
|
|
27996
28731
|
const c = loaded.config.checks;
|
|
27997
28732
|
const jobs = [];
|
|
27998
|
-
for (const e of c.stripe_checkout ?? []) jobs.push(await stripeJob(loaded, e, o, root));
|
|
27999
28733
|
for (const e of c.disclosures ?? []) jobs.push(disclosureJob(loaded, e, root));
|
|
28000
28734
|
for (const e of c.pages ?? []) jobs.push(pageJob(loaded, e, o, root));
|
|
28001
28735
|
for (const e of c.emails ?? []) {
|
|
@@ -28379,6 +29113,7 @@ var init_timing = __esm({
|
|
|
28379
29113
|
var notice_schedule_exports = {};
|
|
28380
29114
|
__export(notice_schedule_exports, {
|
|
28381
29115
|
computeNoticeSchedule: () => computeNoticeSchedule,
|
|
29116
|
+
noticesKindsNote: () => noticesKindsNote,
|
|
28382
29117
|
singleDayNote: () => singleDayNote
|
|
28383
29118
|
});
|
|
28384
29119
|
function singleDayNote(requirementIds, days) {
|
|
@@ -28487,7 +29222,12 @@ function computeNoticeSchedule(input, now = /* @__PURE__ */ new Date()) {
|
|
|
28487
29222
|
if (t.governed) push("annual_terms", termMonths2 >= 12 ? "Yearly terms reminder (first anniversary; the renewal reminder can carry it)" : "Yearly terms reminder (first anniversary)", anniversary, t);
|
|
28488
29223
|
else notRequired.push({ kind: "annual_terms", reason: "No applicable requirement demands a yearly terms reminder under these jurisdictions." });
|
|
28489
29224
|
}
|
|
28490
|
-
if (!input.price_change) {
|
|
29225
|
+
if (!input.price_change && plan2.introAmount !== void 0 && plan2.introAmount < plan2.amount) {
|
|
29226
|
+
const stepUp = addInterval(firstCharge, plan2.interval, plan2.intervalCount * Math.max(1, plan2.introPeriods ?? 1));
|
|
29227
|
+
const t = timing("price_change", stepUp);
|
|
29228
|
+
if (t.governed) push("price_change", `Intro price ends: the regular price is first charged`, stepUp, t);
|
|
29229
|
+
else notRequired.push({ kind: "price_change", reason: "The intro price ends, but no applicable requirement demands a price-change notice for it under these jurisdictions." });
|
|
29230
|
+
} else if (!input.price_change) {
|
|
28491
29231
|
notRequired.push({ kind: "price_change", reason: "No price change planned. Pass price_change (effective_date, new_price) to schedule its advance notice." });
|
|
28492
29232
|
} else {
|
|
28493
29233
|
const eff = parseDate(input.price_change.effective_date, "price_change.effective_date", start);
|
|
@@ -28514,6 +29254,24 @@ function computeNoticeSchedule(input, now = /* @__PURE__ */ new Date()) {
|
|
|
28514
29254
|
}
|
|
28515
29255
|
};
|
|
28516
29256
|
}
|
|
29257
|
+
function noticesKindsNote(kinds, settings) {
|
|
29258
|
+
const scheduled = [...new Set(kinds)];
|
|
29259
|
+
if (!scheduled.length) return null;
|
|
29260
|
+
if (!settings || settings.mode !== "checkbox") {
|
|
29261
|
+
return {
|
|
29262
|
+
code: "notices_you_send",
|
|
29263
|
+
message: `This site's notices are in "You send" mode: your code sends ${scheduled.join(", ")} at these dates (Checkbox tracks them and can call your webhook when each is due). To have Checkbox send them: updateNoticeSettings { "mode": "checkbox", "add_kinds": ${JSON.stringify(scheduled)} }.`,
|
|
29264
|
+
param: "site"
|
|
29265
|
+
};
|
|
29266
|
+
}
|
|
29267
|
+
const off = scheduled.filter((k) => !settings.enabledKinds.includes(k));
|
|
29268
|
+
if (!off.length) return null;
|
|
29269
|
+
return {
|
|
29270
|
+
code: "notices_kinds_off",
|
|
29271
|
+
message: `Checkbox Notices does not send ${off.join(", ")} for this site yet: updateNoticeSettings { "add_kinds": ${JSON.stringify(off)} } turns them on (or your code sends them at these dates).`,
|
|
29272
|
+
param: "site"
|
|
29273
|
+
};
|
|
29274
|
+
}
|
|
28517
29275
|
var CITATION, BY_ID2, day, unix;
|
|
28518
29276
|
var init_notice_schedule = __esm({
|
|
28519
29277
|
"../../apps/api/src/v1/checks/notice-schedule.ts"() {
|
|
@@ -28554,8 +29312,15 @@ function operationFor2(kind) {
|
|
|
28554
29312
|
function onlineEngine(client) {
|
|
28555
29313
|
return { mode: "online", run: (kind, body) => client.sdk.call(operationFor2(kind), body) };
|
|
28556
29314
|
}
|
|
29315
|
+
function resultOf(status, exit, strict) {
|
|
29316
|
+
if (exit === 0) return status === "needs_review" ? "passed with items to review (they do not fail this run)" : "passed";
|
|
29317
|
+
if (status === "needs_review") return `failed: items need review and ${strict ? "--strict is on" : 'a "strict" entry or a blocking check makes them fail the run'} (exit ${exit})`;
|
|
29318
|
+
if (status === "skipped") return `failed: nothing was checked (exit ${exit})`;
|
|
29319
|
+
if (status === "fail") return `failed: fixes needed (exit ${exit})`;
|
|
29320
|
+
return `failed: could not run every check (exit ${exit})`;
|
|
29321
|
+
}
|
|
28557
29322
|
function ciVocabulary(text) {
|
|
28558
|
-
return text.replace(/Pass every plan the page sells \(plans: \[\.\.\.\]\)/g, 'Set "plan": "all" on this page entry in checkbox.json (every plan the page sells)').replace(/Pass plans with every plan the page sells/g, 'Set "plan": "all" on this page entry in checkbox.json').replace(/\(page_kind "checkout"\)/g, '(a pages entry with "kind": "checkout")').replace(/page_kind "checkout"/g, '"kind": "checkout"').replace(/check the renewal (text|text itself) with check_disclosure/g, 'check the renewal $1 with a "disclosures" entry').replace(/\bcheck_disclosure\b/g, 'a "disclosures" entry').replace(/pass merchant\.price \(e\.g\. 19\.99\), merchant\.currency and merchant\.interval/g, 'add the plan to "plans" in checkbox.json (price, currency, interval) and name it in "plan" on this entry').replace(/merchant\.price \+ merchant\.interval/g, 'a plan in "plans" of checkbox.json (and "plan" on this entry)').replace(/pass merchant\.price and merchant\.interval/gi, 'add the plan to "plans" in checkbox.json and name it in "plan" on this entry').replace(/merchant\.intro_percent_off \(or intro_amount_off\) and merchant\.intro_periods/g, "intro_percent_off (or intro_amount_off) and intro_periods on the plan in checkbox.json").replace(/merchant\.intro_\*/g, "the plan's intro_* in checkbox.json").replace(/merchant\.trial_days \((\d+)\)/g, "trial_days ($1) of the plan in checkbox.json").replace(/merchant\.price was ignored/g, "the plan's price in checkbox.json was ignored").replace(/In test mode, pass the page source as html\./g, 'With a test key, list the built page under "file" in checkbox.json instead of "url" (or run --online with a live key, ck_live_\u2026).');
|
|
29323
|
+
return text.replace(/Pass every plan the page sells \(plans: \[\.\.\.\]\)/g, 'Set "plan": "all" on this page entry in checkbox.json (every plan the page sells)').replace(/Pass plans with every plan the page sells/g, 'Set "plan": "all" on this page entry in checkbox.json').replace(/\(page_kind "checkout"\)/g, '(a pages entry with "kind": "checkout")').replace(/page_kind "checkout"/g, '"kind": "checkout"').replace(/check the renewal (text|text itself) with check_disclosure/g, 'check the renewal $1 with a "disclosures" entry').replace(/\bcheck_disclosure\b/g, 'a "disclosures" entry').replace(/pass merchant\.price \(e\.g\. 19\.99\), merchant\.currency and merchant\.interval/g, 'add the plan to "plans" in checkbox.json (price, currency, interval) and name it in "plan" on this entry').replace(/merchant\.price \+ merchant\.interval/g, 'a plan in "plans" of checkbox.json (and "plan" on this entry)').replace(/pass merchant\.price and merchant\.interval/gi, 'add the plan to "plans" in checkbox.json and name it in "plan" on this entry').replace(/merchant\.intro_percent_off \(or intro_amount_off\) and merchant\.intro_periods/g, "intro_percent_off (or intro_amount_off) and intro_periods on the plan in checkbox.json").replace(/merchant\.intro_\*/g, "the plan's intro_* in checkbox.json").replace(/merchant\.trial_days \((\d+)\)/g, "trial_days ($1) of the plan in checkbox.json").replace(/merchant\.price was ignored/g, "the plan's price in checkbox.json was ignored").replace(/comes from merchant\.price and is not verified against the Price\. Connect Stripe on the site, or register the params with createStripeCheckoutConfig on a site whose Stripe account has this Price\./g, "comes from the plan's price in checkbox.json and is not verified against the Price. Connect Stripe on the site in Checkbox, then run checkbox ci --online (CHECKBOX_SECRET_KEY) so Checkbox reads the Price.").replace(/comes from merchant\.price\b/g, "comes from the plan's price in checkbox.json").replace(/\bmerchant\.price says\b/g, "The plan's price in checkbox.json says").replace(/\(or pass merchant\.cancel_url\)/g, "(or set merchant.cancel_url in checkbox.json)").replace(/to confirm, check the email again with sample values in their place \(keep the merge tags in your code\)\./g, `give each tag its value under "merge_tags" in checkbox.json (on this entry, or top level for every email), e.g. "merge_tags": { "price": "plan.price", "interval": "plan.interval" }: Checkbox fills them from the entry's plan before checking, and your template keeps its tags.`).replace(/In test mode, pass the page source as html\./g, 'With a test key, list the built page under "file" in checkbox.json instead of "url" (or run --online with a live key, ck_live_\u2026).');
|
|
28559
29324
|
}
|
|
28560
29325
|
function pricingPageFixes(fixes) {
|
|
28561
29326
|
const reworded = fixes.map(
|
|
@@ -28565,10 +29330,18 @@ function pricingPageFixes(fixes) {
|
|
|
28565
29330
|
const renewalLineFor = /* @__PURE__ */ new Set();
|
|
28566
29331
|
for (const f of reworded) {
|
|
28567
29332
|
const card = /^(Plan card [^:]+): /.exec(f)?.[1] ?? "";
|
|
28568
|
-
const isRenewalLine = /\b(?:Add one line under the price|Put the renewal line under the price|Move the renewal line under the price)/.test(f);
|
|
29333
|
+
const isRenewalLine = /\b(?:Add one line under the price|Put the renewal line under the price|Move the renewal line under the price|Add your own renewal line next to the (?:pay|plan's) button)/.test(f);
|
|
28569
29334
|
if (isRenewalLine) {
|
|
28570
|
-
|
|
28571
|
-
renewalLineFor.
|
|
29335
|
+
const cards = [...card.matchAll(/“([^”]+)”/g)].map((m) => m[1]);
|
|
29336
|
+
const fresh = cards.filter((c) => !renewalLineFor.has(c));
|
|
29337
|
+
if (cards.length ? !fresh.length : renewalLineFor.has(card)) continue;
|
|
29338
|
+
if (!cards.length) renewalLineFor.add(card);
|
|
29339
|
+
for (const c of fresh) renewalLineFor.add(c);
|
|
29340
|
+
if (cards.length && fresh.length < cards.length) {
|
|
29341
|
+
const g = `Plan card ${fresh.map((c) => `\u201C${c}\u201D`).join(", ")}: ${f.slice(card.length + 2)}`;
|
|
29342
|
+
if (!out.includes(g)) out.push(g);
|
|
29343
|
+
continue;
|
|
29344
|
+
}
|
|
28572
29345
|
}
|
|
28573
29346
|
if (!out.includes(f)) out.push(f);
|
|
28574
29347
|
}
|
|
@@ -28594,12 +29367,23 @@ function fixesOf(r, onlyNeedsFix = false) {
|
|
|
28594
29367
|
failing.sort((a, b) => (a.compliance_status === "needs_fix" ? 0 : 1) - (b.compliance_status === "needs_fix" ? 0 : 1) || (RANK2[a.status] ?? 2) - (RANK2[b.status] ?? 2));
|
|
28595
29368
|
for (const c of failing) {
|
|
28596
29369
|
const base = c.compliance?.fix || c.fix || c.compliance?.question?.text || c.explanation || c.title || c.check_id;
|
|
28597
|
-
|
|
29370
|
+
let cards = failingCards(c.explanation);
|
|
29371
|
+
const plain = cards.length > 1 ? cardClauses(c.explanation).filter((x) => /look like the surrounding text/.test(x.clause)).map((x) => x.card) : [];
|
|
29372
|
+
if (plain.length && plain.length < cards.length && /renewal line/i.test(base)) {
|
|
29373
|
+
cards = cards.filter((x) => !plain.includes(x));
|
|
29374
|
+
const standOut = `Plan card ${plain.map((x) => `\u201C${x}\u201D`).join(", ")}: ${STAND_OUT_DECISION}`;
|
|
29375
|
+
if (!onlyNeedsFix && !out.includes(standOut)) out.push(standOut);
|
|
29376
|
+
}
|
|
28598
29377
|
const text = cards.length && base ? `Plan card ${cards.map((x) => `\u201C${x}\u201D`).join(", ")}: ${base}` : base;
|
|
28599
29378
|
if (text && !out.includes(text)) out.push(text);
|
|
28600
29379
|
}
|
|
28601
29380
|
return out;
|
|
28602
29381
|
}
|
|
29382
|
+
function cardClauses(explanation) {
|
|
29383
|
+
if (!explanation) return [];
|
|
29384
|
+
const ms = [...explanation.matchAll(/plan button “([^”]{1,80})”(?: in the card “([^”]{1,80})”)?/g)];
|
|
29385
|
+
return ms.map((m, i) => ({ card: m[2] ?? m[1], clause: explanation.slice(m.index + m[0].length, ms[i + 1]?.index ?? explanation.length) }));
|
|
29386
|
+
}
|
|
28603
29387
|
function failingCards(explanation) {
|
|
28604
29388
|
if (!explanation) return [];
|
|
28605
29389
|
if (/\(\d+ checkout page views? checked\) — e\.g\./.test(explanation)) return [];
|
|
@@ -28616,7 +29400,7 @@ function withPlaceholders(params, placeholders) {
|
|
|
28616
29400
|
}
|
|
28617
29401
|
return copy;
|
|
28618
29402
|
}
|
|
28619
|
-
function correctedOf(type, r, partial = false, placeholders) {
|
|
29403
|
+
function correctedOf(type, r, partial = false, placeholders, pricingPage = false) {
|
|
28620
29404
|
if (type === "stripe_checkout" && r.correction?.patch?.length && partial) {
|
|
28621
29405
|
return { format: "json", title: `Changes to make to the params (JSON Patch; keep your other values)${r.correction.verification?.passes ? ", verified by the rules engine" : ""}`, body: JSON.stringify(r.correction.patch, null, 2) };
|
|
28622
29406
|
}
|
|
@@ -28629,8 +29413,10 @@ function correctedOf(type, r, partial = false, placeholders) {
|
|
|
28629
29413
|
return {
|
|
28630
29414
|
format: "text",
|
|
28631
29415
|
title: `Suggested replacement per plan, each directly above its own plan's button${all ? " (each passes the checks, verified by the rules engine)" : ""}`,
|
|
29416
|
+
// Round 32 (pilot 31, ci-pr-agent): a pricing page links to the checkout, where consent is asked (Stripe Checkout
|
|
29417
|
+
// shows its own checkbox), so no consent checkbox label is suggested for it.
|
|
28632
29418
|
body: perPlan.map((p) => `${p.plan}:
|
|
28633
|
-
${p.disclosure_text}${p.consent_label ? `
|
|
29419
|
+
${p.disclosure_text}${p.consent_label && !pricingPage ? `
|
|
28634
29420
|
Consent checkbox label: ${p.consent_label}` : ""}`).join("\n\n")
|
|
28635
29421
|
};
|
|
28636
29422
|
}
|
|
@@ -28647,7 +29433,8 @@ Consent checkbox label: ${r.replacement.consent_label}` : ""}`
|
|
|
28647
29433
|
};
|
|
28648
29434
|
}
|
|
28649
29435
|
if (type === "email" && r.template && ciStatusOf(r) !== "pass" && r.verdict !== "not_applicable") {
|
|
28650
|
-
|
|
29436
|
+
const toFill = r.template.placeholders ?? [];
|
|
29437
|
+
return { format: "text", title: `Suggested template${r.template.verified ? toFill.length ? ` (passes the checks once you replace ${toFill.join(", ")} with your real value${toFill.length === 1 ? "" : "s"}${toFill.includes("{{support_contact}}") ? ", or set merchant.support_contact in checkbox.json" : ""})` : " (passes the checks, verified by the rules engine)" : ""}`, body: `Subject: ${r.template.subject}
|
|
28651
29438
|
|
|
28652
29439
|
${r.template.text}` };
|
|
28653
29440
|
}
|
|
@@ -28691,13 +29478,20 @@ function noticeCoverage(config, planName, schedule, opts = {}) {
|
|
|
28691
29478
|
const required = /* @__PURE__ */ new Map();
|
|
28692
29479
|
for (const n of schedule.notices ?? []) {
|
|
28693
29480
|
const k = NOTICE_TO_EMAIL[n.kind];
|
|
28694
|
-
if (k && !required.has(k)) required.set(k, { occasion: n.occasion, days: n.days_before });
|
|
28695
|
-
}
|
|
29481
|
+
if (k && !required.has(k)) required.set(k, { occasion: n.occasion, days: n.days_before, citations: n.citations ?? [], jurisdictions: n.jurisdictions ?? [] });
|
|
29482
|
+
}
|
|
29483
|
+
const law = (w) => {
|
|
29484
|
+
if (!w.citations.length) return "";
|
|
29485
|
+
const where2 = w.jurisdictions.map((j) => j.replace(/^US-/, "")).filter((j) => j !== "FED");
|
|
29486
|
+
const fed = w.jurisdictions.includes("US-FED");
|
|
29487
|
+
const places = [...fed ? ["federal"] : [], ...where2];
|
|
29488
|
+
return ` Required by ${w.citations.slice(0, 2).join("; ")}${w.citations.length > 2 ? ` and ${w.citations.length - 2} more` : ""}${places.length ? ` (${places.length > 4 ? `${places.slice(0, 4).join(", ")} and ${places.length - 4} more` : places.join(", ")})` : ""}.`;
|
|
29489
|
+
};
|
|
28696
29490
|
const missing = [...required].filter(([k]) => !have.has(k));
|
|
28697
29491
|
if (!required.size) return { status: "not_applicable", label: "No reminder emails required", fixes: [] };
|
|
28698
29492
|
const sent2 = [...required.keys()].filter((k) => byCheckbox.has(k)).length;
|
|
28699
29493
|
if (!missing.length) return { status: "pass", label: `Checks pass \xB7 ${required.size} required email${required.size === 1 ? "" : "s"} covered${sent2 ? ` (${sent2} sent by Checkbox Notices, ${real ? "as the site's Notice settings confirm" : "as checkbox.json declares; --online confirms it against the site"})` : ""}`, fixes: [] };
|
|
28700
|
-
const fixes = missing.map(([k, w]) => `Add ${/^[aeiou]/i.test(EMAIL_LABEL[k]) ? "an" : "a"} ${EMAIL_LABEL[k]} to checks.emails (kind "${k}"; ${w.days > 0 ? `${w.days} days before: ${w.occasion}` : w.occasion}), or have Checkbox Notices send it${declared.includes(k) ? " (checkbox.json says it does, but the site's Notice settings do not send it)" : ""}
|
|
29494
|
+
const fixes = missing.map(([k, w]) => `Add ${/^[aeiou]/i.test(EMAIL_LABEL[k]) ? "an" : "a"} ${EMAIL_LABEL[k]} to checks.emails (kind "${k}"; ${w.days > 0 ? `${w.days} days before: ${w.occasion}` : w.occasion}), or have Checkbox Notices send it${declared.includes(k) ? " (checkbox.json says it does, but the site's Notice settings do not send it)" : ` ("notices": { "sent_by_checkbox": true } in checkbox.json once Notices is on for the site)`}.${law(w)}`);
|
|
28701
29495
|
return { status: "needs_review", label: `${missing.length} required email${missing.length === 1 ? "" : "s"} not in checkbox.json`, fix: fixes[0], fixes };
|
|
28702
29496
|
}
|
|
28703
29497
|
function unverifiedOffer(r) {
|
|
@@ -28724,6 +29518,11 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
28724
29518
|
return { ...base, status: "error", label: "Could not run", message: `${ciVocabulary(e.message)}${e.param && !failure && /^merchant\./.test(e.param) ? "" : e.param ? ` (${e.param})` : ""}`, fixes: [], ...failure ? { failure } : {} };
|
|
28725
29519
|
}
|
|
28726
29520
|
if (job.type === "notice_schedule") return { ...base, ...noticeCoverage(config, job.plan, report, opts), report };
|
|
29521
|
+
const pairing = (report.warnings ?? []).find((w) => w.code === "price_plan_conflict");
|
|
29522
|
+
if (pairing) {
|
|
29523
|
+
const names = (config.plans ?? []).map((p) => JSON.stringify(p.name)).join(", ");
|
|
29524
|
+
return { ...base, status: "error", label: "Could not run \xB7 plan pairing", message: `${pairing.message.replace(/\. Checkbox offers no corrected params[\s\S]*$/, "")}. Set "plan" on "${job.name}" to the plan this call really sells (one of ${names}); Checkbox shows no fix until the pairing agrees, because a fix would write the other plan's price into this checkout.`, fixes: [], report };
|
|
29525
|
+
}
|
|
28727
29526
|
const alerts = job.warnings.filter((w) => /^The params and checkbox\.json disagree/.test(w));
|
|
28728
29527
|
for (const w of report.warnings ?? []) {
|
|
28729
29528
|
const text = ciVocabulary(w.message);
|
|
@@ -28734,6 +29533,12 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
28734
29533
|
const status = ciStatusOf(report);
|
|
28735
29534
|
const params = job.type === "stripe_checkout" ? job.body?.params ?? void 0 : void 0;
|
|
28736
29535
|
let fixes = (status === "pass" || status === "not_applicable" ? [] : fixesOf(report)).map((f) => ciVocabulary(job.type === "stripe_checkout" ? stripeConsentFix(f, params) : f));
|
|
29536
|
+
const tagsLeft = job.type === "email" && status !== "pass" ? (report.warnings ?? []).find((w) => w.code === "unresolved_placeholders") : void 0;
|
|
29537
|
+
if (tagsLeft) {
|
|
29538
|
+
const tags = [...new Set(tagsLeft.message.match(/\{\{\{?\s*[\w.-]+\s*\}?\}\}|\*\|[\w.-]+\|\*/g) ?? [])];
|
|
29539
|
+
const sample = tags.slice(0, 3).map((t) => t.replace(/[{}*|\s]/g, "")).map((t) => `"${t}": "${/price|amount|cost/i.test(t) ? "plan.price" : /interval|period|frequency|cycle/i.test(t) ? "plan.interval" : /trial/i.test(t) ? "plan.trial_days" : /plan/i.test(t) ? "plan.name" : /cancel/i.test(t) ? "merchant.cancel_url" : "a sample value"}"`);
|
|
29540
|
+
fixes.unshift(`Checkbox cannot read the terms behind ${tags.join(", ")}. Add "merge_tags": { ${sample.join(", ")} } to this entry in checkbox.json (plan.price, plan.interval, \u2026 take the entry's plan), then run checkbox ci again; no render module is needed.`);
|
|
29541
|
+
}
|
|
28737
29542
|
const pricingPage = job.type === "page" && job.body?.page_kind === "pricing";
|
|
28738
29543
|
if (pricingPage) fixes = pricingPageFixes(fixes);
|
|
28739
29544
|
const unreadText = (job.unresolved ?? []).filter((p) => /^(?:custom_text|consent_collection|discounts|subscription_data|allow_promotion_codes|payment_method_collection)\b/.test(p));
|
|
@@ -28768,18 +29573,29 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
28768
29573
|
engine.mode === "offline" ? `The intro price is declared in checkbox.json, not verified: offline, Checkbox cannot read ${what} from Stripe. Run with --online (CHECKBOX_SECRET_KEY, and the site's Stripe key connected) so Checkbox reads it and compares its percent / amount off and duration with the declared intro.` : `The intro price is declared in checkbox.json, not verified: Checkbox could not read ${what} through the site's connected Stripe key (is the site known, via --site or CHECKBOX_SITE, and does the key have Coupons: Read?). Until it is read, its percent / amount off and duration are not compared with the declared intro.`
|
|
28769
29574
|
);
|
|
28770
29575
|
}
|
|
28771
|
-
const
|
|
28772
|
-
const
|
|
29576
|
+
const sharedReview = !unread && !declaredCoupon && status === "pass" && job.type === "stripe_checkout" ? stripeParamsVerdict(report) : void 0;
|
|
29577
|
+
const unreadCoupon = sharedReview?.verdict === "needs_review" && sharedReview.reason ? sharedReview.reason : void 0;
|
|
29578
|
+
if (unreadCoupon) {
|
|
29579
|
+
const id = /discounts use (coupon|promotion code) (\S+?),/.exec(unreadCoupon);
|
|
29580
|
+
const what = id ? `${id[1]} ${id[2]}` : "the coupon";
|
|
29581
|
+
fixes.unshift(
|
|
29582
|
+
engine.mode === "offline" ? `The params apply ${what}, which Checkbox cannot read offline, so its discount and duration (what the shopper is charged first) are not checked. Declare the intro on the plan in checkbox.json (intro_percent_off, intro_amount_off or intro_price, with intro_periods) and state it in custom_text.submit, or run with --online so Checkbox reads the coupon.` : `The params apply ${what}, which Checkbox could not read through the site's connected Stripe key (is the site known, via --site or CHECKBOX_SITE, and does the key have Coupons: Read?), so its discount and duration are not checked. Declare the intro on the plan in checkbox.json and state it in custom_text.submit, or let Checkbox read the coupon.`
|
|
29583
|
+
);
|
|
29584
|
+
}
|
|
29585
|
+
const finalStatus = unread ? literal.length ? "fail" : "needs_review" : declaredCoupon || unreadCoupon ? "needs_review" : status;
|
|
29586
|
+
const corrected = unread ? void 0 : correctedOf(job.type, report, job.partial, job.placeholders, pricingPage);
|
|
28773
29587
|
if (corrected?.format === "json" && job.placeholders) {
|
|
28774
29588
|
const unset = job.warnings.filter((w) => /(?:was|were) not set when Checkbox ran/.test(w));
|
|
28775
29589
|
if (unset.length) Object.assign(base, { alerts: [...unset, ...base.alerts ?? []] });
|
|
28776
29590
|
}
|
|
28777
|
-
const blocking = unread || status === "needs_review" && (job.type === "page" || job.type === "disclosure") && (report.compliance_status === "incomplete" || report.compliance_status === "not_checked") ||
|
|
29591
|
+
const blocking = unread || status === "needs_review" && (job.type === "page" || job.type === "disclosure") && (report.compliance_status === "incomplete" || report.compliance_status === "not_checked") || // Round 31 (pilot 30 item 2, headless-agent-pr false pass: a "strict" entry with a declared-but-unverified coupon exited
|
|
29592
|
+
// 0): the entry's strictness applies to the status the check reports (finalStatus), not the engine's raw verdict.
|
|
29593
|
+
finalStatus === "needs_review" && job.type === "stripe_checkout" && (!!job.strict || !!offer);
|
|
28778
29594
|
return {
|
|
28779
29595
|
...base,
|
|
28780
29596
|
status: finalStatus,
|
|
28781
29597
|
...blocking ? { blocking: true } : {},
|
|
28782
|
-
label: unread ? `${literal.length ? `${literal.length} fix${literal.length === 1 ? "" : "es"} needed \xB7 ` : ""}Not checked: ${unreadText[0]} is an expression` : declaredCoupon ? "Needs review \xB7 coupon declared, not verified" : countedLabel(report.compliance?.label || DEFAULT_LABEL[status], report, pricingPage ? (f) => pricingPageFixes(f.map(ciVocabulary)) : void 0),
|
|
29598
|
+
label: unread ? `${literal.length ? `${literal.length} fix${literal.length === 1 ? "" : "es"} needed \xB7 ` : ""}Not checked: ${unreadText[0]} is an expression` : declaredCoupon ? "Needs review \xB7 coupon declared, not verified" : unreadCoupon ? "Needs review \xB7 coupon not read" : countedLabel(report.compliance?.label || DEFAULT_LABEL[status], report, pricingPage ? (f) => pricingPageFixes(f.map(ciVocabulary)) : void 0),
|
|
28783
29599
|
...fixes[0] ? { fix: fixes[0] } : {},
|
|
28784
29600
|
fixes,
|
|
28785
29601
|
...corrected ? { corrected } : {},
|
|
@@ -28847,10 +29663,11 @@ async function runCi(jobs, engine, opts) {
|
|
|
28847
29663
|
checks,
|
|
28848
29664
|
...versioned?.rules_version ? { rules_version: versioned.rules_version } : {},
|
|
28849
29665
|
...versioned?.catalog_version ? { catalog_version: versioned.catalog_version } : {},
|
|
28850
|
-
disclaimer: CI_DISCLAIMER
|
|
29666
|
+
disclaimer: CI_DISCLAIMER,
|
|
29667
|
+
result: resultOf(status, exit, opts.strict)
|
|
28851
29668
|
};
|
|
28852
29669
|
}
|
|
28853
|
-
var ENGINE_KIND, CI_DISCLAIMER, RANK2, OK_STATUSES, NOTICE_TO_EMAIL, EMAIL_LABEL, DEFAULT_LABEL, ALERT_CODES;
|
|
29670
|
+
var ENGINE_KIND, STRIPE_OBJECTS_OFFLINE_NOTICE, CI_DISCLAIMER, RANK2, OK_STATUSES, STAND_OUT_DECISION, NOTICE_TO_EMAIL, EMAIL_LABEL, DEFAULT_LABEL, ALERT_CODES;
|
|
28854
29671
|
var init_runner = __esm({
|
|
28855
29672
|
"src/ci/runner.ts"() {
|
|
28856
29673
|
"use strict";
|
|
@@ -28858,6 +29675,7 @@ var init_runner = __esm({
|
|
|
28858
29675
|
init_http();
|
|
28859
29676
|
init_src();
|
|
28860
29677
|
init_types2();
|
|
29678
|
+
init_stripe_verdict();
|
|
28861
29679
|
init_config3();
|
|
28862
29680
|
ENGINE_KIND = {
|
|
28863
29681
|
stripe_checkout: "stripe_checkout_session",
|
|
@@ -28866,9 +29684,11 @@ var init_runner = __esm({
|
|
|
28866
29684
|
email: "email",
|
|
28867
29685
|
notice_schedule: "notice_schedule"
|
|
28868
29686
|
};
|
|
29687
|
+
STRIPE_OBJECTS_OFFLINE_NOTICE = "Stripe objects (Payment Links, and the trial or price set on a Stripe Price) are not checked offline: this change touches a trial, price or interval, so run checkbox ci --online (CHECKBOX_SECRET_KEY, with Stripe connected on the site) to check that Stripe sells the same terms.";
|
|
28869
29688
|
CI_DISCLAIMER = "Automated check against Checkbox's catalog of automatic-renewal laws; not legal advice.";
|
|
28870
29689
|
RANK2 = { fail: 0, needs_review: 1 };
|
|
28871
29690
|
OK_STATUSES = /* @__PURE__ */ new Set(["compliant", "pending_live_check", "verified_live", "not_applicable"]);
|
|
29691
|
+
STAND_OUT_DECISION = "Decision for you (not a fix): its renewal line is there and legible but looks like the text around it. California asks for renewal terms that stand out, so consider larger type, bold (weight 600 or more), a contrasting colour, or a border.";
|
|
28872
29692
|
NOTICE_TO_EMAIL = {
|
|
28873
29693
|
acknowledgment: "acknowledgment",
|
|
28874
29694
|
trial_ending: "trial_reminder",
|
|
@@ -28913,21 +29733,28 @@ function renderHuman(run2, c, opts = {}) {
|
|
|
28913
29733
|
const lines = [];
|
|
28914
29734
|
const versions = run2.rules_version ? ` \xB7 rules ${run2.rules_version} \xB7 catalog ${run2.catalog_version}` : "";
|
|
28915
29735
|
lines.push(`${c.bold("Checkbox CI")} ${run2.config} \xB7 ${run2.mode}${versions} \xB7 ${run2.checks.length} check${run2.checks.length === 1 ? "" : "s"}`);
|
|
29736
|
+
const unreadPrices = [];
|
|
28916
29737
|
const width = Math.min(40, Math.max(...run2.checks.map((x) => x.name.length), 10));
|
|
28917
29738
|
for (const x of run2.checks) {
|
|
28918
29739
|
const label = x.status === "fail" || x.status === "error" ? c.red(x.label) : x.status === "needs_review" ? c.yellow(x.label) : x.status === "pass" ? c.green(x.label) : c.dim(x.label);
|
|
28919
29740
|
lines.push(` ${icon(x.status, c)} ${x.name.padEnd(width)} \u2192 ${label}`);
|
|
28920
29741
|
if (x.message) lines.push(c.dim(` ${wrap(x.message, 110, " ")}`));
|
|
28921
29742
|
if (x.fix) lines.push(` fix: ${wrap(x.fix, 110, " ")}${c.dim(` (${where(x)})`)}`);
|
|
28922
|
-
if (opts.verbose) for (const f of x.fixes.slice(1)) lines.push(` fix: ${wrap(f, 110, " ")}`);
|
|
29743
|
+
if (opts.verbose) for (const f of x.fixes.slice(1)) lines.push(/^Plan card [^:]+: Decision for you\b/.test(f) ? ` decision: ${wrap(f.replace(/Decision for you \(not a fix\): /, ""), 110, " ")}` : ` fix: ${wrap(f, 110, " ")}`);
|
|
28923
29744
|
const alerts = x.alerts ?? [];
|
|
28924
29745
|
for (const a of alerts) lines.push(c.yellow(` warning: ${wrap(a, 110, " ")}`));
|
|
28925
|
-
for (const w of x.warnings.filter((w2) => !alerts.includes(w2)).slice(0, opts.verbose ? 10 : alerts.length ? 0 : 1)) lines.push(c.dim(` note: ${wrap(w, 110, " ")}`));
|
|
29746
|
+
for (const w of x.warnings.filter((w2) => !alerts.includes(w2) && !PRICE_UNREAD.test(w2)).slice(0, opts.verbose ? 10 : alerts.length ? 0 : 1)) lines.push(c.dim(` note: ${wrap(w, 110, " ")}`));
|
|
29747
|
+
for (const w of x.warnings) {
|
|
29748
|
+
const id = PRICE_UNREAD.exec(w)?.[1];
|
|
29749
|
+
if (id && !unreadPrices.includes(id)) unreadPrices.push(id);
|
|
29750
|
+
}
|
|
28926
29751
|
if (x.corrected && opts.verbose) lines.push(c.dim(` ${x.corrected.title}:`), ...x.corrected.body.split("\n").map((l) => c.dim(` ${l}`)));
|
|
28927
29752
|
}
|
|
29753
|
+
if (unreadPrices.length) lines.push("", c.dim(wrap(`note: ${unreadPrices.length === 1 ? `Price ${unreadPrices[0]} was` : `Prices ${unreadPrices.join(", ")} were`} not read (no connected Stripe account has ${unreadPrices.length === 1 ? "it" : "them"}), so the amounts checked come from "plans" in checkbox.json. To check them against Stripe, connect Stripe on the site in Checkbox and run checkbox ci --online.`, 110, " ")));
|
|
28928
29754
|
const tone = run2.status === "pass" ? c.green : run2.status === "needs_review" && run2.exit_code === 0 ? c.yellow : c.red;
|
|
28929
29755
|
if (run2.status === "skipped") lines.push("", c.red(wrap(nothingCheckedMessage(run2), 110, "")));
|
|
28930
|
-
lines.push("",
|
|
29756
|
+
if (run2.offline_notice) lines.push("", c.yellow(wrap(`Not checked offline: ${run2.offline_notice}`, 110, " ")));
|
|
29757
|
+
lines.push("", `${tone(overallLine(run2))} \xB7 exit ${run2.exit_code}${run2.result ? ` (${run2.result.replace(/ \(exit \d+\)$/, "")})` : ""}`);
|
|
28931
29758
|
if (run2.checks.some((x) => x.corrected) && !opts.verbose) lines.push(c.dim("Corrected params / text: --verbose, --json, or the job summary in GitHub Actions."));
|
|
28932
29759
|
lines.push(c.dim(run2.disclaimer));
|
|
28933
29760
|
return lines;
|
|
@@ -28952,6 +29779,8 @@ function stepSummary(run2) {
|
|
|
28952
29779
|
const versions = run2.rules_version ? ` \xB7 rules ${run2.rules_version} \xB7 catalog ${run2.catalog_version}` : "";
|
|
28953
29780
|
lines.push(`**${overallLine(run2)}** \u2014 \`${run2.config}\` \xB7 ${run2.mode}${versions}`, "");
|
|
28954
29781
|
if (run2.status === "skipped") lines.push(`\u274C ${nothingCheckedMessage(run2)}`, "");
|
|
29782
|
+
if (run2.result) lines.push(`Result: ${run2.result}.`, "");
|
|
29783
|
+
if (run2.offline_notice) lines.push(`\u26A0\uFE0F ${run2.offline_notice}`, "");
|
|
28955
29784
|
lines.push("| Check | Status | Fix | File |", "|---|---|---|---|");
|
|
28956
29785
|
for (const x of run2.checks) {
|
|
28957
29786
|
const fix0 = x.status === "error" || x.status === "skipped" ? [x.message, x.fix].filter(Boolean).join(" ") : x.fix ? `${x.fix}${x.fixes.length > 1 ? ` (+${x.fixes.length - 1} more)` : ""}` : x.status === "needs_review" && x.message ? x.message : "";
|
|
@@ -28974,7 +29803,7 @@ function stepSummary(run2) {
|
|
|
28974
29803
|
function escapeHtml(s2) {
|
|
28975
29804
|
return s2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
28976
29805
|
}
|
|
28977
|
-
var TYPE_LABEL, where, escData, escProp, cell, STATUS_MD;
|
|
29806
|
+
var TYPE_LABEL, where, PRICE_UNREAD, escData, escProp, cell, STATUS_MD;
|
|
28978
29807
|
var init_report = __esm({
|
|
28979
29808
|
"src/ci/report.ts"() {
|
|
28980
29809
|
"use strict";
|
|
@@ -28988,6 +29817,7 @@ var init_report = __esm({
|
|
|
28988
29817
|
notice_schedule: "Notice schedule"
|
|
28989
29818
|
};
|
|
28990
29819
|
where = (c) => `${c.target.file}${c.target.line && c.target.line > 1 ? `:${c.target.line}` : ""}`;
|
|
29820
|
+
PRICE_UNREAD = /^line_items use (\S+), which Checkbox could not read\b/;
|
|
28991
29821
|
escData = (s2) => s2.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
28992
29822
|
escProp = (s2) => escData(s2).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
28993
29823
|
cell = (s2) => s2.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
|
|
@@ -29170,6 +30000,8 @@ __export(ci_exports, {
|
|
|
29170
30000
|
attachSiteForCoupons: () => attachSiteForCoupons,
|
|
29171
30001
|
couponIdsIn: () => couponIdsIn,
|
|
29172
30002
|
default: () => ci_default,
|
|
30003
|
+
siteFactsFor: () => siteFactsFor,
|
|
30004
|
+
termsChanged: () => termsChanged,
|
|
29173
30005
|
touchesConfig: () => touchesConfig,
|
|
29174
30006
|
undiffableInputs: () => undiffableInputs
|
|
29175
30007
|
});
|
|
@@ -29177,6 +30009,16 @@ import { execFileSync as execFileSync2 } from "node:child_process";
|
|
|
29177
30009
|
import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync2, realpathSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
29178
30010
|
import { createInterface } from "node:readline";
|
|
29179
30011
|
import { dirname as dirname7, join as join9, relative as relative5, resolve as resolve7 } from "node:path";
|
|
30012
|
+
function termsChanged(cwd, base, jobs) {
|
|
30013
|
+
if (jobs.some((j) => j.type === "stripe_checkout" && (j.sold || j.warnings.some((w) => /^The params and checkbox\.json disagree/.test(w))))) return true;
|
|
30014
|
+
try {
|
|
30015
|
+
const args = ["diff", "-U0", "--no-color", ...base ? [`${base}...HEAD`] : ["HEAD"], "--", ".", ":(exclude)*.md", ":(exclude)**/*.test.*", ":(exclude)**/*.lock", ":(exclude)package-lock.json", ":(exclude)pnpm-lock.yaml"];
|
|
30016
|
+
const out = execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 20 * 1024 * 1024 });
|
|
30017
|
+
return out.split("\n").some((l) => /^[+-](?![+-])/.test(l) && /\b(?:trial(?:_period)?_?days|trialDays|trial_end|unit_amount|interval(?:_count)?|price|amount)\b/i.test(l));
|
|
30018
|
+
} catch {
|
|
30019
|
+
return false;
|
|
30020
|
+
}
|
|
30021
|
+
}
|
|
29180
30022
|
function gitChanged(cwd, base) {
|
|
29181
30023
|
try {
|
|
29182
30024
|
const root = execFileSync2("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
@@ -29246,6 +30088,36 @@ async function confirm(question) {
|
|
|
29246
30088
|
rl.close();
|
|
29247
30089
|
}
|
|
29248
30090
|
}
|
|
30091
|
+
async function siteFactsFor(ctx, setupSession) {
|
|
30092
|
+
if (!setupSession) return {};
|
|
30093
|
+
try {
|
|
30094
|
+
if (!ctx.credentials()) return {};
|
|
30095
|
+
const client = ctx.client();
|
|
30096
|
+
const sess = await Promise.race([client.sdk.setupSessions.retrieve(setupSession), new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 8e3))]);
|
|
30097
|
+
const out = {};
|
|
30098
|
+
const b = sess?.business ?? {};
|
|
30099
|
+
if (b.merchant_name) out.merchant_name = b.merchant_name;
|
|
30100
|
+
if (b.site_url) out.site_url = b.site_url;
|
|
30101
|
+
const cancel = b.cancellation_url ?? b.account_url;
|
|
30102
|
+
if (cancel) out.account_url = cancel;
|
|
30103
|
+
const plans = (sess?.site_plans ?? []).filter((p) => typeof p.amount === "number" && p.amount > 0 && typeof p.interval === "string");
|
|
30104
|
+
if (plans.length) {
|
|
30105
|
+
out.plans = plans.map((p) => ({
|
|
30106
|
+
// A name that is only the price ("$29.00 / month") is left out: the plans module names the plan better.
|
|
30107
|
+
...p.name && !/^\W?\s*[\d.,]+/.test(p.name) ? { name: p.name } : {},
|
|
30108
|
+
amount: p.amount,
|
|
30109
|
+
currency: p.currency ?? "usd",
|
|
30110
|
+
interval: p.interval,
|
|
30111
|
+
...p.interval_count && p.interval_count > 1 ? { interval_count: p.interval_count } : {},
|
|
30112
|
+
...p.trial_days ? { trial_days: p.trial_days } : {},
|
|
30113
|
+
...p.stripe_price_ids?.length ? { stripe_price_ids: p.stripe_price_ids } : {}
|
|
30114
|
+
}));
|
|
30115
|
+
}
|
|
30116
|
+
return out;
|
|
30117
|
+
} catch {
|
|
30118
|
+
return {};
|
|
30119
|
+
}
|
|
30120
|
+
}
|
|
29249
30121
|
async function runInit(ctx, argv) {
|
|
29250
30122
|
const p = parseArgs(argv, { boolean: ["yes", "force", "dry-run", "workflow"], string: ["config"], alias: { y: "yes" } });
|
|
29251
30123
|
if (p.positionals.length) throw new UsageError(`Unexpected argument "${p.positionals[0]}"`);
|
|
@@ -29256,13 +30128,24 @@ async function runInit(ctx, argv) {
|
|
|
29256
30128
|
const withWorkflow = bool(p.flags, "workflow") !== false;
|
|
29257
30129
|
const projects = ctx.config.load().init?.projects ?? {};
|
|
29258
30130
|
let facts = {};
|
|
30131
|
+
let setupSession;
|
|
29259
30132
|
for (let dir = resolve7(ctx.cwd); ; dir = dirname7(dir)) {
|
|
29260
|
-
|
|
29261
|
-
|
|
30133
|
+
const proj = projects[dir];
|
|
30134
|
+
if (proj?.facts || proj?.setup_session) {
|
|
30135
|
+
facts = proj.facts ?? {};
|
|
30136
|
+
setupSession = proj.setup_session;
|
|
29262
30137
|
break;
|
|
29263
30138
|
}
|
|
29264
30139
|
if (dirname7(dir) === dir) break;
|
|
29265
30140
|
}
|
|
30141
|
+
const fromSite = await siteFactsFor(ctx, setupSession);
|
|
30142
|
+
facts = { ...fromSite, ...facts, ...fromSite.plans && !facts.plans ? { plans: fromSite.plans } : {} };
|
|
30143
|
+
if (Array.isArray(facts.plans) && Array.isArray(fromSite.plans)) {
|
|
30144
|
+
facts.plans = facts.plans.map((p2) => {
|
|
30145
|
+
const m = fromSite.plans.find((x) => x.amount === p2.amount && x.interval === p2.interval);
|
|
30146
|
+
return m && !p2.stripe_price_ids ? { ...p2, stripe_price_ids: m.stripe_price_ids } : p2;
|
|
30147
|
+
});
|
|
30148
|
+
}
|
|
29266
30149
|
const s2 = scaffoldConfig(ctx.cwd, ctx.env, facts);
|
|
29267
30150
|
const actionFor = (f) => existsSync8(f) ? force ? "overwrite" : "keep" : "create";
|
|
29268
30151
|
const files = [{ path: configPath, content: `${JSON.stringify(s2.config, null, 2)}
|
|
@@ -29294,8 +30177,13 @@ async function runInit(ctx, argv) {
|
|
|
29294
30177
|
found: s2.found,
|
|
29295
30178
|
...withWorkflow ? { workflow_steps: workflowSteps } : {},
|
|
29296
30179
|
todos: s2.todos,
|
|
30180
|
+
// Round 32 (pilot 31, ci-pr-agent: "Fill in the 2 TODO item(s)" with 1 TODO in the file): suggestions are notes;
|
|
30181
|
+
// the count is the number of "TODO" values in the file.
|
|
30182
|
+
todo_values: s2.todoValues,
|
|
30183
|
+
notes: s2.notes,
|
|
30184
|
+
...Object.keys(fromSite).length ? { from_site: Object.keys(fromSite) } : {},
|
|
29297
30185
|
config: s2.config,
|
|
29298
|
-
next_action: s2.
|
|
30186
|
+
next_action: s2.todoValues ? { type: "edit_config", message: `Fill in the ${s2.todoValues} "TODO" value${s2.todoValues === 1 ? "" : "s"} in ${relName(configPath)} (checkbox ci refuses TODO values), then run \`checkbox ci\` and fix what it reports.` } : { type: "run_ci", message: "Run `checkbox ci` locally, fix what it reports, then commit checkbox.json and the workflow." }
|
|
29299
30187
|
};
|
|
29300
30188
|
ctx.out.result(result, () => {
|
|
29301
30189
|
const lines = [];
|
|
@@ -29305,6 +30193,7 @@ async function runInit(ctx, argv) {
|
|
|
29305
30193
|
if (withWorkflow && workflowSteps.length) lines.push("", `The workflow runs ${workflowSteps.map((c) => `\`${c}\``).join(" and ")} before the checks, so they work on a fresh clone.`);
|
|
29306
30194
|
if (needs.urlPages) lines.push("", 'Note: "url" pages are fetched by the API (checkbox ci --online), which needs a LIVE key (ck_live_\u2026) on an activated account. Without one, list the built page as "file".');
|
|
29307
30195
|
if (s2.todos.length) lines.push("", "To do:", ...s2.todos.map((t) => ` - ${t}`));
|
|
30196
|
+
if (s2.notes.length) lines.push("", "Also:", ...s2.notes.map((t) => ` - ${t}`));
|
|
29308
30197
|
lines.push("", `${ctx.out.c.cyan("Next:")} ${result.next_action.message}`);
|
|
29309
30198
|
if (!write) lines.push("", ctx.out.c.dim(`${relName(configPath)}:`), ...JSON.stringify(s2.config, null, 2).split("\n").map((l) => ctx.out.c.dim(` ${l}`)));
|
|
29310
30199
|
return lines;
|
|
@@ -29469,13 +30358,17 @@ ${message}
|
|
|
29469
30358
|
const engine = client ? onlineEngine(client) : offlineEngine();
|
|
29470
30359
|
const notices = client && declaredByCheckbox(loaded.config).length ? await readNotices(ctx, client, str(p.flags, "site")) : void 0;
|
|
29471
30360
|
const noticesLine = Math.max(1, loaded.raw.split("\n").findIndex((l) => l.includes('"sent_by_checkbox"')) + 1);
|
|
29472
|
-
const
|
|
30361
|
+
const run0 = await runCi(jobs, engine, { config: loaded.config, configPath: configName, strict, extra, ...notices ? { notices, noticesTarget: { file: relative5(workspace, path).split("\\").join("/") || configName, line: noticesLine } } : {} });
|
|
30362
|
+
const baseRef = str(p.flags, "base") ?? (ctx.env.GITHUB_BASE_REF ? `origin/${ctx.env.GITHUB_BASE_REF}` : void 0);
|
|
30363
|
+
const run2 = !client && termsChanged(ctx.cwd, baseRef, jobs) ? { ...run0, offline_notice: STRIPE_OBJECTS_OFFLINE_NOTICE } : run0;
|
|
29473
30364
|
ctx.out.result(run2, () => renderHuman(run2, ctx.out.c, { verbose: bool(p.flags, "verbose") === true }));
|
|
29474
30365
|
if (gha) {
|
|
29475
30366
|
for (const a of annotations(run2)) emit(a);
|
|
29476
30367
|
writeGithubFile(ctx.env.GITHUB_STEP_SUMMARY, stepSummary(run2));
|
|
30368
|
+
if (run2.offline_notice) emit(`::notice title=Checkbox%3A Stripe not checked offline::${run2.offline_notice}`);
|
|
29477
30369
|
writeGithubFile(ctx.env.GITHUB_OUTPUT, `status=${run2.status}
|
|
29478
30370
|
exit_code=${run2.exit_code}
|
|
30371
|
+
result=${run2.result}
|
|
29479
30372
|
`);
|
|
29480
30373
|
}
|
|
29481
30374
|
return run2.exit_code;
|
|
@@ -29535,6 +30428,14 @@ function findAnchor(src, target) {
|
|
|
29535
30428
|
}
|
|
29536
30429
|
}
|
|
29537
30430
|
}
|
|
30431
|
+
if (!text && !id && tag) {
|
|
30432
|
+
const all = [...src.matchAll(tagRe)];
|
|
30433
|
+
if (all.length === 1) return openAt(all[0].index + 1, `the only <${tag}> in this file`);
|
|
30434
|
+
if (tag === "button" || tag === "input") {
|
|
30435
|
+
const submit = all.filter((m) => /^[^>]*\btype\s*=\s*(?:"submit"|'submit'|\{\s*["']submit["']\s*\})/i.test(src.slice(m.index, src.indexOf(">", m.index) + 1)));
|
|
30436
|
+
if (submit.length === 1) return openAt(submit[0].index + 1, `the only <${tag} type="submit"> in this file`);
|
|
30437
|
+
}
|
|
30438
|
+
}
|
|
29538
30439
|
return null;
|
|
29539
30440
|
}
|
|
29540
30441
|
function lineStart(src, idx) {
|
|
@@ -29884,6 +30785,16 @@ var init_device = __esm({
|
|
|
29884
30785
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
29885
30786
|
import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "node:fs";
|
|
29886
30787
|
import { dirname as dirname8, join as join10 } from "node:path";
|
|
30788
|
+
function pinnedRelease(body) {
|
|
30789
|
+
const b = body;
|
|
30790
|
+
if (!b || typeof b.version !== "string" || !/^\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(b.version)) return void 0;
|
|
30791
|
+
if (typeof b.integrity !== "string" || !/^sha384-[A-Za-z0-9+/]{64}$/.test(b.integrity)) return void 0;
|
|
30792
|
+
return { version: b.version, integrity: b.integrity };
|
|
30793
|
+
}
|
|
30794
|
+
function snippetScriptTag(s2, jsx = false, attrs = "") {
|
|
30795
|
+
const pin = s2.pinned ? ` integrity="${s2.pinned.integrity}" ${jsx ? "crossOrigin" : "crossorigin"}="anonymous"` : "";
|
|
30796
|
+
return `<script${attrs} async src="${srcOf(s2)}"${pin} data-key="${s2.snippetKey}" data-api="${s2.apiBase.replace(/\/+$/, "")}"></script>`;
|
|
30797
|
+
}
|
|
29887
30798
|
function read(root, rel2) {
|
|
29888
30799
|
try {
|
|
29889
30800
|
return readFileSync11(join10(root, rel2), "utf8");
|
|
@@ -29892,19 +30803,20 @@ function read(root, rel2) {
|
|
|
29892
30803
|
}
|
|
29893
30804
|
}
|
|
29894
30805
|
function srcOf(s2) {
|
|
29895
|
-
|
|
30806
|
+
const api = s2.apiBase.replace(/\/+$/, "");
|
|
30807
|
+
return s2.pinned ? `${api}/s/${s2.pinned.version}.js` : `${api}/s.js`;
|
|
29896
30808
|
}
|
|
29897
30809
|
function jsxBlock(s2, indent) {
|
|
29898
30810
|
return [
|
|
29899
30811
|
`${indent}{/* ${MARK_START} \u2014 Checkbox subscription-compliance monitoring (https://checkbox.site/docs/snippet) */}`,
|
|
29900
|
-
`${indent}
|
|
30812
|
+
`${indent}${snippetScriptTag(s2, true)}`,
|
|
29901
30813
|
`${indent}{/* ${MARK_END} */}`
|
|
29902
30814
|
];
|
|
29903
30815
|
}
|
|
29904
30816
|
function htmlBlock(s2, indent, attrs = "") {
|
|
29905
30817
|
return [
|
|
29906
30818
|
`${indent}<!-- ${MARK_START} \u2014 Checkbox subscription-compliance monitoring (https://checkbox.site/docs/snippet) -->`,
|
|
29907
|
-
`${indent}
|
|
30819
|
+
`${indent}${snippetScriptTag(s2, false, attrs)}`,
|
|
29908
30820
|
`${indent}<!-- ${MARK_END} -->`
|
|
29909
30821
|
];
|
|
29910
30822
|
}
|
|
@@ -29963,7 +30875,7 @@ function planSnippet(d, s2) {
|
|
|
29963
30875
|
const root = d.root;
|
|
29964
30876
|
const file = d.layoutFile;
|
|
29965
30877
|
const manual = `Add this tag inside <head> on every page (your root layout / template):
|
|
29966
|
-
${s2.scriptTag}`;
|
|
30878
|
+
${s2.pinned ? snippetScriptTag(s2) : s2.scriptTag}`;
|
|
29967
30879
|
if (!file) return { change: null, manual };
|
|
29968
30880
|
const existing = read(root, file);
|
|
29969
30881
|
const jsx = (f) => f === "nextjs-app" || f === "nextjs-pages" || f === "remix" || f === "react-router";
|
|
@@ -29973,21 +30885,25 @@ function planSnippet(d, s2) {
|
|
|
29973
30885
|
return { change: { file, action: existing === null ? "create" : "modify", summary: "Nuxt client plugin that adds the snippet to every page", contents, added: contents.trim().split("\n") } };
|
|
29974
30886
|
}
|
|
29975
30887
|
if (existing === null) {
|
|
30888
|
+
if (d.framework === "nextjs-app") {
|
|
30889
|
+
const contents = NEXT_APP_LAYOUT(s2, /\.tsx$/.test(file));
|
|
30890
|
+
return { change: { file, action: "create", summary: "Root layout with the snippet in <head>", contents, added: contents.trim().split("\n") } };
|
|
30891
|
+
}
|
|
29976
30892
|
if (d.framework === "nextjs-pages") {
|
|
29977
30893
|
const contents = NEXT_DOCUMENT(s2);
|
|
29978
30894
|
return { change: { file, action: "create", summary: "Custom _document with the snippet in <Head>", contents, added: contents.trim().split("\n") } };
|
|
29979
30895
|
}
|
|
29980
30896
|
return { change: null, manual };
|
|
29981
30897
|
}
|
|
29982
|
-
if (existing.includes(`data-key="${s2.snippetKey}"`) && existing.includes(MARK_START)) {
|
|
29983
|
-
return { change: { file, action: "unchanged", summary: "Snippet already installed", added: [] } };
|
|
30898
|
+
if (existing.includes(`data-key="${s2.snippetKey}"`) && existing.includes(MARK_START) && existing.includes(`src="${srcOf(s2)}"`) && (!s2.pinned || existing.includes(s2.pinned.integrity))) {
|
|
30899
|
+
return { change: { file, action: "unchanged", summary: s2.pinned ? `Snippet already installed (pinned release ${s2.pinned.version} with SRI)` : "Snippet already installed", added: [] } };
|
|
29984
30900
|
}
|
|
29985
30901
|
const isJsx = jsx(d.framework);
|
|
29986
30902
|
const indentRe = isJsx ? d.framework === "nextjs-pages" ? /<Head\b[^>]*>/ : /<head\b[^>]*>/ : /<\/head>/i;
|
|
29987
30903
|
const indent = isJsx ? indentOf2(existing, indentRe) : indentOf2(existing, /<\/head>/i, " ");
|
|
29988
30904
|
const block = isJsx ? jsxBlock(s2, indent) : htmlBlock(s2, indent, d.framework === "astro" ? " is:inline" : "");
|
|
29989
30905
|
const replaced = replaceMarked(existing, block);
|
|
29990
|
-
if (replaced !== null) return { change: { file, action: "modify", summary: "Snippet tag updated (new site key)", contents: replaced, added: block } };
|
|
30906
|
+
if (replaced !== null) return { change: { file, action: "modify", summary: existing.includes(`data-key="${s2.snippetKey}"`) ? s2.pinned ? `Snippet tag pinned to release ${s2.pinned.version} with SRI` : "Snippet tag updated" : "Snippet tag updated (new site key)", contents: replaced, added: block } };
|
|
29991
30907
|
let next = null;
|
|
29992
30908
|
switch (d.framework) {
|
|
29993
30909
|
case "nextjs-app": {
|
|
@@ -30018,7 +30934,7 @@ ${indentOf2(existing, /<Head\s*\/>/, "")}</Head>`) : null);
|
|
|
30018
30934
|
}
|
|
30019
30935
|
if (!next) return { change: null, manual: `Could not find <head> in ${file}. ${manual}` };
|
|
30020
30936
|
if (!snippetPlacedSafely(next, isJsx)) return { change: null, manual: `Could not place the snippet safely in ${file} (its <head> is not where Checkbox can patch it without breaking the file), so ${file} was NOT changed. ${manual}` };
|
|
30021
|
-
return { change: { file, action: "modify", summary: `Snippet added to <head> in ${file}`, contents: next, added: block } };
|
|
30937
|
+
return { change: { file, action: "modify", summary: `Snippet added to <head> in ${file}${s2.pinned ? ` (pinned release ${s2.pinned.version} with SRI)` : ""}`, contents: next, added: block } };
|
|
30022
30938
|
}
|
|
30023
30939
|
function envVarNames(d) {
|
|
30024
30940
|
return {
|
|
@@ -30101,7 +31017,7 @@ function applyChange(root, c) {
|
|
|
30101
31017
|
mkdirSync3(dirname8(abs), { recursive: true });
|
|
30102
31018
|
writeFileSync4(abs, c.contents, c.file.startsWith(".env") ? { mode: 384 } : void 0);
|
|
30103
31019
|
}
|
|
30104
|
-
var MARK_START, MARK_END, NEXT_DOCUMENT, NUXT_PLUGIN;
|
|
31020
|
+
var MARK_START, MARK_END, NEXT_DOCUMENT, NEXT_APP_LAYOUT, NUXT_PLUGIN;
|
|
30105
31021
|
var init_patch2 = __esm({
|
|
30106
31022
|
"src/commands/init/patch.ts"() {
|
|
30107
31023
|
"use strict";
|
|
@@ -30122,11 +31038,22 @@ ${jsxBlock(s2, " ").join("\n")}
|
|
|
30122
31038
|
</Html>
|
|
30123
31039
|
);
|
|
30124
31040
|
}
|
|
31041
|
+
`;
|
|
31042
|
+
NEXT_APP_LAYOUT = (s2, ts) => `${ts ? "import type { ReactNode } from 'react';\n\n" : ""}export default function RootLayout({ children }${ts ? ": { children: ReactNode }" : ""}) {
|
|
31043
|
+
return (
|
|
31044
|
+
<html lang="en">
|
|
31045
|
+
<head>
|
|
31046
|
+
${jsxBlock(s2, " ").join("\n")}
|
|
31047
|
+
</head>
|
|
31048
|
+
<body>{children}</body>
|
|
31049
|
+
</html>
|
|
31050
|
+
);
|
|
31051
|
+
}
|
|
30125
31052
|
`;
|
|
30126
31053
|
NUXT_PLUGIN = (s2) => `// ${MARK_START} \u2014 Checkbox subscription-compliance monitoring (https://checkbox.site/docs/snippet)
|
|
30127
31054
|
export default defineNuxtPlugin(() => {
|
|
30128
31055
|
useHead({
|
|
30129
|
-
script: [{ src: '${srcOf(s2)}', async: true, 'data-key': '${s2.snippetKey}', 'data-api': '${s2.apiBase.replace(/\/+$/, "")}' }],
|
|
31056
|
+
script: [{ src: '${srcOf(s2)}', async: true,${s2.pinned ? ` integrity: '${s2.pinned.integrity}', crossorigin: 'anonymous',` : ""} 'data-key': '${s2.snippetKey}', 'data-api': '${s2.apiBase.replace(/\/+$/, "")}' }],
|
|
30130
31057
|
});
|
|
30131
31058
|
});
|
|
30132
31059
|
// ${MARK_END}
|
|
@@ -30142,8 +31069,11 @@ __export(init_exports, {
|
|
|
30142
31069
|
configLines: () => configLines,
|
|
30143
31070
|
configNameOf: () => configNameOf,
|
|
30144
31071
|
default: () => init_default,
|
|
31072
|
+
liveKeyNextAction: () => liveKeyNextAction,
|
|
30145
31073
|
parsePlanFlag: () => parsePlanFlag,
|
|
30146
|
-
|
|
31074
|
+
snippetWords: () => snippetWords,
|
|
31075
|
+
syncCheckoutConfigs: () => syncCheckoutConfigs,
|
|
31076
|
+
withEvaluatedAt: () => withEvaluatedAt
|
|
30147
31077
|
});
|
|
30148
31078
|
import { existsSync as existsSync11 } from "node:fs";
|
|
30149
31079
|
import { join as join11 } from "node:path";
|
|
@@ -30163,6 +31093,10 @@ function saveState(ctx, root, patch) {
|
|
|
30163
31093
|
}
|
|
30164
31094
|
ctx.config.save({ init: { ...cfg, projects } });
|
|
30165
31095
|
}
|
|
31096
|
+
function withEvaluatedAt(c) {
|
|
31097
|
+
const t = c.evaluated;
|
|
31098
|
+
return typeof t === "number" && t > 0 ? { evaluated_at: new Date(t * 1e3).toISOString(), ...c } : c;
|
|
31099
|
+
}
|
|
30166
31100
|
async function ask2(question) {
|
|
30167
31101
|
const rl = createInterface2({ input: process.stdin, output: process.stderr, terminal: true });
|
|
30168
31102
|
try {
|
|
@@ -30190,6 +31124,7 @@ function projectFacts(d) {
|
|
|
30190
31124
|
...d.layoutFile ? { layout_file: d.layoutFile } : {},
|
|
30191
31125
|
env_file: d.envFile,
|
|
30192
31126
|
...d.publicDir ? { public_dir: d.publicDir } : {},
|
|
31127
|
+
...d.mailer ? { mailer: d.mailer } : {},
|
|
30193
31128
|
checkout_code: d.checkoutCode.slice(0, 100).map((c) => ({ file: c.file.slice(0, 500), line: c.line, kind: c.kind, snippet: c.snippet.slice(0, 500) })),
|
|
30194
31129
|
pay_buttons: d.payButtons.slice(0, 100).map((c) => ({ file: c.file.slice(0, 500), line: c.line, kind: c.kind, snippet: c.snippet.slice(0, 500) }))
|
|
30195
31130
|
};
|
|
@@ -30227,7 +31162,7 @@ async function syncCheckoutConfigs(client, site, sessions, root) {
|
|
|
30227
31162
|
const call = (perFile.get(s2.file) ?? 0) + 1;
|
|
30228
31163
|
perFile.set(s2.file, call);
|
|
30229
31164
|
if (!registrable(s2.params, s2.unresolved)) {
|
|
30230
|
-
out.push({ file: s2.file, line: s2.line, skipped: s2.unresolved.length ? `values not readable statically: ${s2.unresolved.slice(0, 5).join(", ")}` : "not a subscription-mode Checkout Session" });
|
|
31165
|
+
out.push({ file: s2.file, line: s2.line, skipped: s2.unresolved.length ? `values not readable statically: ${s2.unresolved.slice(0, 5).join(", ")} (a variable, call or template string). Next: register the exact params your server sends with createStripeCheckoutConfig (POST /v1/sites/{site}/stripe_checkout_configs), or, once Stripe is connected, create one Checkout Session in Stripe test mode so Checkbox reads it` : "not a subscription-mode Checkout Session" });
|
|
30231
31166
|
continue;
|
|
30232
31167
|
}
|
|
30233
31168
|
try {
|
|
@@ -30265,11 +31200,32 @@ function configLines(configs, now = Date.now()) {
|
|
|
30265
31200
|
};
|
|
30266
31201
|
return ["Checkout params registered:", ...configs.map((c) => c.id ? ` ${c.file}:${c.line} \u2192 ${c.id}${age(c.registered_at)}${c.replaced?.length ? ` (replaced ${c.replaced.length} older config${c.replaced.length === 1 ? "" : "s"})` : ""}` : ` ${c.file}${c.line ? `:${c.line}` : ""}: ${c.skipped}`)];
|
|
30267
31202
|
}
|
|
31203
|
+
async function snippetRelease(client) {
|
|
31204
|
+
try {
|
|
31205
|
+
return pinnedRelease(await client.get("/s/latest.json"));
|
|
31206
|
+
} catch {
|
|
31207
|
+
return void 0;
|
|
31208
|
+
}
|
|
31209
|
+
}
|
|
30268
31210
|
function registrable(params, unresolved) {
|
|
30269
31211
|
if (!params) return false;
|
|
30270
31212
|
if (unresolved.some((u) => /^(mode|line_items|subscription_data|custom_text|consent_collection)\b/.test(u))) return false;
|
|
30271
31213
|
return params.mode === "subscription" && Array.isArray(params.line_items) && params.line_items.length > 0;
|
|
30272
31214
|
}
|
|
31215
|
+
function snippetWords(homepage, live) {
|
|
31216
|
+
if (homepage) return "detected";
|
|
31217
|
+
if (live) return "live (seen through the page captures it sends; not in the homepage HTML Checkbox loaded)";
|
|
31218
|
+
return "NOT detected yet (deploy, then verify again)";
|
|
31219
|
+
}
|
|
31220
|
+
function liveKeyNextAction(dashboard) {
|
|
31221
|
+
const url = `${dashboard.replace(/\/+$/, "")}/settings/developers?mode=live&create=key`;
|
|
31222
|
+
return {
|
|
31223
|
+
type: "ask_human_for_live_key",
|
|
31224
|
+
message: `Ask the human for live mode (an agent never creates live keys): either they approve again with \`checkbox init --live --relogin --yes --json\` and tick \u201CAlso create live-mode keys\u201D on the approval page, or they create a live key in the Checkbox dashboard (Live mode, Agent & API: ${url}) and put it in the project env as CHECKBOX_LIVE_SECRET_KEY (never in the chat). Then run \`checkbox init --live --yes --json\`: it links the live site and writes the domain-verification file. Live checks start once the domain is verified.`,
|
|
31225
|
+
command: "checkbox init --live --relogin --yes --json",
|
|
31226
|
+
url
|
|
31227
|
+
};
|
|
31228
|
+
}
|
|
30273
31229
|
function detectionSummary(d, siteUrl) {
|
|
30274
31230
|
return {
|
|
30275
31231
|
framework: d.framework,
|
|
@@ -30292,9 +31248,14 @@ function detectionSummary(d, siteUrl) {
|
|
|
30292
31248
|
function fileOut(c) {
|
|
30293
31249
|
return { file: c.file, action: c.action, summary: c.summary, added: c.added };
|
|
30294
31250
|
}
|
|
30295
|
-
function
|
|
31251
|
+
function resultOf2(status, d, session, changes, extra, apiBase) {
|
|
30296
31252
|
const names = envVarNames(d);
|
|
30297
|
-
const next = session.next_action.type === "done" ? {
|
|
31253
|
+
const next = session.next_action.type === "done" ? {
|
|
31254
|
+
type: "done",
|
|
31255
|
+
// Round 32 (pilot 31 item 1): a test-mode setup says so, with the go-live steps left (go_live), never a bare "set up".
|
|
31256
|
+
message: session.go_live?.steps.length ? `Checkbox is set up in Stripe test mode. ${session.go_live.summary} After each change to checkout, emails or cancellation, run \`checkbox plan --json\` and apply what it returns.` : "Checkbox is set up. After each change to checkout, emails or cancellation, run `checkbox plan --json` and apply what it returns.",
|
|
31257
|
+
command: "checkbox plan --json"
|
|
31258
|
+
} : session.next_action;
|
|
30298
31259
|
return {
|
|
30299
31260
|
object: "checkbox_init",
|
|
30300
31261
|
status,
|
|
@@ -30309,7 +31270,10 @@ function resultOf(status, d, session, changes, extra, apiBase) {
|
|
|
30309
31270
|
install: session.install,
|
|
30310
31271
|
stripe: { ...session.stripe, ...extra.stripe ?? {} },
|
|
30311
31272
|
...extra.live && Object.keys(extra.live).length ? { live: extra.live } : {},
|
|
30312
|
-
compliance:
|
|
31273
|
+
// Round 32 (pilot 31, discovery-agent: "compliance.evaluated" read like a count but is a Unix timestamp): the CLI says
|
|
31274
|
+
// when, as evaluated_at (ISO 8601), next to the API's field.
|
|
31275
|
+
compliance: session.compliance ? withEvaluatedAt(session.compliance) : session.compliance,
|
|
31276
|
+
...session.go_live ? { go_live: session.go_live } : {},
|
|
30313
31277
|
steps: session.steps.map((s2) => ({ key: s2.key, status: s2.status, summary: s2.summary })),
|
|
30314
31278
|
warnings: extra.warnings ?? [],
|
|
30315
31279
|
owner_notified: session.owner_notified,
|
|
@@ -30332,6 +31296,10 @@ function renderSession(ctx, s2) {
|
|
|
30332
31296
|
for (const t of todo.slice(0, 5)) lines.push(` ${t.status === "needs_fix" || t.status === "not_set_up" ? c.red("\u2717") : c.yellow("?")} ${t.title} ${c.dim(`(${t.label})`)}`);
|
|
30333
31297
|
if (todo.length > 5) lines.push(c.dim(` \u2026 ${todo.length - 5} more: checkbox plan`));
|
|
30334
31298
|
}
|
|
31299
|
+
if (s2.go_live?.steps.length) {
|
|
31300
|
+
lines.push("", c.bold(`Before real customers (Stripe test mode so far): ${s2.go_live.steps.length} step${s2.go_live.steps.length === 1 ? "" : "s"}`));
|
|
31301
|
+
s2.go_live.steps.forEach((st, i) => lines.push(` ${i + 1}. ${c.dim(st.who === "person" ? "[owner]" : "[agent]")} ${st.text}`));
|
|
31302
|
+
}
|
|
30335
31303
|
lines.push("", `${c.cyan("next")} ${s2.next_action.message}${s2.next_action.command ? `
|
|
30336
31304
|
${s2.next_action.command}` : ""}`, `${c.dim("review")} ${s2.review_url}`);
|
|
30337
31305
|
return lines;
|
|
@@ -30357,6 +31325,7 @@ var init_init = __esm({
|
|
|
30357
31325
|
"use strict";
|
|
30358
31326
|
init_args();
|
|
30359
31327
|
init_config();
|
|
31328
|
+
init_src2();
|
|
30360
31329
|
init_http();
|
|
30361
31330
|
init_types2();
|
|
30362
31331
|
init_detect();
|
|
@@ -30364,7 +31333,7 @@ var init_init = __esm({
|
|
|
30364
31333
|
init_patch2();
|
|
30365
31334
|
VERSION_FLAGS = {
|
|
30366
31335
|
string: ["site-url", "checkout-url", "account-url", "terms-url", "privacy-url", "refund-url", "cancellation-url", "wait-seconds", "agent", "support-email", "merchant-name", "plan"],
|
|
30367
|
-
boolean: ["yes", "dry-run", "open", "wait", "verify", "complete", "live", "stripe", "snippet", "relogin"],
|
|
31336
|
+
boolean: ["yes", "dry-run", "open", "wait", "verify", "complete", "live", "stripe", "snippet", "pin", "relogin"],
|
|
30368
31337
|
alias: { y: "yes" }
|
|
30369
31338
|
};
|
|
30370
31339
|
interactive = (ctx) => !ctx.flags.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
@@ -30376,8 +31345,11 @@ var init_init = __esm({
|
|
|
30376
31345
|
"Run it in the project root. What it does (idempotent; re-run any time):",
|
|
30377
31346
|
" 1. detects the framework, billing platform, checkout code and pay buttons",
|
|
30378
31347
|
' 2. signs you in: prints ONE link ("Open \u2026 and click Approve") and opens the browser; new accounts are free',
|
|
30379
|
-
" 3. writes CHECKBOX_SECRET_KEY and the publishable key to .env.local / .env (and git-ignores it)",
|
|
30380
|
-
"
|
|
31348
|
+
" 3. writes CHECKBOX_SECRET_KEY and the publishable key to .env.local / .env (and git-ignores it); when that file is",
|
|
31349
|
+
" tracked by git, or the folder is deployed as it is, the key is not written to it and stays only in the CLI config",
|
|
31350
|
+
" (~/.config/checkbox/config.json), which the output says",
|
|
31351
|
+
" 4. installs the Checkbox snippet in the root layout (marked with checkbox:snippet comments): the pinned release",
|
|
31352
|
+
" (/s/<version>.js with integrity= and crossorigin=, Subresource Integrity); --no-pin uses the unpinned /s.js",
|
|
30381
31353
|
" 5. registers the site, Terms / refund / cancellation policy URLs (fetched and checked by Checkbox)",
|
|
30382
31354
|
" 6. with your permission, uses the project's Stripe restricted TEST key (rk_test_\u2026; sk_test_ and live keys are refused) for a synthetic test subscription",
|
|
30383
31355
|
" 7. runs the first checks and prints the next step (`checkbox plan --json`); the human gets the summary email",
|
|
@@ -30453,7 +31425,7 @@ var init_init = __esm({
|
|
|
30453
31425
|
if (bool(flags, "verify")) {
|
|
30454
31426
|
const v = await client2.sdk.setupSessions.verify(state.setup_session);
|
|
30455
31427
|
session2 = v.setup_session;
|
|
30456
|
-
out.info(v.homepage.ok ? `Site loaded; snippet ${v.homepage.snippet_detected
|
|
31428
|
+
out.info(v.homepage.ok ? `Site loaded; snippet ${snippetWords(v.homepage.snippet_detected, session2.install?.detected_on_site ?? null)}` : `Could not load the site: ${v.homepage.error}`);
|
|
30457
31429
|
} else {
|
|
30458
31430
|
session2 = await client2.sdk.setupSessions.retrieve(state.setup_session);
|
|
30459
31431
|
}
|
|
@@ -30463,7 +31435,7 @@ var init_init = __esm({
|
|
|
30463
31435
|
const configs = await syncCheckoutConfigs(client2, session2.site, d.checkoutSessions, root).catch((err) => [{ file: "", line: 0, skipped: err instanceof Error ? err.message : String(err) }]);
|
|
30464
31436
|
if (configs.length) stripeOut.checkout_configs = configs;
|
|
30465
31437
|
}
|
|
30466
|
-
out.result(
|
|
31438
|
+
out.result(resultOf2("verified", d, session2, [], Object.keys(stripeOut).length ? { stripe: stripeOut } : {}, apiBase), () => [...renderSession(ctx, session2), ...configLines(stripeOut.checkout_configs)]);
|
|
30467
31439
|
return EXIT.ok;
|
|
30468
31440
|
}
|
|
30469
31441
|
if (dryRun && (!state.setup_session || !creds)) {
|
|
@@ -30492,10 +31464,11 @@ var init_init = __esm({
|
|
|
30492
31464
|
let token = null;
|
|
30493
31465
|
let apiKey = creds?.apiKey;
|
|
30494
31466
|
let sessionId = state.setup_session;
|
|
30495
|
-
const
|
|
31467
|
+
const pendingLogin = state.pending && Date.parse(state.pending.expires_at) > Date.now() + 1e4 && state.pending.api_base === apiBase ? state.pending : void 0;
|
|
31468
|
+
const reuse = !!(state.setup_session && creds && bool(flags, "relogin") !== true && !pendingLogin);
|
|
30496
31469
|
if (!reuse) {
|
|
30497
31470
|
const anon = ctx.client({ apiKey: "device-login", apiBase });
|
|
30498
|
-
let pending =
|
|
31471
|
+
let pending = pendingLogin;
|
|
30499
31472
|
if (!pending) {
|
|
30500
31473
|
const agent2 = detectAgent(ctx.env, agentName);
|
|
30501
31474
|
const wantStripe = bool(flags, "stripe") !== false && d.billingPlatform === "stripe" && !!d.stripeTestKey;
|
|
@@ -30508,7 +31481,8 @@ var init_init = __esm({
|
|
|
30508
31481
|
...merchantName ? { merchant_name: merchantName.slice(0, 100) } : {},
|
|
30509
31482
|
framework: d.framework,
|
|
30510
31483
|
...d.packageManager ? { package_manager: d.packageManager } : {},
|
|
30511
|
-
|
|
31484
|
+
// "custom" means nothing was detected: never sent as a declared platform (the API then looks for Stripe itself).
|
|
31485
|
+
...d.billingPlatform !== "custom" ? { billing_platform: d.billingPlatform } : {},
|
|
30512
31486
|
...bool(flags, "live") ? { live: true } : {},
|
|
30513
31487
|
...wantStripe ? { stripe_test_key_hint: d.stripeTestKey.hint } : {}
|
|
30514
31488
|
});
|
|
@@ -30539,7 +31513,7 @@ var init_init = __esm({
|
|
|
30539
31513
|
expires_at: pending.expires_at,
|
|
30540
31514
|
next_action: {
|
|
30541
31515
|
type: "ask_human_to_approve",
|
|
30542
|
-
message: `Ask the human to open ${pending.verification_uri_complete} and click Approve,
|
|
31516
|
+
message: `Ask the human to open ${pending.verification_uri_complete} and click Approve.${d.billingPlatform === "stripe" && bool(flags, "stripe") !== false ? ` ${approvalKeyAsk({ stripe: true, keyFound: !!d.stripeTestKey })}` : ""} Then run \`checkbox init --yes --json\` again (it continues where it stopped and remembers ${Object.keys(factsToRemember).length ? Object.keys(factsToRemember).join(", ") : "the flags given now"}).`,
|
|
30543
31517
|
command: "checkbox init --yes --json",
|
|
30544
31518
|
url: pending.verification_uri_complete
|
|
30545
31519
|
}
|
|
@@ -30623,7 +31597,9 @@ var init_init = __esm({
|
|
|
30623
31597
|
if (session.install.script_tag && session.install.snippet_key) {
|
|
30624
31598
|
if (bool(flags, "snippet") === false) manualSnippet = session.install.script_tag;
|
|
30625
31599
|
else {
|
|
30626
|
-
const
|
|
31600
|
+
const pinned = bool(flags, "pin") === false ? void 0 : await snippetRelease(client);
|
|
31601
|
+
if (!pinned && bool(flags, "pin") !== false) warnings.push("The pinned snippet release (/s/latest.json) could not be read, so the unpinned /s.js tag was used. Run `checkbox init` again to pin it with Subresource Integrity.");
|
|
31602
|
+
const plan2 = planSnippet(d, { scriptTag: session.install.script_tag, snippetKey: session.install.snippet_key, apiBase, ...pinned ? { pinned } : {} });
|
|
30627
31603
|
if (plan2.change) changes.push(plan2.change);
|
|
30628
31604
|
if (plan2.manual) manualSnippet = plan2.manual;
|
|
30629
31605
|
}
|
|
@@ -30680,9 +31656,12 @@ var init_init = __esm({
|
|
|
30680
31656
|
}
|
|
30681
31657
|
const live = {};
|
|
30682
31658
|
if (!dryRun && bool(flags, "live") && siteUrl) {
|
|
30683
|
-
const liveKey = token?.live_secret_key ?? void 0;
|
|
30684
|
-
if (!liveKey) {
|
|
30685
|
-
|
|
31659
|
+
const liveKey = token?.live_secret_key ?? ctx.env.CHECKBOX_LIVE_SECRET_KEY ?? d.env.CHECKBOX_LIVE_SECRET_KEY ?? void 0;
|
|
31660
|
+
if (!liveKey || !/^ck_live_/.test(liveKey)) {
|
|
31661
|
+
const ask3 = liveKeyNextAction(session.review_url.replace(/\/setup\/[^/]*$/, ""));
|
|
31662
|
+
live.skipped = "no_live_key";
|
|
31663
|
+
live.next_action = ask3;
|
|
31664
|
+
warnings.push(`Live mode not set up yet: it needs a live Checkbox key (ck_live_\u2026), and only a person creates one. ${ask3.message}`);
|
|
30686
31665
|
} else {
|
|
30687
31666
|
try {
|
|
30688
31667
|
const lc = ctx.client({ apiKey: liveKey, apiBase });
|
|
@@ -30696,7 +31675,7 @@ var init_init = __esm({
|
|
|
30696
31675
|
changes.push(wk);
|
|
30697
31676
|
}
|
|
30698
31677
|
}
|
|
30699
|
-
session = await
|
|
31678
|
+
session = await lc.sdk.setupSessions.update(sessionId, { live_site: liveSite.id });
|
|
30700
31679
|
saveState(ctx, root, { live_site: liveSite.id });
|
|
30701
31680
|
live.site = liveSite.id;
|
|
30702
31681
|
live.domain_verification = { status: dv.status, file: dv.file?.url ?? null, dns_txt: dv.dns_record ? `${dv.dns_record.name} = ${dv.dns_record.value}` : null };
|
|
@@ -30709,7 +31688,8 @@ var init_init = __esm({
|
|
|
30709
31688
|
out.info("Running the first checks\u2026");
|
|
30710
31689
|
session = (await client.sdk.setupSessions.verify(sessionId)).setup_session;
|
|
30711
31690
|
}
|
|
30712
|
-
const doc =
|
|
31691
|
+
const doc = resultOf2(dryRun ? "dry_run" : "configured", d, session, changes, { stripe, live, warnings, manualSnippet, token, envNotWritten: envTracked && !!token }, apiBase);
|
|
31692
|
+
if (live.next_action && (session.next_action.type === "done" || session.next_action.type === "complete_setup")) doc.next_action = live.next_action;
|
|
30713
31693
|
out.result(doc, () => [...renderFiles(ctx, changes, dryRun), ...manualSnippet ? [`Add the snippet yourself: ${manualSnippet}`] : [], ...warnings.map((w) => `${out.c.yellow("!")} ${w}`), ...configLines(stripe.checkout_configs), ...renderSession(ctx, session)]);
|
|
30714
31694
|
return EXIT.ok;
|
|
30715
31695
|
}
|