@checkbox-site/cli 0.4.6 → 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/dist/checkbox.js +798 -100
- package/package.json +1 -1
package/dist/checkbox.js
CHANGED
|
@@ -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
|
}),
|
|
@@ -12106,6 +12171,13 @@ function checkoutSessionsOf(config) {
|
|
|
12106
12171
|
if (!Array.isArray(raw)) return [];
|
|
12107
12172
|
return raw.filter((s2) => !!s2 && typeof s2 === "object" && typeof s2.id === "string" && Array.isArray(s2.lineItems));
|
|
12108
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
|
+
}
|
|
12109
12181
|
function newestFirst(sessions) {
|
|
12110
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);
|
|
12111
12183
|
}
|
|
@@ -16376,6 +16448,73 @@ var init_intro_terms = __esm({
|
|
|
16376
16448
|
}
|
|
16377
16449
|
});
|
|
16378
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
|
+
|
|
16379
16518
|
// ../rules/src/checks/notice-content.ts
|
|
16380
16519
|
function offlineOnlyCancelIn(text, cancelMethods) {
|
|
16381
16520
|
const offline = contactOnlySentence(text);
|
|
@@ -16414,11 +16553,21 @@ function noticeContent(e, opts = {}) {
|
|
|
16414
16553
|
];
|
|
16415
16554
|
const amount = a.recurringPrices[0]?.money ?? positiveAmounts(a)[0];
|
|
16416
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;
|
|
16417
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);
|
|
16418
16566
|
else if (amount) quotes.amount = quote(amount);
|
|
16419
16567
|
const freq = a.recurringPrices[0]?.frequency ?? a.frequencies[0];
|
|
16420
16568
|
const freqs = [...a.recurringPrices.map((r) => r.frequency), ...a.frequencies].filter((f) => f.unit);
|
|
16421
|
-
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);
|
|
16422
16571
|
const misses = (f) => !plans.some((p) => frequencyMatchesPlan(f.unit, f.count, p));
|
|
16423
16572
|
const wrongCharged = plans.length ? charged.find(misses) : void 0;
|
|
16424
16573
|
if (wrongCharged) wrong.frequency = quote(wrongCharged);
|
|
@@ -16485,6 +16634,7 @@ var init_notice_content = __esm({
|
|
|
16485
16634
|
init_context();
|
|
16486
16635
|
init_intro_terms();
|
|
16487
16636
|
init_result();
|
|
16637
|
+
init_charge_claims();
|
|
16488
16638
|
ELEMENT_LABELS = {
|
|
16489
16639
|
amount: "the amount charged",
|
|
16490
16640
|
frequency: "how often the subscription is charged",
|
|
@@ -19223,6 +19373,7 @@ var init_signup = __esm({
|
|
|
19223
19373
|
init_offer_view();
|
|
19224
19374
|
init_plan_terms();
|
|
19225
19375
|
init_cancellation();
|
|
19376
|
+
init_charge_claims();
|
|
19226
19377
|
init_intro_terms();
|
|
19227
19378
|
init_intro_terms();
|
|
19228
19379
|
init_result();
|
|
@@ -19388,6 +19539,29 @@ var init_signup = __esm({
|
|
|
19388
19539
|
const pl = view.plan?.plan;
|
|
19389
19540
|
return !!pl && hasIntro(pl) && r.money.amount !== null && Math.abs(r.money.amount - pl.introAmount / 10 ** currencyDigits(pl.currency || "usd")) < 5e-3;
|
|
19390
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
|
+
}
|
|
19391
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;
|
|
19392
19566
|
if (foreign) {
|
|
19393
19567
|
const own = parsed.find(recurringMatches) ?? rp;
|
|
@@ -23865,6 +24039,10 @@ function fixTextsFor(t, style2) {
|
|
|
23865
24039
|
if (style2 === "full") return fixTexts(t);
|
|
23866
24040
|
return { disclosure: compactDisclosureText(t), consentLabel: COMPACT_CONSENT_LABEL, cta: compactCtaLabel(t) };
|
|
23867
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
|
+
}
|
|
23868
24046
|
var COMPACT_CONSENT_LABEL;
|
|
23869
24047
|
var init_compact = __esm({
|
|
23870
24048
|
"../rules/src/fixes/compact.ts"() {
|
|
@@ -23881,9 +24059,10 @@ function shortDisclosureText(t) {
|
|
|
23881
24059
|
const trial = !!t.trialDays && t.trialDays > 0;
|
|
23882
24060
|
const intro = introPhrase(t);
|
|
23883
24061
|
const cancel = `Cancel anytime online at ${t.cancelUrl}`;
|
|
23884
|
-
|
|
23885
|
-
if (
|
|
23886
|
-
|
|
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}.`;
|
|
23887
24066
|
}
|
|
23888
24067
|
var init_short = __esm({
|
|
23889
24068
|
"../rules/src/fixes/short.ts"() {
|
|
@@ -24595,6 +24774,45 @@ var init_stripe = __esm({
|
|
|
24595
24774
|
}
|
|
24596
24775
|
});
|
|
24597
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
|
+
|
|
24598
24816
|
// ../rules/src/preflight/stripe.ts
|
|
24599
24817
|
function clone(v) {
|
|
24600
24818
|
return JSON.parse(JSON.stringify(v));
|
|
@@ -24987,10 +25205,6 @@ function correct(params, shape, checks, forLink, style2 = "full") {
|
|
|
24987
25205
|
}
|
|
24988
25206
|
return { patch, corrected: out, texts, needText, needConsent };
|
|
24989
25207
|
}
|
|
24990
|
-
function stripeCompactConsent(t) {
|
|
24991
|
-
const trial = t.trialDays && t.trialDays > 0 ? ` after the ${t.trialDays}-day free trial` : "";
|
|
24992
|
-
return `I agree that my subscription renews automatically at ${formatMoney(t.amount, t.currency)} every ${intervalPhrase(t.interval, t.intervalCount)}${trial} until I cancel.`;
|
|
24993
|
-
}
|
|
24994
25208
|
async function bestCorrection(params, shape, checks, forLink, judgeAfter) {
|
|
24995
25209
|
const none = () => ({ fix: correct(params, shape, [], forLink), after: checks });
|
|
24996
25210
|
const first2 = correct(params, shape, checks, forLink, "compact");
|
|
@@ -25071,6 +25285,12 @@ function withResolvedPrices(input, knownPrices, amountSource = "merchant.price")
|
|
|
25071
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" }]
|
|
25072
25286
|
};
|
|
25073
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
|
+
}
|
|
25074
25294
|
async function checkStripeCheckoutSession(input0, opts = {}) {
|
|
25075
25295
|
const { jurisdictions, asOf, now } = common(input0, opts.now);
|
|
25076
25296
|
const resolvedPrices = withResolvedPrices(input0, opts.knownPrices, opts.amountSource);
|
|
@@ -25086,7 +25306,8 @@ async function checkStripeCheckoutSession(input0, opts = {}) {
|
|
|
25086
25306
|
const shape = readParams(params, input.merchant, "session", asOf);
|
|
25087
25307
|
const url = "https://checkout.stripe.com/c/pay/cs_test_preflight";
|
|
25088
25308
|
const checks = await judge2(shape, url, jurisdictions, asOf, now, "preflight-stripe-before");
|
|
25089
|
-
const
|
|
25309
|
+
const pairing = pricePairingConflict(input0, resolvedPrices);
|
|
25310
|
+
const { fix, after } = pairing ? { fix: correct(params, shape, [], false), after: checks } : await bestCorrection(
|
|
25090
25311
|
params,
|
|
25091
25312
|
shape,
|
|
25092
25313
|
checks,
|
|
@@ -25101,12 +25322,12 @@ async function checkStripeCheckoutSession(input0, opts = {}) {
|
|
|
25101
25322
|
verification: { checks: d.checks, passes: d.passes, remaining_checks: d.remaining, note: HOSTED_STYLING_NOTE },
|
|
25102
25323
|
placeholders
|
|
25103
25324
|
};
|
|
25104
|
-
const warnings = [...resolvedPrices.warnings, ...shape.warnings];
|
|
25325
|
+
const warnings = [...resolvedPrices.warnings, ...pairing ? [pairing] : [], ...shape.warnings];
|
|
25105
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.' });
|
|
25106
25327
|
const channel2 = noticeChannelField(params);
|
|
25107
25328
|
if (channel2) warnings.push(channel2);
|
|
25108
25329
|
return {
|
|
25109
|
-
...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 }),
|
|
25110
25331
|
kind: "stripe_checkout_session",
|
|
25111
25332
|
correction
|
|
25112
25333
|
};
|
|
@@ -25124,7 +25345,8 @@ async function checkStripePaymentLink(input0, opts = {}) {
|
|
|
25124
25345
|
const id = typeof params.id === "string" ? params.id : void 0;
|
|
25125
25346
|
const url = typeof params.url === "string" && /^https:\/\/buy\.stripe\.com\//.test(params.url) ? params.url : "https://buy.stripe.com/test_preflight";
|
|
25126
25347
|
const checks = await judge2(shape, url, jurisdictions, asOf, now, "preflight-link-before");
|
|
25127
|
-
const
|
|
25348
|
+
const pairing = pricePairingConflict(input0, resolvedPrices);
|
|
25349
|
+
const { fix, after } = pairing ? { fix: correct(params, shape, [], true), after: checks } : await bestCorrection(
|
|
25128
25350
|
params,
|
|
25129
25351
|
shape,
|
|
25130
25352
|
checks,
|
|
@@ -25163,11 +25385,11 @@ async function checkStripePaymentLink(input0, opts = {}) {
|
|
|
25163
25385
|
...update_params ? { update_params } : {},
|
|
25164
25386
|
needs_new_link_for_consent: needsNew
|
|
25165
25387
|
};
|
|
25166
|
-
const warnings = [...resolvedPrices.warnings, ...shape.warnings];
|
|
25388
|
+
const warnings = [...resolvedPrices.warnings, ...pairing ? [pairing] : [], ...shape.warnings];
|
|
25167
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.' });
|
|
25168
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).` : "";
|
|
25169
25391
|
return {
|
|
25170
|
-
...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 }),
|
|
25171
25393
|
kind: "stripe_payment_link",
|
|
25172
25394
|
correction
|
|
25173
25395
|
};
|
|
@@ -25246,7 +25468,7 @@ function noticeChannelField(params) {
|
|
|
25246
25468
|
param: `params.custom_fields[${i}]`
|
|
25247
25469
|
};
|
|
25248
25470
|
}
|
|
25249
|
-
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;
|
|
25250
25472
|
var init_stripe2 = __esm({
|
|
25251
25473
|
"../rules/src/preflight/stripe.ts"() {
|
|
25252
25474
|
"use strict";
|
|
@@ -25259,6 +25481,7 @@ var init_stripe2 = __esm({
|
|
|
25259
25481
|
init_common();
|
|
25260
25482
|
init_disclosure();
|
|
25261
25483
|
init_model();
|
|
25484
|
+
init_price_hint();
|
|
25262
25485
|
TEXT_CHECKS = [
|
|
25263
25486
|
"signup.disclosure.autorenew_language",
|
|
25264
25487
|
"signup.disclosure.price",
|
|
@@ -25277,6 +25500,7 @@ var init_stripe2 = __esm({
|
|
|
25277
25500
|
STRIPE_GREY = { fontSizePx: 13, fontWeight: 400, color: "rgba(26, 26, 26, 0.7)", backgroundColor: "rgb(255, 255, 255)" };
|
|
25278
25501
|
DAY_MS3 = 864e5;
|
|
25279
25502
|
open = (c) => !!c && (c.status === "fail" || c.status === "needs_review");
|
|
25503
|
+
PAIRING_ACTION = (w) => ({ type: "human_review", message: `${w.message}` });
|
|
25280
25504
|
STRIPE_HOSTED_STYLING_NOTE = HOSTED_STYLING_NOTE;
|
|
25281
25505
|
}
|
|
25282
25506
|
});
|
|
@@ -25286,6 +25510,8 @@ function stripeParamsVerdict(r) {
|
|
|
25286
25510
|
if (r.verdict === "not_applicable") return { verdict: "not_applicable", open: [] };
|
|
25287
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));
|
|
25288
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 };
|
|
25289
25515
|
const coupon = (r.warnings ?? []).find((w) => w.code === "coupon_unverified");
|
|
25290
25516
|
if (open2.length) return { verdict: "needs_review", open: open2, ...coupon ? { reason: coupon.message } : {} };
|
|
25291
25517
|
if (coupon) return { verdict: "needs_review", open: [], reason: coupon.message };
|
|
@@ -25686,7 +25912,8 @@ ${html ?? ""}`, kind);
|
|
|
25686
25912
|
text: t.text,
|
|
25687
25913
|
html: htmlOf(t.text),
|
|
25688
25914
|
merge_tags: t.tags,
|
|
25689
|
-
|
|
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)),
|
|
25690
25917
|
verified: remaining.length === 0,
|
|
25691
25918
|
remaining_checks: remaining
|
|
25692
25919
|
};
|
|
@@ -26114,6 +26341,7 @@ __export(preflight_exports, {
|
|
|
26114
26341
|
checkoutSessionsOf: () => checkoutSessionsOf,
|
|
26115
26342
|
emailTemplate: () => emailTemplate,
|
|
26116
26343
|
extractStaticPage: () => extractStaticPage,
|
|
26344
|
+
intervalHintOf: () => intervalHintOf,
|
|
26117
26345
|
lineItemPriceIds: () => lineItemPriceIds,
|
|
26118
26346
|
modelCheckoutConfig: () => modelCheckoutConfig,
|
|
26119
26347
|
modelContext: () => modelContext,
|
|
@@ -26121,6 +26349,7 @@ __export(preflight_exports, {
|
|
|
26121
26349
|
parseHtml: () => parseHtml,
|
|
26122
26350
|
parseJurisdictionList: () => parseJurisdictionList,
|
|
26123
26351
|
paymentLinksOf: () => paymentLinksOf,
|
|
26352
|
+
priceIntervalConflict: () => priceIntervalConflict,
|
|
26124
26353
|
resolvePlan: () => resolvePlan,
|
|
26125
26354
|
resolvePriceIds: () => resolvePriceIds,
|
|
26126
26355
|
runChecks: () => runChecks,
|
|
@@ -26128,7 +26357,9 @@ __export(preflight_exports, {
|
|
|
26128
26357
|
signupStripeObjects: () => signupStripeObjects,
|
|
26129
26358
|
stripeParamsVerdict: () => stripeParamsVerdict,
|
|
26130
26359
|
supportContactLine: () => supportContactLine,
|
|
26131
|
-
|
|
26360
|
+
testRunSessionIds: () => testRunSessionIds,
|
|
26361
|
+
withIntroOffers: () => withIntroOffers,
|
|
26362
|
+
withTestRunSessions: () => withTestRunSessions
|
|
26132
26363
|
});
|
|
26133
26364
|
var init_preflight = __esm({
|
|
26134
26365
|
"../rules/src/preflight/index.ts"() {
|
|
@@ -26136,6 +26367,7 @@ var init_preflight = __esm({
|
|
|
26136
26367
|
init_common();
|
|
26137
26368
|
init_disclosure();
|
|
26138
26369
|
init_stripe2();
|
|
26370
|
+
init_price_hint();
|
|
26139
26371
|
init_stripe_verdict();
|
|
26140
26372
|
init_email();
|
|
26141
26373
|
init_html_page();
|
|
@@ -26321,7 +26553,8 @@ var init_check = __esm({
|
|
|
26321
26553
|
}
|
|
26322
26554
|
const hf = str(f, "html-file");
|
|
26323
26555
|
if (hf) html = await readInput(ctx, hf);
|
|
26324
|
-
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>)');
|
|
26325
26558
|
if (!text && !html) throw new UsageError("Give the email body with --text, --file or --html-file");
|
|
26326
26559
|
const terms2 = planFrom(f);
|
|
26327
26560
|
const pp = str(f, "previous-price");
|
|
@@ -26483,7 +26716,7 @@ function loadConfig(path) {
|
|
|
26483
26716
|
function validateConfig(value) {
|
|
26484
26717
|
const is = new Issues();
|
|
26485
26718
|
if (!isObj2(value)) return ["must be a JSON object (see https://checkbox.site/docs/guides/ci#config)"];
|
|
26486
|
-
is.keys("", value, ["$schema", "merchant", "jurisdictions", "plans", "notices", "checks", "coverage"]);
|
|
26719
|
+
is.keys("", value, ["$schema", "merchant", "jurisdictions", "plans", "notices", "merge_tags", "checks", "coverage"]);
|
|
26487
26720
|
const cov = value.coverage;
|
|
26488
26721
|
if (cov !== void 0 && cov !== false) {
|
|
26489
26722
|
if (!isObj2(cov)) is.add("coverage", `must be false or an object like { "ignore": ["scripts/**"] } (got ${show(cov)})`);
|
|
@@ -26551,6 +26784,15 @@ function validateConfig(value) {
|
|
|
26551
26784
|
}
|
|
26552
26785
|
}
|
|
26553
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);
|
|
26554
26796
|
const checks = value.checks;
|
|
26555
26797
|
if (!isObj2(checks)) {
|
|
26556
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)})`);
|
|
@@ -26635,7 +26877,8 @@ function validateConfig(value) {
|
|
|
26635
26877
|
entries(
|
|
26636
26878
|
"emails",
|
|
26637
26879
|
(at, e) => {
|
|
26638
|
-
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);
|
|
26639
26882
|
is.oneOf(`${at}.kind`, e.kind, EMAIL_KINDS2, true);
|
|
26640
26883
|
is.exactlyOne(at, e, ["file", "text", "html", "module"], [["text", "html"]]);
|
|
26641
26884
|
if (e.file !== void 0) {
|
|
@@ -26668,7 +26911,7 @@ function entryLine(raw, name) {
|
|
|
26668
26911
|
const i = lines.findIndex((l) => needle.test(l));
|
|
26669
26912
|
return i >= 0 ? i + 1 : 1;
|
|
26670
26913
|
}
|
|
26671
|
-
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;
|
|
26672
26915
|
var init_config3 = __esm({
|
|
26673
26916
|
"src/ci/config.ts"() {
|
|
26674
26917
|
"use strict";
|
|
@@ -26677,6 +26920,7 @@ var init_config3 = __esm({
|
|
|
26677
26920
|
INTERVALS4 = ["day", "week", "month", "year"];
|
|
26678
26921
|
EMAIL_KINDS2 = ["acknowledgment", "trial_reminder", "renewal_reminder", "price_change", "annual_terms", "charge_notice", "cancellation_confirmation"];
|
|
26679
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"];
|
|
26680
26924
|
ConfigError = class extends Error {
|
|
26681
26925
|
constructor(file, issues) {
|
|
26682
26926
|
super(`${file}: ${issues.length === 1 ? issues[0] : `${issues.length} problems:
|
|
@@ -27030,6 +27274,12 @@ function detectBrand(root, layoutFile, pkgName) {
|
|
|
27030
27274
|
}
|
|
27031
27275
|
return void 0;
|
|
27032
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
|
+
}
|
|
27033
27283
|
function readJson(path) {
|
|
27034
27284
|
try {
|
|
27035
27285
|
return JSON.parse(readFileSync5(path, "utf8"));
|
|
@@ -27089,12 +27339,49 @@ function walk2(root, visit) {
|
|
|
27089
27339
|
}
|
|
27090
27340
|
}
|
|
27091
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
|
+
}
|
|
27092
27376
|
function detectFramework(root, deps) {
|
|
27093
27377
|
const has2 = (d) => d in deps;
|
|
27094
27378
|
if (existsSync4(join4(root, "layout", "theme.liquid")) && existsSync4(join4(root, "config", "settings_schema.json"))) return { framework: "shopify-theme", layoutFile: "layout/theme.liquid" };
|
|
27095
27379
|
if (has2("next")) {
|
|
27096
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"]);
|
|
27097
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"}` };
|
|
27098
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"]);
|
|
27099
27386
|
const pagesDir = existsSync4(join4(root, "src", "pages")) ? "src/pages" : "pages";
|
|
27100
27387
|
return { framework: "nextjs-pages", layoutFile: doc ?? `${pagesDir}/_document.${existsSync4(join4(root, "tsconfig.json")) ? "tsx" : "js"}` };
|
|
@@ -27181,11 +27468,11 @@ function routeOf(rel2) {
|
|
|
27181
27468
|
}
|
|
27182
27469
|
return null;
|
|
27183
27470
|
}
|
|
27184
|
-
function findStripeKey(env2, processEnv) {
|
|
27471
|
+
function findStripeKey(env2, processEnv, envFile = {}) {
|
|
27185
27472
|
const sources = [];
|
|
27186
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"]) {
|
|
27187
27474
|
sources.push([`${k} (environment)`, processEnv[k]]);
|
|
27188
|
-
sources.push([`${k} (.env)`, env2[k]]);
|
|
27475
|
+
sources.push([`${k} (${envFile[k] ?? ".env"})`, env2[k]]);
|
|
27189
27476
|
}
|
|
27190
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()]);
|
|
27191
27478
|
const hint = (key) => `${key.slice(0, 8)}\u2026${key.slice(-4)}`;
|
|
@@ -27285,11 +27572,15 @@ function detectProject(root, processEnv = {}) {
|
|
|
27285
27572
|
const { framework, layoutFile } = detectFramework(root, deps);
|
|
27286
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;
|
|
27287
27574
|
const env2 = {};
|
|
27575
|
+
const envFile = {};
|
|
27288
27576
|
for (const f of [".env", ".env.production", ".env.local", ".env.development", ".env.development.local"]) {
|
|
27289
27577
|
const t = readText(join4(root, f));
|
|
27290
|
-
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;
|
|
27291
27582
|
}
|
|
27292
|
-
|
|
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";
|
|
27293
27584
|
const checkoutCode = [];
|
|
27294
27585
|
const payButtons = [];
|
|
27295
27586
|
const checkoutSessions = [];
|
|
@@ -27319,8 +27610,9 @@ function detectProject(root, processEnv = {}) {
|
|
|
27319
27610
|
checkoutSessions.push({ file: rel2, line: line2, ...parsed.value ? { params: redactSecretsDeep(parsed.value) } : {}, unresolved: parsed.unresolved });
|
|
27320
27611
|
}
|
|
27321
27612
|
});
|
|
27613
|
+
if (billingPlatform === "custom" && checkoutCode.some((c) => c.kind.startsWith("stripe_"))) billingPlatform = "stripe";
|
|
27322
27614
|
const site = detectSiteUrl(root, env2, pkg);
|
|
27323
|
-
const stripeKey = findStripeKey(env2, processEnv);
|
|
27615
|
+
const stripeKey = findStripeKey(env2, processEnv, envFile);
|
|
27324
27616
|
const brand = detectBrand(root, layoutFile, typeof pkg?.name === "string" ? pkg.name : void 0);
|
|
27325
27617
|
return {
|
|
27326
27618
|
root,
|
|
@@ -27341,16 +27633,18 @@ function detectProject(root, processEnv = {}) {
|
|
|
27341
27633
|
...stripeKey.refused ? { stripeKeyRefused: stripeKey.refused } : {},
|
|
27342
27634
|
...brand ? { brand } : {},
|
|
27343
27635
|
plans: plansFromSessions(checkoutSessions),
|
|
27344
|
-
env: env2
|
|
27636
|
+
env: env2,
|
|
27637
|
+
mailer: detectMailer(deps, framework, !!pkg)
|
|
27345
27638
|
};
|
|
27346
27639
|
}
|
|
27347
|
-
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;
|
|
27348
27641
|
var init_detect = __esm({
|
|
27349
27642
|
"src/commands/init/detect.ts"() {
|
|
27350
27643
|
"use strict";
|
|
27351
27644
|
init_src2();
|
|
27352
27645
|
init_literal();
|
|
27353
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"];
|
|
27354
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__"]);
|
|
27355
27649
|
CODE_EXT = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|liquid|html|ejs|hbs|erb)$/i;
|
|
27356
27650
|
MAX_FILES = 4e3;
|
|
@@ -27366,6 +27660,7 @@ var init_detect = __esm({
|
|
|
27366
27660
|
CHECKOUT_PATTERNS = [
|
|
27367
27661
|
{ kind: "stripe_checkout_session", re: /checkout\.sessions\.create\s*\(/ },
|
|
27368
27662
|
{ kind: "stripe_payment_link", re: /paymentLinks\.create\s*\(|https:\/\/buy\.stripe\.com\// },
|
|
27663
|
+
{ kind: "stripe_checkout_link", re: /https:\/\/checkout\.stripe\.com\// },
|
|
27369
27664
|
{ kind: "stripe_pricing_table", re: /<stripe-pricing-table\b/ },
|
|
27370
27665
|
{ kind: "stripe_subscription_api", re: /subscriptions\.create\s*\(/ },
|
|
27371
27666
|
{ kind: "stripe_elements", re: /<(PaymentElement|CardElement|ExpressCheckoutElement)\b|elements\.create\(\s*['"]payment['"]/ },
|
|
@@ -27380,6 +27675,47 @@ var init_detect = __esm({
|
|
|
27380
27675
|
// src/ci/scaffold.ts
|
|
27381
27676
|
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
|
|
27382
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
|
+
}
|
|
27383
27719
|
function walk3(root, visit) {
|
|
27384
27720
|
let n = 0;
|
|
27385
27721
|
const go = (dir, depth) => {
|
|
@@ -27429,7 +27765,8 @@ function plansFromLiterals(text, file) {
|
|
|
27429
27765
|
const trial = /\btrial(?:_period)?_?days\s*:\s*(\d+)/i.exec(body)?.[1];
|
|
27430
27766
|
if (out.some((p) => p.name === name)) continue;
|
|
27431
27767
|
const key = m[1]?.trim() && /^[A-Za-z_$][\w$]*$/.test(m[1].trim()) ? m[1].trim() : void 0;
|
|
27432
|
-
|
|
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 });
|
|
27433
27770
|
}
|
|
27434
27771
|
return out;
|
|
27435
27772
|
}
|
|
@@ -27480,10 +27817,84 @@ function pageSource(root, built) {
|
|
|
27480
27817
|
const hits = bases.flatMap((b) => [".tsx", ".jsx", ".ts", ".js", ".svelte", ".vue", ".astro", ".mdx"].map((e) => `${b}${e}`)).filter((f) => existsSync5(join5(root, f)));
|
|
27481
27818
|
return hits.length === 1 ? hits[0] : void 0;
|
|
27482
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
|
+
}
|
|
27483
27893
|
function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
27484
27894
|
const det = detectProject(root, env2);
|
|
27485
27895
|
const todos = [];
|
|
27486
27896
|
const found = [];
|
|
27897
|
+
const notes2 = [];
|
|
27487
27898
|
const names = /* @__PURE__ */ new Set();
|
|
27488
27899
|
const merchant = {};
|
|
27489
27900
|
const fact = (k) => typeof facts[k] === "string" && facts[k].trim() ? facts[k].trim() : void 0;
|
|
@@ -27511,8 +27922,21 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27511
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));
|
|
27512
27923
|
return m ? String(m.name) : void 0;
|
|
27513
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));
|
|
27514
27938
|
const plans = det.plans.map((p, i) => ({
|
|
27515
|
-
name: p.name ?? initName(p) ?? `Plan ${i + 1}`,
|
|
27939
|
+
name: p.name ?? initName(p) ?? literalFor(p)?.name ?? `Plan ${i + 1}`,
|
|
27516
27940
|
amount: p.amount,
|
|
27517
27941
|
currency: p.currency,
|
|
27518
27942
|
interval: p.interval,
|
|
@@ -27520,31 +27944,28 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27520
27944
|
...p.trial_days ? { trial_days: p.trial_days } : {}
|
|
27521
27945
|
}));
|
|
27522
27946
|
for (const p of det.plans) found.push(`plan ${p.name ?? ""} ${p.amount / 100} ${p.currency}/${p.interval} (${p.source})`.replace(/\s+/g, " "));
|
|
27523
|
-
const planKeys = /* @__PURE__ */ new Map();
|
|
27524
27947
|
if (!plans.length && Array.isArray(facts.plans)) {
|
|
27525
27948
|
for (const [i, p] of facts.plans.entries()) {
|
|
27526
27949
|
if (typeof p.amount !== "number" || typeof p.interval !== "string") continue;
|
|
27527
|
-
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 } : {} });
|
|
27528
27951
|
found.push(`plan ${String(plans[plans.length - 1].name)} (from checkbox init --plan)`);
|
|
27529
27952
|
}
|
|
27530
27953
|
}
|
|
27531
27954
|
if (!plans.length) {
|
|
27532
|
-
|
|
27533
|
-
|
|
27534
|
-
|
|
27535
|
-
|
|
27536
|
-
|
|
27537
|
-
|
|
27538
|
-
|
|
27539
|
-
|
|
27540
|
-
|
|
27541
|
-
|
|
27542
|
-
|
|
27543
|
-
|
|
27544
|
-
|
|
27545
|
-
|
|
27546
|
-
}
|
|
27547
|
-
});
|
|
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]);
|
|
27548
27969
|
}
|
|
27549
27970
|
if (!plans.length) {
|
|
27550
27971
|
plans.push({ name: "TODO: plan name", price: "TODO: e.g. 19.99", currency: "usd", interval: "month" });
|
|
@@ -27588,11 +28009,20 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27588
28009
|
} else if (s2.unresolved.length) found.push(`${s2.file}:${s2.line}: Checkout Session (${s2.unresolved.length} value(s) are expressions, read as-is)`);
|
|
27589
28010
|
else found.push(`${s2.file}:${s2.line}: Checkout Session`);
|
|
27590
28011
|
entry.strict = true;
|
|
27591
|
-
const
|
|
27592
|
-
if (
|
|
27593
|
-
|
|
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) {
|
|
27594
28021
|
for (const p of plans) stripe.push({ ...entry, name: unique(names, `${String(entry.name).replace(/\)$/, "")}: ${String(p.name)})`), plan: String(p.name) });
|
|
27595
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})` : ""}`);
|
|
27596
28026
|
} else stripe.push(entry);
|
|
27597
28027
|
}
|
|
27598
28028
|
const emails = [];
|
|
@@ -27602,12 +28032,22 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27602
28032
|
if (IN_EMAIL_DIR.test(rel2)) {
|
|
27603
28033
|
const kind = emailKindOf(rel2);
|
|
27604
28034
|
if (/\.(?:html?|txt|md)$/i.test(rel2) && kind) {
|
|
27605
|
-
|
|
27606
|
-
|
|
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")` : ""}`);
|
|
27607
28047
|
return;
|
|
27608
28048
|
}
|
|
27609
28049
|
if (/\.[jt]sx$|\.mjml$/i.test(rel2) && kind) {
|
|
27610
|
-
|
|
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)`);
|
|
27611
28051
|
return;
|
|
27612
28052
|
}
|
|
27613
28053
|
}
|
|
@@ -27631,8 +28071,22 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27631
28071
|
}
|
|
27632
28072
|
}
|
|
27633
28073
|
});
|
|
27634
|
-
|
|
27635
|
-
|
|
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`);
|
|
27636
28090
|
}
|
|
27637
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)');
|
|
27638
28092
|
const checks = {};
|
|
@@ -27642,20 +28096,24 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27642
28096
|
if (emails.length) checks.emails = emails;
|
|
27643
28097
|
checks.notice_schedule = true;
|
|
27644
28098
|
if (!stripe.length && !disclosures.length && !pages.length && !emails.length) {
|
|
27645
|
-
|
|
28099
|
+
notes2.push('No checkout code, email templates or pages were found: add entries under "checks" (https://checkbox.site/docs/guides/ci#config)');
|
|
27646
28100
|
}
|
|
27647
28101
|
return {
|
|
27648
28102
|
config: { $schema: SCHEMA_URL, merchant, jurisdictions: "all", plans, checks },
|
|
27649
28103
|
todos,
|
|
27650
|
-
found
|
|
28104
|
+
found,
|
|
28105
|
+
notes: notes2,
|
|
28106
|
+
todoValues: countTodoValues({ merchant, plans, checks })
|
|
27651
28107
|
};
|
|
27652
28108
|
}
|
|
27653
|
-
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;
|
|
27654
28110
|
var init_scaffold = __esm({
|
|
27655
28111
|
"src/ci/scaffold.ts"() {
|
|
27656
28112
|
"use strict";
|
|
27657
28113
|
init_detect();
|
|
28114
|
+
init_preflight();
|
|
27658
28115
|
init_config3();
|
|
28116
|
+
PAGE_ROUTE = /^(pricing|checkout|subscribe|plans)$/i;
|
|
27659
28117
|
IGNORE = /* @__PURE__ */ new Set(["node_modules", ".git", ".next", ".nuxt", ".svelte-kit", ".output", ".vercel", ".netlify", ".turbo", ".cache", "coverage", ".astro", "vendor", ".venv", "__pycache__", ".github"]);
|
|
27660
28118
|
MAX_FILES2 = 5e3;
|
|
27661
28119
|
EMAIL_NAMES = [
|
|
@@ -28161,6 +28619,53 @@ function pageJob(loaded, e, o, root) {
|
|
|
28161
28619
|
}
|
|
28162
28620
|
return job;
|
|
28163
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
|
+
}
|
|
28164
28669
|
async function emailJob(loaded, e, o, root, planName = e.plan, name = e.name) {
|
|
28165
28670
|
const { config, dir } = loaded;
|
|
28166
28671
|
const job = { name, type: "email", ...planName ? { plan: planName } : {}, target: { file: rel(loaded, root, loaded.path), line: entryLine(loaded.raw, e.name) }, inputs: [], warnings: [] };
|
|
@@ -28181,6 +28686,8 @@ async function emailJob(loaded, e, o, root, planName = e.plan, name = e.name) {
|
|
|
28181
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 } : {} };
|
|
28182
28687
|
} else throw new InputError(`${e.module} must return { subject, text | html } or a string`);
|
|
28183
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));
|
|
28184
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>`);
|
|
28185
28692
|
if (!msg2.text && !msg2.html) throw new InputError("the email body is empty");
|
|
28186
28693
|
const terms2 = { ...planTerms(config, pickPlan(config, planName)) };
|
|
@@ -28606,6 +29113,7 @@ var init_timing = __esm({
|
|
|
28606
29113
|
var notice_schedule_exports = {};
|
|
28607
29114
|
__export(notice_schedule_exports, {
|
|
28608
29115
|
computeNoticeSchedule: () => computeNoticeSchedule,
|
|
29116
|
+
noticesKindsNote: () => noticesKindsNote,
|
|
28609
29117
|
singleDayNote: () => singleDayNote
|
|
28610
29118
|
});
|
|
28611
29119
|
function singleDayNote(requirementIds, days) {
|
|
@@ -28714,7 +29222,12 @@ function computeNoticeSchedule(input, now = /* @__PURE__ */ new Date()) {
|
|
|
28714
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);
|
|
28715
29223
|
else notRequired.push({ kind: "annual_terms", reason: "No applicable requirement demands a yearly terms reminder under these jurisdictions." });
|
|
28716
29224
|
}
|
|
28717
|
-
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) {
|
|
28718
29231
|
notRequired.push({ kind: "price_change", reason: "No price change planned. Pass price_change (effective_date, new_price) to schedule its advance notice." });
|
|
28719
29232
|
} else {
|
|
28720
29233
|
const eff = parseDate(input.price_change.effective_date, "price_change.effective_date", start);
|
|
@@ -28741,6 +29254,24 @@ function computeNoticeSchedule(input, now = /* @__PURE__ */ new Date()) {
|
|
|
28741
29254
|
}
|
|
28742
29255
|
};
|
|
28743
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
|
+
}
|
|
28744
29275
|
var CITATION, BY_ID2, day, unix;
|
|
28745
29276
|
var init_notice_schedule = __esm({
|
|
28746
29277
|
"../../apps/api/src/v1/checks/notice-schedule.ts"() {
|
|
@@ -28781,8 +29312,15 @@ function operationFor2(kind) {
|
|
|
28781
29312
|
function onlineEngine(client) {
|
|
28782
29313
|
return { mode: "online", run: (kind, body) => client.sdk.call(operationFor2(kind), body) };
|
|
28783
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
|
+
}
|
|
28784
29322
|
function ciVocabulary(text) {
|
|
28785
|
-
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(/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).');
|
|
28786
29324
|
}
|
|
28787
29325
|
function pricingPageFixes(fixes) {
|
|
28788
29326
|
const reworded = fixes.map(
|
|
@@ -28833,8 +29371,8 @@ function fixesOf(r, onlyNeedsFix = false) {
|
|
|
28833
29371
|
const plain = cards.length > 1 ? cardClauses(c.explanation).filter((x) => /look like the surrounding text/.test(x.clause)).map((x) => x.card) : [];
|
|
28834
29372
|
if (plain.length && plain.length < cards.length && /renewal line/i.test(base)) {
|
|
28835
29373
|
cards = cards.filter((x) => !plain.includes(x));
|
|
28836
|
-
const standOut = `Plan card ${plain.map((x) => `\u201C${x}\u201D`).join(", ")}: ${
|
|
28837
|
-
if (!out.includes(standOut)) out.push(standOut);
|
|
29374
|
+
const standOut = `Plan card ${plain.map((x) => `\u201C${x}\u201D`).join(", ")}: ${STAND_OUT_DECISION}`;
|
|
29375
|
+
if (!onlyNeedsFix && !out.includes(standOut)) out.push(standOut);
|
|
28838
29376
|
}
|
|
28839
29377
|
const text = cards.length && base ? `Plan card ${cards.map((x) => `\u201C${x}\u201D`).join(", ")}: ${base}` : base;
|
|
28840
29378
|
if (text && !out.includes(text)) out.push(text);
|
|
@@ -28862,7 +29400,7 @@ function withPlaceholders(params, placeholders) {
|
|
|
28862
29400
|
}
|
|
28863
29401
|
return copy;
|
|
28864
29402
|
}
|
|
28865
|
-
function correctedOf(type, r, partial = false, placeholders) {
|
|
29403
|
+
function correctedOf(type, r, partial = false, placeholders, pricingPage = false) {
|
|
28866
29404
|
if (type === "stripe_checkout" && r.correction?.patch?.length && partial) {
|
|
28867
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) };
|
|
28868
29406
|
}
|
|
@@ -28875,8 +29413,10 @@ function correctedOf(type, r, partial = false, placeholders) {
|
|
|
28875
29413
|
return {
|
|
28876
29414
|
format: "text",
|
|
28877
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.
|
|
28878
29418
|
body: perPlan.map((p) => `${p.plan}:
|
|
28879
|
-
${p.disclosure_text}${p.consent_label ? `
|
|
29419
|
+
${p.disclosure_text}${p.consent_label && !pricingPage ? `
|
|
28880
29420
|
Consent checkbox label: ${p.consent_label}` : ""}`).join("\n\n")
|
|
28881
29421
|
};
|
|
28882
29422
|
}
|
|
@@ -28893,7 +29433,8 @@ Consent checkbox label: ${r.replacement.consent_label}` : ""}`
|
|
|
28893
29433
|
};
|
|
28894
29434
|
}
|
|
28895
29435
|
if (type === "email" && r.template && ciStatusOf(r) !== "pass" && r.verdict !== "not_applicable") {
|
|
28896
|
-
|
|
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}
|
|
28897
29438
|
|
|
28898
29439
|
${r.template.text}` };
|
|
28899
29440
|
}
|
|
@@ -28937,13 +29478,20 @@ function noticeCoverage(config, planName, schedule, opts = {}) {
|
|
|
28937
29478
|
const required = /* @__PURE__ */ new Map();
|
|
28938
29479
|
for (const n of schedule.notices ?? []) {
|
|
28939
29480
|
const k = NOTICE_TO_EMAIL[n.kind];
|
|
28940
|
-
if (k && !required.has(k)) required.set(k, { occasion: n.occasion, days: n.days_before });
|
|
28941
|
-
}
|
|
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
|
+
};
|
|
28942
29490
|
const missing = [...required].filter(([k]) => !have.has(k));
|
|
28943
29491
|
if (!required.size) return { status: "not_applicable", label: "No reminder emails required", fixes: [] };
|
|
28944
29492
|
const sent2 = [...required.keys()].filter((k) => byCheckbox.has(k)).length;
|
|
28945
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: [] };
|
|
28946
|
-
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)}`);
|
|
28947
29495
|
return { status: "needs_review", label: `${missing.length} required email${missing.length === 1 ? "" : "s"} not in checkbox.json`, fix: fixes[0], fixes };
|
|
28948
29496
|
}
|
|
28949
29497
|
function unverifiedOffer(r) {
|
|
@@ -28970,6 +29518,11 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
28970
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 } : {} };
|
|
28971
29519
|
}
|
|
28972
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
|
+
}
|
|
28973
29526
|
const alerts = job.warnings.filter((w) => /^The params and checkbox\.json disagree/.test(w));
|
|
28974
29527
|
for (const w of report.warnings ?? []) {
|
|
28975
29528
|
const text = ciVocabulary(w.message);
|
|
@@ -28980,6 +29533,12 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
28980
29533
|
const status = ciStatusOf(report);
|
|
28981
29534
|
const params = job.type === "stripe_checkout" ? job.body?.params ?? void 0 : void 0;
|
|
28982
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
|
+
}
|
|
28983
29542
|
const pricingPage = job.type === "page" && job.body?.page_kind === "pricing";
|
|
28984
29543
|
if (pricingPage) fixes = pricingPageFixes(fixes);
|
|
28985
29544
|
const unreadText = (job.unresolved ?? []).filter((p) => /^(?:custom_text|consent_collection|discounts|subscription_data|allow_promotion_codes|payment_method_collection)\b/.test(p));
|
|
@@ -29024,7 +29583,7 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
29024
29583
|
);
|
|
29025
29584
|
}
|
|
29026
29585
|
const finalStatus = unread ? literal.length ? "fail" : "needs_review" : declaredCoupon || unreadCoupon ? "needs_review" : status;
|
|
29027
|
-
const corrected = unread ? void 0 : correctedOf(job.type, report, job.partial, job.placeholders);
|
|
29586
|
+
const corrected = unread ? void 0 : correctedOf(job.type, report, job.partial, job.placeholders, pricingPage);
|
|
29028
29587
|
if (corrected?.format === "json" && job.placeholders) {
|
|
29029
29588
|
const unset = job.warnings.filter((w) => /(?:was|were) not set when Checkbox ran/.test(w));
|
|
29030
29589
|
if (unset.length) Object.assign(base, { alerts: [...unset, ...base.alerts ?? []] });
|
|
@@ -29104,10 +29663,11 @@ async function runCi(jobs, engine, opts) {
|
|
|
29104
29663
|
checks,
|
|
29105
29664
|
...versioned?.rules_version ? { rules_version: versioned.rules_version } : {},
|
|
29106
29665
|
...versioned?.catalog_version ? { catalog_version: versioned.catalog_version } : {},
|
|
29107
|
-
disclaimer: CI_DISCLAIMER
|
|
29666
|
+
disclaimer: CI_DISCLAIMER,
|
|
29667
|
+
result: resultOf(status, exit, opts.strict)
|
|
29108
29668
|
};
|
|
29109
29669
|
}
|
|
29110
|
-
var ENGINE_KIND, CI_DISCLAIMER, RANK2, OK_STATUSES,
|
|
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;
|
|
29111
29671
|
var init_runner = __esm({
|
|
29112
29672
|
"src/ci/runner.ts"() {
|
|
29113
29673
|
"use strict";
|
|
@@ -29124,10 +29684,11 @@ var init_runner = __esm({
|
|
|
29124
29684
|
email: "email",
|
|
29125
29685
|
notice_schedule: "notice_schedule"
|
|
29126
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.";
|
|
29127
29688
|
CI_DISCLAIMER = "Automated check against Checkbox's catalog of automatic-renewal laws; not legal advice.";
|
|
29128
29689
|
RANK2 = { fail: 0, needs_review: 1 };
|
|
29129
29690
|
OK_STATUSES = /* @__PURE__ */ new Set(["compliant", "pending_live_check", "verified_live", "not_applicable"]);
|
|
29130
|
-
|
|
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.";
|
|
29131
29692
|
NOTICE_TO_EMAIL = {
|
|
29132
29693
|
acknowledgment: "acknowledgment",
|
|
29133
29694
|
trial_ending: "trial_reminder",
|
|
@@ -29172,21 +29733,28 @@ function renderHuman(run2, c, opts = {}) {
|
|
|
29172
29733
|
const lines = [];
|
|
29173
29734
|
const versions = run2.rules_version ? ` \xB7 rules ${run2.rules_version} \xB7 catalog ${run2.catalog_version}` : "";
|
|
29174
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 = [];
|
|
29175
29737
|
const width = Math.min(40, Math.max(...run2.checks.map((x) => x.name.length), 10));
|
|
29176
29738
|
for (const x of run2.checks) {
|
|
29177
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);
|
|
29178
29740
|
lines.push(` ${icon(x.status, c)} ${x.name.padEnd(width)} \u2192 ${label}`);
|
|
29179
29741
|
if (x.message) lines.push(c.dim(` ${wrap(x.message, 110, " ")}`));
|
|
29180
29742
|
if (x.fix) lines.push(` fix: ${wrap(x.fix, 110, " ")}${c.dim(` (${where(x)})`)}`);
|
|
29181
|
-
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, " ")}`);
|
|
29182
29744
|
const alerts = x.alerts ?? [];
|
|
29183
29745
|
for (const a of alerts) lines.push(c.yellow(` warning: ${wrap(a, 110, " ")}`));
|
|
29184
|
-
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
|
+
}
|
|
29185
29751
|
if (x.corrected && opts.verbose) lines.push(c.dim(` ${x.corrected.title}:`), ...x.corrected.body.split("\n").map((l) => c.dim(` ${l}`)));
|
|
29186
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, " ")));
|
|
29187
29754
|
const tone = run2.status === "pass" ? c.green : run2.status === "needs_review" && run2.exit_code === 0 ? c.yellow : c.red;
|
|
29188
29755
|
if (run2.status === "skipped") lines.push("", c.red(wrap(nothingCheckedMessage(run2), 110, "")));
|
|
29189
|
-
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+\)$/, "")})` : ""}`);
|
|
29190
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."));
|
|
29191
29759
|
lines.push(c.dim(run2.disclaimer));
|
|
29192
29760
|
return lines;
|
|
@@ -29211,6 +29779,8 @@ function stepSummary(run2) {
|
|
|
29211
29779
|
const versions = run2.rules_version ? ` \xB7 rules ${run2.rules_version} \xB7 catalog ${run2.catalog_version}` : "";
|
|
29212
29780
|
lines.push(`**${overallLine(run2)}** \u2014 \`${run2.config}\` \xB7 ${run2.mode}${versions}`, "");
|
|
29213
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}`, "");
|
|
29214
29784
|
lines.push("| Check | Status | Fix | File |", "|---|---|---|---|");
|
|
29215
29785
|
for (const x of run2.checks) {
|
|
29216
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 : "";
|
|
@@ -29233,7 +29803,7 @@ function stepSummary(run2) {
|
|
|
29233
29803
|
function escapeHtml(s2) {
|
|
29234
29804
|
return s2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
29235
29805
|
}
|
|
29236
|
-
var TYPE_LABEL, where, escData, escProp, cell, STATUS_MD;
|
|
29806
|
+
var TYPE_LABEL, where, PRICE_UNREAD, escData, escProp, cell, STATUS_MD;
|
|
29237
29807
|
var init_report = __esm({
|
|
29238
29808
|
"src/ci/report.ts"() {
|
|
29239
29809
|
"use strict";
|
|
@@ -29247,6 +29817,7 @@ var init_report = __esm({
|
|
|
29247
29817
|
notice_schedule: "Notice schedule"
|
|
29248
29818
|
};
|
|
29249
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/;
|
|
29250
29821
|
escData = (s2) => s2.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
29251
29822
|
escProp = (s2) => escData(s2).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
29252
29823
|
cell = (s2) => s2.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
|
|
@@ -29429,6 +30000,8 @@ __export(ci_exports, {
|
|
|
29429
30000
|
attachSiteForCoupons: () => attachSiteForCoupons,
|
|
29430
30001
|
couponIdsIn: () => couponIdsIn,
|
|
29431
30002
|
default: () => ci_default,
|
|
30003
|
+
siteFactsFor: () => siteFactsFor,
|
|
30004
|
+
termsChanged: () => termsChanged,
|
|
29432
30005
|
touchesConfig: () => touchesConfig,
|
|
29433
30006
|
undiffableInputs: () => undiffableInputs
|
|
29434
30007
|
});
|
|
@@ -29436,6 +30009,16 @@ import { execFileSync as execFileSync2 } from "node:child_process";
|
|
|
29436
30009
|
import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync2, realpathSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
29437
30010
|
import { createInterface } from "node:readline";
|
|
29438
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
|
+
}
|
|
29439
30022
|
function gitChanged(cwd, base) {
|
|
29440
30023
|
try {
|
|
29441
30024
|
const root = execFileSync2("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
@@ -29505,6 +30088,36 @@ async function confirm(question) {
|
|
|
29505
30088
|
rl.close();
|
|
29506
30089
|
}
|
|
29507
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
|
+
}
|
|
29508
30121
|
async function runInit(ctx, argv) {
|
|
29509
30122
|
const p = parseArgs(argv, { boolean: ["yes", "force", "dry-run", "workflow"], string: ["config"], alias: { y: "yes" } });
|
|
29510
30123
|
if (p.positionals.length) throw new UsageError(`Unexpected argument "${p.positionals[0]}"`);
|
|
@@ -29515,13 +30128,24 @@ async function runInit(ctx, argv) {
|
|
|
29515
30128
|
const withWorkflow = bool(p.flags, "workflow") !== false;
|
|
29516
30129
|
const projects = ctx.config.load().init?.projects ?? {};
|
|
29517
30130
|
let facts = {};
|
|
30131
|
+
let setupSession;
|
|
29518
30132
|
for (let dir = resolve7(ctx.cwd); ; dir = dirname7(dir)) {
|
|
29519
|
-
|
|
29520
|
-
|
|
30133
|
+
const proj = projects[dir];
|
|
30134
|
+
if (proj?.facts || proj?.setup_session) {
|
|
30135
|
+
facts = proj.facts ?? {};
|
|
30136
|
+
setupSession = proj.setup_session;
|
|
29521
30137
|
break;
|
|
29522
30138
|
}
|
|
29523
30139
|
if (dirname7(dir) === dir) break;
|
|
29524
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
|
+
}
|
|
29525
30149
|
const s2 = scaffoldConfig(ctx.cwd, ctx.env, facts);
|
|
29526
30150
|
const actionFor = (f) => existsSync8(f) ? force ? "overwrite" : "keep" : "create";
|
|
29527
30151
|
const files = [{ path: configPath, content: `${JSON.stringify(s2.config, null, 2)}
|
|
@@ -29553,8 +30177,13 @@ async function runInit(ctx, argv) {
|
|
|
29553
30177
|
found: s2.found,
|
|
29554
30178
|
...withWorkflow ? { workflow_steps: workflowSteps } : {},
|
|
29555
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) } : {},
|
|
29556
30185
|
config: s2.config,
|
|
29557
|
-
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." }
|
|
29558
30187
|
};
|
|
29559
30188
|
ctx.out.result(result, () => {
|
|
29560
30189
|
const lines = [];
|
|
@@ -29564,6 +30193,7 @@ async function runInit(ctx, argv) {
|
|
|
29564
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.`);
|
|
29565
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".');
|
|
29566
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}`));
|
|
29567
30197
|
lines.push("", `${ctx.out.c.cyan("Next:")} ${result.next_action.message}`);
|
|
29568
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}`)));
|
|
29569
30199
|
return lines;
|
|
@@ -29728,13 +30358,17 @@ ${message}
|
|
|
29728
30358
|
const engine = client ? onlineEngine(client) : offlineEngine();
|
|
29729
30359
|
const notices = client && declaredByCheckbox(loaded.config).length ? await readNotices(ctx, client, str(p.flags, "site")) : void 0;
|
|
29730
30360
|
const noticesLine = Math.max(1, loaded.raw.split("\n").findIndex((l) => l.includes('"sent_by_checkbox"')) + 1);
|
|
29731
|
-
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;
|
|
29732
30364
|
ctx.out.result(run2, () => renderHuman(run2, ctx.out.c, { verbose: bool(p.flags, "verbose") === true }));
|
|
29733
30365
|
if (gha) {
|
|
29734
30366
|
for (const a of annotations(run2)) emit(a);
|
|
29735
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}`);
|
|
29736
30369
|
writeGithubFile(ctx.env.GITHUB_OUTPUT, `status=${run2.status}
|
|
29737
30370
|
exit_code=${run2.exit_code}
|
|
30371
|
+
result=${run2.result}
|
|
29738
30372
|
`);
|
|
29739
30373
|
}
|
|
29740
30374
|
return run2.exit_code;
|
|
@@ -29794,6 +30428,14 @@ function findAnchor(src, target) {
|
|
|
29794
30428
|
}
|
|
29795
30429
|
}
|
|
29796
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
|
+
}
|
|
29797
30439
|
return null;
|
|
29798
30440
|
}
|
|
29799
30441
|
function lineStart(src, idx) {
|
|
@@ -30243,6 +30885,10 @@ function planSnippet(d, s2) {
|
|
|
30243
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") } };
|
|
30244
30886
|
}
|
|
30245
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
|
+
}
|
|
30246
30892
|
if (d.framework === "nextjs-pages") {
|
|
30247
30893
|
const contents = NEXT_DOCUMENT(s2);
|
|
30248
30894
|
return { change: { file, action: "create", summary: "Custom _document with the snippet in <Head>", contents, added: contents.trim().split("\n") } };
|
|
@@ -30371,7 +31017,7 @@ function applyChange(root, c) {
|
|
|
30371
31017
|
mkdirSync3(dirname8(abs), { recursive: true });
|
|
30372
31018
|
writeFileSync4(abs, c.contents, c.file.startsWith(".env") ? { mode: 384 } : void 0);
|
|
30373
31019
|
}
|
|
30374
|
-
var MARK_START, MARK_END, NEXT_DOCUMENT, NUXT_PLUGIN;
|
|
31020
|
+
var MARK_START, MARK_END, NEXT_DOCUMENT, NEXT_APP_LAYOUT, NUXT_PLUGIN;
|
|
30375
31021
|
var init_patch2 = __esm({
|
|
30376
31022
|
"src/commands/init/patch.ts"() {
|
|
30377
31023
|
"use strict";
|
|
@@ -30392,6 +31038,17 @@ ${jsxBlock(s2, " ").join("\n")}
|
|
|
30392
31038
|
</Html>
|
|
30393
31039
|
);
|
|
30394
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
|
+
}
|
|
30395
31052
|
`;
|
|
30396
31053
|
NUXT_PLUGIN = (s2) => `// ${MARK_START} \u2014 Checkbox subscription-compliance monitoring (https://checkbox.site/docs/snippet)
|
|
30397
31054
|
export default defineNuxtPlugin(() => {
|
|
@@ -30412,8 +31069,11 @@ __export(init_exports, {
|
|
|
30412
31069
|
configLines: () => configLines,
|
|
30413
31070
|
configNameOf: () => configNameOf,
|
|
30414
31071
|
default: () => init_default,
|
|
31072
|
+
liveKeyNextAction: () => liveKeyNextAction,
|
|
30415
31073
|
parsePlanFlag: () => parsePlanFlag,
|
|
30416
|
-
|
|
31074
|
+
snippetWords: () => snippetWords,
|
|
31075
|
+
syncCheckoutConfigs: () => syncCheckoutConfigs,
|
|
31076
|
+
withEvaluatedAt: () => withEvaluatedAt
|
|
30417
31077
|
});
|
|
30418
31078
|
import { existsSync as existsSync11 } from "node:fs";
|
|
30419
31079
|
import { join as join11 } from "node:path";
|
|
@@ -30433,6 +31093,10 @@ function saveState(ctx, root, patch) {
|
|
|
30433
31093
|
}
|
|
30434
31094
|
ctx.config.save({ init: { ...cfg, projects } });
|
|
30435
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
|
+
}
|
|
30436
31100
|
async function ask2(question) {
|
|
30437
31101
|
const rl = createInterface2({ input: process.stdin, output: process.stderr, terminal: true });
|
|
30438
31102
|
try {
|
|
@@ -30460,6 +31124,7 @@ function projectFacts(d) {
|
|
|
30460
31124
|
...d.layoutFile ? { layout_file: d.layoutFile } : {},
|
|
30461
31125
|
env_file: d.envFile,
|
|
30462
31126
|
...d.publicDir ? { public_dir: d.publicDir } : {},
|
|
31127
|
+
...d.mailer ? { mailer: d.mailer } : {},
|
|
30463
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) })),
|
|
30464
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) }))
|
|
30465
31130
|
};
|
|
@@ -30547,6 +31212,20 @@ function registrable(params, unresolved) {
|
|
|
30547
31212
|
if (unresolved.some((u) => /^(mode|line_items|subscription_data|custom_text|consent_collection)\b/.test(u))) return false;
|
|
30548
31213
|
return params.mode === "subscription" && Array.isArray(params.line_items) && params.line_items.length > 0;
|
|
30549
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
|
+
}
|
|
30550
31229
|
function detectionSummary(d, siteUrl) {
|
|
30551
31230
|
return {
|
|
30552
31231
|
framework: d.framework,
|
|
@@ -30569,9 +31248,14 @@ function detectionSummary(d, siteUrl) {
|
|
|
30569
31248
|
function fileOut(c) {
|
|
30570
31249
|
return { file: c.file, action: c.action, summary: c.summary, added: c.added };
|
|
30571
31250
|
}
|
|
30572
|
-
function
|
|
31251
|
+
function resultOf2(status, d, session, changes, extra, apiBase) {
|
|
30573
31252
|
const names = envVarNames(d);
|
|
30574
|
-
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;
|
|
30575
31259
|
return {
|
|
30576
31260
|
object: "checkbox_init",
|
|
30577
31261
|
status,
|
|
@@ -30586,7 +31270,10 @@ function resultOf(status, d, session, changes, extra, apiBase) {
|
|
|
30586
31270
|
install: session.install,
|
|
30587
31271
|
stripe: { ...session.stripe, ...extra.stripe ?? {} },
|
|
30588
31272
|
...extra.live && Object.keys(extra.live).length ? { live: extra.live } : {},
|
|
30589
|
-
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 } : {},
|
|
30590
31277
|
steps: session.steps.map((s2) => ({ key: s2.key, status: s2.status, summary: s2.summary })),
|
|
30591
31278
|
warnings: extra.warnings ?? [],
|
|
30592
31279
|
owner_notified: session.owner_notified,
|
|
@@ -30609,6 +31296,10 @@ function renderSession(ctx, s2) {
|
|
|
30609
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})`)}`);
|
|
30610
31297
|
if (todo.length > 5) lines.push(c.dim(` \u2026 ${todo.length - 5} more: checkbox plan`));
|
|
30611
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
|
+
}
|
|
30612
31303
|
lines.push("", `${c.cyan("next")} ${s2.next_action.message}${s2.next_action.command ? `
|
|
30613
31304
|
${s2.next_action.command}` : ""}`, `${c.dim("review")} ${s2.review_url}`);
|
|
30614
31305
|
return lines;
|
|
@@ -30634,6 +31325,7 @@ var init_init = __esm({
|
|
|
30634
31325
|
"use strict";
|
|
30635
31326
|
init_args();
|
|
30636
31327
|
init_config();
|
|
31328
|
+
init_src2();
|
|
30637
31329
|
init_http();
|
|
30638
31330
|
init_types2();
|
|
30639
31331
|
init_detect();
|
|
@@ -30733,7 +31425,7 @@ var init_init = __esm({
|
|
|
30733
31425
|
if (bool(flags, "verify")) {
|
|
30734
31426
|
const v = await client2.sdk.setupSessions.verify(state.setup_session);
|
|
30735
31427
|
session2 = v.setup_session;
|
|
30736
|
-
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}`);
|
|
30737
31429
|
} else {
|
|
30738
31430
|
session2 = await client2.sdk.setupSessions.retrieve(state.setup_session);
|
|
30739
31431
|
}
|
|
@@ -30743,7 +31435,7 @@ var init_init = __esm({
|
|
|
30743
31435
|
const configs = await syncCheckoutConfigs(client2, session2.site, d.checkoutSessions, root).catch((err) => [{ file: "", line: 0, skipped: err instanceof Error ? err.message : String(err) }]);
|
|
30744
31436
|
if (configs.length) stripeOut.checkout_configs = configs;
|
|
30745
31437
|
}
|
|
30746
|
-
out.result(
|
|
31438
|
+
out.result(resultOf2("verified", d, session2, [], Object.keys(stripeOut).length ? { stripe: stripeOut } : {}, apiBase), () => [...renderSession(ctx, session2), ...configLines(stripeOut.checkout_configs)]);
|
|
30747
31439
|
return EXIT.ok;
|
|
30748
31440
|
}
|
|
30749
31441
|
if (dryRun && (!state.setup_session || !creds)) {
|
|
@@ -30772,10 +31464,11 @@ var init_init = __esm({
|
|
|
30772
31464
|
let token = null;
|
|
30773
31465
|
let apiKey = creds?.apiKey;
|
|
30774
31466
|
let sessionId = state.setup_session;
|
|
30775
|
-
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);
|
|
30776
31469
|
if (!reuse) {
|
|
30777
31470
|
const anon = ctx.client({ apiKey: "device-login", apiBase });
|
|
30778
|
-
let pending =
|
|
31471
|
+
let pending = pendingLogin;
|
|
30779
31472
|
if (!pending) {
|
|
30780
31473
|
const agent2 = detectAgent(ctx.env, agentName);
|
|
30781
31474
|
const wantStripe = bool(flags, "stripe") !== false && d.billingPlatform === "stripe" && !!d.stripeTestKey;
|
|
@@ -30788,7 +31481,8 @@ var init_init = __esm({
|
|
|
30788
31481
|
...merchantName ? { merchant_name: merchantName.slice(0, 100) } : {},
|
|
30789
31482
|
framework: d.framework,
|
|
30790
31483
|
...d.packageManager ? { package_manager: d.packageManager } : {},
|
|
30791
|
-
|
|
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 } : {},
|
|
30792
31486
|
...bool(flags, "live") ? { live: true } : {},
|
|
30793
31487
|
...wantStripe ? { stripe_test_key_hint: d.stripeTestKey.hint } : {}
|
|
30794
31488
|
});
|
|
@@ -30819,7 +31513,7 @@ var init_init = __esm({
|
|
|
30819
31513
|
expires_at: pending.expires_at,
|
|
30820
31514
|
next_action: {
|
|
30821
31515
|
type: "ask_human_to_approve",
|
|
30822
|
-
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"}).`,
|
|
30823
31517
|
command: "checkbox init --yes --json",
|
|
30824
31518
|
url: pending.verification_uri_complete
|
|
30825
31519
|
}
|
|
@@ -30962,9 +31656,12 @@ var init_init = __esm({
|
|
|
30962
31656
|
}
|
|
30963
31657
|
const live = {};
|
|
30964
31658
|
if (!dryRun && bool(flags, "live") && siteUrl) {
|
|
30965
|
-
const liveKey = token?.live_secret_key ?? void 0;
|
|
30966
|
-
if (!liveKey) {
|
|
30967
|
-
|
|
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}`);
|
|
30968
31665
|
} else {
|
|
30969
31666
|
try {
|
|
30970
31667
|
const lc = ctx.client({ apiKey: liveKey, apiBase });
|
|
@@ -30978,7 +31675,7 @@ var init_init = __esm({
|
|
|
30978
31675
|
changes.push(wk);
|
|
30979
31676
|
}
|
|
30980
31677
|
}
|
|
30981
|
-
session = await
|
|
31678
|
+
session = await lc.sdk.setupSessions.update(sessionId, { live_site: liveSite.id });
|
|
30982
31679
|
saveState(ctx, root, { live_site: liveSite.id });
|
|
30983
31680
|
live.site = liveSite.id;
|
|
30984
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 };
|
|
@@ -30991,7 +31688,8 @@ var init_init = __esm({
|
|
|
30991
31688
|
out.info("Running the first checks\u2026");
|
|
30992
31689
|
session = (await client.sdk.setupSessions.verify(sessionId)).setup_session;
|
|
30993
31690
|
}
|
|
30994
|
-
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;
|
|
30995
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)]);
|
|
30996
31694
|
return EXIT.ok;
|
|
30997
31695
|
}
|