@checkbox-site/cli 0.4.6 → 0.4.8
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 +1232 -210
- package/package.json +1 -1
package/dist/checkbox.js
CHANGED
|
@@ -3295,7 +3295,7 @@ var init_operations = __esm({
|
|
|
3295
3295
|
/**
|
|
3296
3296
|
* Store a billing test-mode key.
|
|
3297
3297
|
*
|
|
3298
|
-
* Stores the site's Stripe TEST-mode restricted key (rk_test_…), after checking it with Stripe the way the dashboard's Connect Stripe does. An agent may call this itself once the owner approved the setup; every store is logged on the site's setup timeline and Billing connection line, and the human can revoke it in one click. Live keys are refused (test_key_not_test_mode: a human adds live keys in the dashboard); the owner must have ticked
|
|
3298
|
+
* Stores the site's Stripe TEST-mode restricted key (rk_test_…), after checking it with Stripe the way the dashboard's Connect Stripe does. An agent may call this itself once the owner approved the setup; every store is logged on the site's setup timeline and Billing connection line, and the human can revoke it in one click. Live keys are refused (test_key_not_test_mode: a human adds live keys in the dashboard); the owner must have ticked the box “Let your agent store a Stripe test-mode restricted key for this project” (agent sign-in) or “Let this app store a Stripe test-mode restricted key for your sites” (app connection) (checkbox id consent-stripe-test-key) on the approval page of the agent's sign-in or connection (else test_key_declined); a key whose Stripe account looks like another site's is refused with billing_account_mismatch (with confirm_account_mismatch: true the key is connected on hold until a person confirms the account in the dashboard; only a person can confirm it); secret keys (sk_test_…) are refused (test_key_not_restricted). On a test-mode site without a Stripe connection the key also becomes the site's Stripe connection: one key for checks, fixes (only after the human approves each) and test runs. The key's checklist: Customers: Write, Products: Read, Prices: Read, Coupons: Read, Subscriptions: Write, Test clocks: Write, Customer portal: Write, Checkout Sessions: Write, Payment Links: Write, Webhook Endpoints: Write. Replaces an existing key. Next: createTestRun.
|
|
3299
3299
|
*
|
|
3300
3300
|
* @param site Unique identifier of the site (site_…).
|
|
3301
3301
|
* @see POST /v1/sites/{site}/test_credentials (operation `createTestCredential`, scope `test_runs:write`)
|
|
@@ -3617,7 +3617,7 @@ var SDK_VERSION;
|
|
|
3617
3617
|
var init_version = __esm({
|
|
3618
3618
|
"../sdk-node/src/version.ts"() {
|
|
3619
3619
|
"use strict";
|
|
3620
|
-
SDK_VERSION = "0.2.
|
|
3620
|
+
SDK_VERSION = "0.2.6";
|
|
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,71 @@ 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 testKeyBoxName(via = "either") {
|
|
4761
|
+
const label = via === "device" ? `\u201C${DEVICE_TEST_KEY_LABEL}\u201D` : via === "oauth" ? `\u201C${OAUTH_TEST_KEY_LABEL}\u201D` : `\u201C${DEVICE_TEST_KEY_LABEL}\u201D (agent sign-in) or \u201C${OAUTH_TEST_KEY_LABEL}\u201D (app connection)`;
|
|
4762
|
+
return `the box ${label} (checkbox id ${TEST_KEY_CHECKBOX_ID})`;
|
|
4763
|
+
}
|
|
4764
|
+
function approvalKeyAsk(opts = {}) {
|
|
4765
|
+
if (opts.stripe === false) return "";
|
|
4766
|
+
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)`;
|
|
4767
|
+
return `${opts.stripe ? "In the same message" : "If the site sells through Stripe, in the same message"}, ask them to tick ${testKeyBoxName("device")} 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.`;
|
|
4768
|
+
}
|
|
4769
|
+
var STRIPE_KEY_PERMISSIONS, STRIPE_TEST_API_KEYS_URL, DEVICE_TEST_KEY_LABEL, OAUTH_TEST_KEY_LABEL, TEST_KEY_CHECKBOX_ID;
|
|
4739
4770
|
var init_stripe_key = __esm({
|
|
4740
4771
|
"../core/src/stripe-key.ts"() {
|
|
4741
4772
|
"use strict";
|
|
4773
|
+
STRIPE_KEY_PERMISSIONS = [
|
|
4774
|
+
{ 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)." },
|
|
4775
|
+
{ resource: "Products", access: "Read", why: "Plan names next to your prices.", uses: ["checks", "test_runs"] },
|
|
4776
|
+
{ resource: "Prices", access: "Read", why: "Amounts, billing intervals and trial lengths, compared with what your pages say.", uses: ["checks", "test_runs"] },
|
|
4777
|
+
{ resource: "Coupons", access: "Read", why: "Only when your hosted cancellation page offers a coupon as the save offer.", uses: ["fixes"], optional: true },
|
|
4778
|
+
{ 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." },
|
|
4779
|
+
{ 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." },
|
|
4780
|
+
{ 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)." },
|
|
4781
|
+
{ 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)." },
|
|
4782
|
+
{ 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." },
|
|
4783
|
+
{ 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." }
|
|
4784
|
+
];
|
|
4785
|
+
STRIPE_TEST_API_KEYS_URL = "https://dashboard.stripe.com/test/apikeys";
|
|
4786
|
+
DEVICE_TEST_KEY_LABEL = "Let your agent store a Stripe test-mode restricted key for this project";
|
|
4787
|
+
OAUTH_TEST_KEY_LABEL = "Let this app store a Stripe test-mode restricted key for your sites";
|
|
4788
|
+
TEST_KEY_CHECKBOX_ID = "consent-stripe-test-key";
|
|
4742
4789
|
}
|
|
4743
4790
|
});
|
|
4744
4791
|
|
|
4745
4792
|
// ../core/src/go-live.ts
|
|
4793
|
+
function goLiveSteps(input = {}) {
|
|
4794
|
+
const out = [];
|
|
4795
|
+
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." });
|
|
4796
|
+
if (input.termsOfServiceRequired && !input.termsOfServiceUrlSet) out.push({ key: "stripe_terms_url", who: "person", text: input.termsOfServiceConditional ? GO_LIVE_TERMS_URL_STEP_CONDITIONAL : GO_LIVE_TERMS_URL_STEP, url: STRIPE_PUBLIC_DETAILS });
|
|
4797
|
+
if (!input.liveKeyConnected)
|
|
4798
|
+
out.push({
|
|
4799
|
+
key: "live_stripe_key",
|
|
4800
|
+
who: "person",
|
|
4801
|
+
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."
|
|
4802
|
+
});
|
|
4803
|
+
if (input.checkoutKind === "payment_link")
|
|
4804
|
+
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." });
|
|
4805
|
+
else if (input.checkoutKind === "stripe_checkout")
|
|
4806
|
+
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)." });
|
|
4807
|
+
out.push({
|
|
4808
|
+
key: "promote_site",
|
|
4809
|
+
who: "person",
|
|
4810
|
+
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."}`
|
|
4811
|
+
});
|
|
4812
|
+
return out;
|
|
4813
|
+
}
|
|
4814
|
+
var STRIPE_PUBLIC_DETAILS, GO_LIVE_TERMS_URL_STEP, GO_LIVE_TERMS_URL_STEP_CONDITIONAL, GO_LIVE_STEPS;
|
|
4746
4815
|
var init_go_live = __esm({
|
|
4747
4816
|
"../core/src/go-live.ts"() {
|
|
4748
4817
|
"use strict";
|
|
4818
|
+
STRIPE_PUBLIC_DETAILS = "https://dashboard.stripe.com/settings/public";
|
|
4819
|
+
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.';
|
|
4820
|
+
GO_LIVE_TERMS_URL_STEP_CONDITIONAL = 'Set your Terms of Service URL in the Stripe Dashboard (Settings > Public details) before your checkout asks shoppers to accept your terms (consent_collection.terms_of_service = "required", which the checkout fix adds): Stripe refuses those Checkout Sessions until that URL is set.';
|
|
4821
|
+
GO_LIVE_STEPS = goLiveSteps().map((s2) => s2.text);
|
|
4749
4822
|
}
|
|
4750
4823
|
});
|
|
4751
4824
|
|
|
@@ -4788,6 +4861,13 @@ var init_secrets = __esm({
|
|
|
4788
4861
|
}
|
|
4789
4862
|
});
|
|
4790
4863
|
|
|
4864
|
+
// ../core/src/site-unreachable.ts
|
|
4865
|
+
var init_site_unreachable = __esm({
|
|
4866
|
+
"../core/src/site-unreachable.ts"() {
|
|
4867
|
+
"use strict";
|
|
4868
|
+
}
|
|
4869
|
+
});
|
|
4870
|
+
|
|
4791
4871
|
// ../core/src/index.ts
|
|
4792
4872
|
var init_src2 = __esm({
|
|
4793
4873
|
"../core/src/index.ts"() {
|
|
@@ -4799,6 +4879,7 @@ var init_src2 = __esm({
|
|
|
4799
4879
|
init_stripe_key();
|
|
4800
4880
|
init_go_live();
|
|
4801
4881
|
init_secrets();
|
|
4882
|
+
init_site_unreachable();
|
|
4802
4883
|
}
|
|
4803
4884
|
});
|
|
4804
4885
|
|
|
@@ -4988,7 +5069,7 @@ var init_california = __esm({
|
|
|
4988
5069
|
notes: notes(
|
|
4989
5070
|
AB2863_APPLICABILITY,
|
|
4990
5071
|
"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
|
|
5072
|
+
'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
5073
|
'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
5074
|
)
|
|
4994
5075
|
}),
|
|
@@ -5837,7 +5918,7 @@ var init_new_york = __esm({
|
|
|
5837
5918
|
NYC_EFFECTIVE = "2026-10-01";
|
|
5838
5919
|
NYC = "6 RCNY \xA7 5-110.1";
|
|
5839
5920
|
NYC_RULE_VERIFIED = "2026-10-31";
|
|
5840
|
-
NYC_NOTE = "NYC DCWP rule (adopted July 10, 2026; effective Oct. 1, 2026), verified against the adopted text on 2026-10-31. Applies to New York City consumers; evaluated wherever US-NY applies (
|
|
5921
|
+
NYC_NOTE = "NYC DCWP rule (adopted July 10, 2026; effective Oct. 1, 2026), verified against the adopted text on 2026-10-31. Applies to New York City consumers; evaluated wherever US-NY applies (how far the rule reaches outside the city is not settled yet). Exempt (\xA7 5-110.3): municipal franchise holders, DFS-regulated entities, licensed alarm operators, banks and licensed financial institutions, and service-contract sellers.";
|
|
5841
5922
|
PART_W_NOTE = "Current text from L. 2025, ch. 58, Part W, effective Nov. 5, 2025.";
|
|
5842
5923
|
PRE_PART_W = "Prior version: L. 2020, ch. 267 text in force Feb. 9, 2021 - Nov. 4, 2025 (sunset when Part W took effect); kept so evaluations as of earlier dates apply the law then in force.";
|
|
5843
5924
|
NEW_YORK_REQUIREMENTS = [
|
|
@@ -6286,12 +6367,12 @@ var init_new_york = __esm({
|
|
|
6286
6367
|
sourceUrl: NYC_RULE_URL,
|
|
6287
6368
|
effectiveDate: NYC_EFFECTIVE,
|
|
6288
6369
|
lastVerified: NYC_RULE_VERIFIED,
|
|
6289
|
-
summary: "City rule mirroring GBL \xA7 527-a: material terms (including the deadline to act) before consent or billing information; cancellation as easy as consent, in the same medium and every consent medium, with an online option (website or email) for in-person sign-ups; no obstruction or unreasonable save offers; 15-45 day renewal notice (initial term of one year or more renewing for six months or more) and 3-21 day trial notice (trials over a month) in the consumer-selected channel; material-change notice at least 5 business days and no more than 30 days before the change (Checkbox
|
|
6370
|
+
summary: "City rule mirroring GBL \xA7 527-a: material terms (including the deadline to act) before consent or billing information; cancellation as easy as consent, in the same medium and every consent medium, with an online option (website or email) for in-person sign-ups; no obstruction or unreasonable save offers; 15-45 day renewal notice (initial term of one year or more renewing for six months or more) and 3-21 day trial notice (trials over a month) in the consumer-selected channel; material-change notice at least 5 business days and no more than 30 days before the change (Checkbox Notices counts the business days, skipping weekends and public holidays). Violators refund every amount charged after the first cancellation attempt (\xA7 5-110.2); DCWP penalties $525 / $1,050 / $3,500. Checked together with New York State's rules.",
|
|
6290
6371
|
status: "in_force",
|
|
6291
6372
|
confidence: "medium",
|
|
6292
6373
|
verification: "primary",
|
|
6293
6374
|
requirementIdPrefixes: ["US-NY.NYC."],
|
|
6294
|
-
scope: "New York City consumers. Exempt (\xA7 5-110.3): municipal franchise holders (e.g. cable), DFS-regulated entities, licensed alarm operators, banks and licensed financial institutions, and service-contract sellers.
|
|
6375
|
+
scope: "New York City consumers. Exempt (\xA7 5-110.3): municipal franchise holders (e.g. cable), DFS-regulated entities, licensed alarm operators, banks and licensed financial institutions, and service-contract sellers. How far the rule reaches outside the city, and how it works alongside the Attorney General's enforcement of the state law, are not settled yet (hence medium confidence).",
|
|
6295
6376
|
amendments: [
|
|
6296
6377
|
{ citation: "DCWP Notice of Adoption (proposed Apr. 8, 2026; hearing May 8, 2026; adopted July 10, 2026)", effectiveDate: NYC_EFFECTIVE, summary: 'Adds 6 RCNY Part 8 "Click to Cancel" (\xA7\xA7 5-110 to 5-110.3).' }
|
|
6297
6378
|
]
|
|
@@ -10157,11 +10238,11 @@ function findRecurringPrices(text, amounts, freqs) {
|
|
|
10157
10238
|
let best = null;
|
|
10158
10239
|
for (const f of freqs) {
|
|
10159
10240
|
const after = f.index >= money3.index;
|
|
10160
|
-
const
|
|
10161
|
-
if (
|
|
10162
|
-
if (
|
|
10241
|
+
const gap2 = after ? f.index - (money3.index + money3.text.length) : money3.index - (f.index + f.text.length);
|
|
10242
|
+
if (gap2 < -1) continue;
|
|
10243
|
+
if (gap2 > (after ? 45 : 60)) continue;
|
|
10163
10244
|
if (SENTENCE_BREAK_RE.test(between(text, money3, f))) continue;
|
|
10164
|
-
if (!best ||
|
|
10245
|
+
if (!best || gap2 < best.gap) best = { f, gap: gap2 };
|
|
10165
10246
|
}
|
|
10166
10247
|
if (best) {
|
|
10167
10248
|
const start = Math.min(money3.index, best.f.index);
|
|
@@ -11440,10 +11521,10 @@ function withIntroOffers(declared, billing, offers) {
|
|
|
11440
11521
|
}
|
|
11441
11522
|
function shortPlanTerms(p) {
|
|
11442
11523
|
const digits = currencyDigitsOf(p.currency);
|
|
11443
|
-
const
|
|
11524
|
+
const major2 = p.amount / 10 ** digits;
|
|
11444
11525
|
const cur = (p.currency || "usd").toLowerCase();
|
|
11445
11526
|
const sym = { usd: "$", eur: "\u20AC", gbp: "\xA3", jpy: "\xA5" }[cur];
|
|
11446
|
-
const num3 = Number.isInteger(
|
|
11527
|
+
const num3 = Number.isInteger(major2) ? String(major2) : major2.toFixed(digits);
|
|
11447
11528
|
const money3 = sym ? `${sym}${num3}` : `${num3} ${cur.toUpperCase()}`;
|
|
11448
11529
|
const every = (p.intervalCount || 1) > 1 ? ` every ${p.intervalCount} ${p.interval}s` : `/${p.interval}`;
|
|
11449
11530
|
const trial = (p.trialDays ?? 0) > 0 ? ` with a ${p.trialDays}-day free trial` : "";
|
|
@@ -11713,11 +11794,11 @@ function formatMoney(amount, currency) {
|
|
|
11713
11794
|
} catch {
|
|
11714
11795
|
digits = 2;
|
|
11715
11796
|
}
|
|
11716
|
-
const
|
|
11797
|
+
const major2 = amount / 10 ** digits;
|
|
11717
11798
|
try {
|
|
11718
|
-
return new Intl.NumberFormat("en-US", { style: "currency", currency: code2, minimumFractionDigits: digits }).format(
|
|
11799
|
+
return new Intl.NumberFormat("en-US", { style: "currency", currency: code2, minimumFractionDigits: digits }).format(major2);
|
|
11719
11800
|
} catch {
|
|
11720
|
-
return `${
|
|
11801
|
+
return `${major2.toFixed(digits)} ${code2}`;
|
|
11721
11802
|
}
|
|
11722
11803
|
}
|
|
11723
11804
|
function intervalPhrase(interval, count = 1) {
|
|
@@ -12106,6 +12187,13 @@ function checkoutSessionsOf(config) {
|
|
|
12106
12187
|
if (!Array.isArray(raw)) return [];
|
|
12107
12188
|
return raw.filter((s2) => !!s2 && typeof s2 === "object" && typeof s2.id === "string" && Array.isArray(s2.lineItems));
|
|
12108
12189
|
}
|
|
12190
|
+
function withTestRunSessions(sessions, runSessionIds) {
|
|
12191
|
+
if (!runSessionIds.size) return [...sessions];
|
|
12192
|
+
return sessions.map((s2) => !s2.checkboxTestRun && runSessionIds.has(s2.id) ? { ...s2, checkboxTestRun: true } : s2);
|
|
12193
|
+
}
|
|
12194
|
+
function testRunSessionIds(runs) {
|
|
12195
|
+
return new Set(runs.map((r) => r.state?.sessionId).filter((x) => typeof x === "string" && x.startsWith("cs_")));
|
|
12196
|
+
}
|
|
12109
12197
|
function newestFirst(sessions) {
|
|
12110
12198
|
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
12199
|
}
|
|
@@ -13333,7 +13421,7 @@ function checkoutConfigViews(ctx) {
|
|
|
13333
13421
|
return (ctx.checkoutConfigs ?? []).filter((c) => c && c.capture).map((c) => ({
|
|
13334
13422
|
...buildSignupView(c.capture),
|
|
13335
13423
|
label: `${c.kind === "payment_link" ? "Stripe Payment Link" : "Stripe Checkout config"} \u201C${c.name ?? c.id}\u201D`,
|
|
13336
|
-
checkoutConfig: { id: c.id, ...c.name ? { name: c.name } : {}, consentRequired: c.consentRequired, ...c.kind ? { kind: c.kind } : {}, ...c.url ? { url: c.url } : {} },
|
|
13424
|
+
checkoutConfig: { id: c.id, ...c.name ? { name: c.name } : {}, consentRequired: c.consentRequired, ...c.kind ? { kind: c.kind } : {}, ...c.url ? { url: c.url } : {}, ...typeof c.trialDays === "number" ? { trialDays: c.trialDays } : {} },
|
|
13337
13425
|
// A Payment Link is read from Stripe on every billing read: it shows the link as of that read.
|
|
13338
13426
|
...c.kind === "payment_link" && readAt ? { observedAt: readAt } : {}
|
|
13339
13427
|
}));
|
|
@@ -13801,7 +13889,7 @@ function platformKey(platform) {
|
|
|
13801
13889
|
}
|
|
13802
13890
|
function portalUnlinkedFix(portalUrl) {
|
|
13803
13891
|
const login = portalUrl && /^https:\/\/billing\.stripe\.com\/p\/login\//.test(portalUrl) ? portalUrl : "billing.stripe.com/p/login/\u2026";
|
|
13804
|
-
return `Add a \u201CManage subscription\u201D link
|
|
13892
|
+
return `Add a \u201CManage subscription\u201D link that opens your Stripe customer portal (${login}) to your account page, or to your home page or footer if your site has no account page, then run a scan so Checkbox sees it.`;
|
|
13805
13893
|
}
|
|
13806
13894
|
function paymentLinkCancelRemediation(portalUrl, loginPageOn) {
|
|
13807
13895
|
const link = portalUrl && /^https:\/\/billing\.stripe\.com\//.test(portalUrl) ? portalUrl : void 0;
|
|
@@ -14907,6 +14995,18 @@ var init_help_pages = __esm({
|
|
|
14907
14995
|
});
|
|
14908
14996
|
|
|
14909
14997
|
// ../rules/src/checks/cancel-paths.ts
|
|
14998
|
+
function mayOpenPortal(el, pageUrl2) {
|
|
14999
|
+
const href = el.href;
|
|
15000
|
+
if (!href) return true;
|
|
15001
|
+
try {
|
|
15002
|
+
const target = new URL(href, pageUrl2);
|
|
15003
|
+
const page = new URL(pageUrl2);
|
|
15004
|
+
if (target.host !== page.host) return true;
|
|
15005
|
+
return /(?:^|\/)(?:api|portal|billing|billing-portal|create-portal-session|customer-portal)(?:\/|$|[-_?])/i.test(target.pathname);
|
|
15006
|
+
} catch {
|
|
15007
|
+
return true;
|
|
15008
|
+
}
|
|
15009
|
+
}
|
|
14910
15010
|
function inAppPath(input) {
|
|
14911
15011
|
const flow = input.ctx.cancellation?.via === "stripe_portal" ? void 0 : input.ctx.cancellation;
|
|
14912
15012
|
if (flow) {
|
|
@@ -14942,7 +15042,7 @@ function inAppPath(input) {
|
|
|
14942
15042
|
return true;
|
|
14943
15043
|
}
|
|
14944
15044
|
});
|
|
14945
|
-
if (leadsOnline.length) return { kind: "in_app", state: "offered", detail: `your own page offers a cancel option (${p.url})`, url: p.url };
|
|
15045
|
+
if (leadsOnline.length) return { kind: "in_app", state: "offered", detail: `your own page offers a cancel option (${p.url})`, url: p.url, ...leadsOnline.every((el) => mayOpenPortal(el, p.url)) ? { mayOpenPortal: true } : {} };
|
|
14946
15046
|
}
|
|
14947
15047
|
if (input.ctx.facts?.cancelsInApp === true) return { kind: "in_app", state: "unconfirmed", detail: "subscribers cancel in your own app, but Checkbox has not seen a cancellation complete there yet" };
|
|
14948
15048
|
return void 0;
|
|
@@ -15620,6 +15720,16 @@ var init_cancellation = __esm({
|
|
|
15620
15720
|
{ unverified: true, source: "snippet" }
|
|
15621
15721
|
);
|
|
15622
15722
|
}
|
|
15723
|
+
if (controls.length && cfg?.selfServeOnline === false && controls.every((el) => mayOpenPortal(el, page.url))) {
|
|
15724
|
+
return makeResult(
|
|
15725
|
+
id,
|
|
15726
|
+
"needs_review",
|
|
15727
|
+
`A cancel page with an online cancel option (\u201C${quote(controls[0].text)}\u201D) was found at ${page.url}, but cancellation is off in your billing platform's customer portal: if this option opens the portal, subscribers cannot cancel there. Not yet verified by a cancellation test: run a test run, or turn cancellation on in the portal`,
|
|
15728
|
+
[...captureRefs(page), ...configRefs],
|
|
15729
|
+
0.5,
|
|
15730
|
+
{ inAppOffered: true, url: page.url, selfServeOnline: false, portalCancelOff: true }
|
|
15731
|
+
);
|
|
15732
|
+
}
|
|
15623
15733
|
if (controls.length) {
|
|
15624
15734
|
return makeResult(
|
|
15625
15735
|
id,
|
|
@@ -16043,6 +16153,16 @@ var init_config2 = __esm({
|
|
|
16043
16153
|
}
|
|
16044
16154
|
if (c.selfServeOnline === false) {
|
|
16045
16155
|
const flow = input.ctx.cancellation;
|
|
16156
|
+
if (inApp?.state === "offered" && inApp.mayOpenPortal && model.hosted?.state !== "offered" && !(model.offline && !model.offline.soft)) {
|
|
16157
|
+
return makeResult(
|
|
16158
|
+
id,
|
|
16159
|
+
"needs_review",
|
|
16160
|
+
`Self-serve cancellation is off in ${name}'s portal. Your own page offers a cancel option (${inApp.url}), but Checkbox has not followed it to a finished cancellation yet: if it opens the ${name} customer portal, subscribers cannot cancel there. Run a test run (it cancels through your page), or turn cancellation on in the portal`,
|
|
16161
|
+
refs(input),
|
|
16162
|
+
0.6,
|
|
16163
|
+
{ ...data, ownCancelPage: true, inAppOfferedUntested: true, inAppUnobserved: true }
|
|
16164
|
+
);
|
|
16165
|
+
}
|
|
16046
16166
|
if (model.otherOnlineWorks) {
|
|
16047
16167
|
const via = inApp?.state === "works" && flow ? "subscribers cancel online through your own page (a test cancellation completed; see \u201Ccancel online\u201D)" : model.hosted?.state === "offered" ? `subscribers cancel through the Checkbox hosted cancellation page (${model.hosted.url}); see \u201Ccancel online\u201D` : `your own page offers a cancel option (${inApp?.url}); online cancellation is judged on that page (see \u201Ccancel online\u201D), so the portal setting is not required`;
|
|
16048
16168
|
return makeResult(
|
|
@@ -16248,8 +16368,8 @@ function introEmailFinding(rawText, plans, opts) {
|
|
|
16248
16368
|
const usable = plans.filter((p2) => p2.amount > 0 && p2.interval);
|
|
16249
16369
|
if (!usable.length) return void 0;
|
|
16250
16370
|
const text = normalizeText(rawText);
|
|
16251
|
-
const
|
|
16252
|
-
const regular = [...usable.map((p2) =>
|
|
16371
|
+
const major2 = (p2, minor) => minor / 10 ** currencyDigitsOf(p2.currency);
|
|
16372
|
+
const regular = [...usable.map((p2) => major2(p2, p2.amount)), ...opts.extraAmounts ?? []];
|
|
16253
16373
|
const isRegular = (n) => regular.some((r) => Math.abs(r - n) < 5e-3);
|
|
16254
16374
|
const offRegular = (sentence) => findAmounts(sentence).filter((m) => {
|
|
16255
16375
|
if (typeof m.amount !== "number" || !(m.amount > 0) || isRegular(m.amount)) return false;
|
|
@@ -16296,7 +16416,7 @@ function introEmailFinding(rawText, plans, opts) {
|
|
|
16296
16416
|
};
|
|
16297
16417
|
}
|
|
16298
16418
|
const fits = (p2) => {
|
|
16299
|
-
const intro =
|
|
16419
|
+
const intro = major2(p2, p2.introAmount);
|
|
16300
16420
|
const pct = (1 - p2.introAmount / p2.amount) * 100;
|
|
16301
16421
|
return statedAmounts.every((m) => Math.abs(m.amount - intro) < 5e-3) && percents.every((x) => Math.abs(x.value - pct) <= 0.5);
|
|
16302
16422
|
};
|
|
@@ -16325,6 +16445,7 @@ function introEmailFinding(rawText, plans, opts) {
|
|
|
16325
16445
|
fix: `State how long the promo really lasts: ${introTermsPhrase(p2)}.`
|
|
16326
16446
|
};
|
|
16327
16447
|
}
|
|
16448
|
+
if (statesIntroEndDate(text)) return void 0;
|
|
16328
16449
|
const p = fitting[0];
|
|
16329
16450
|
return {
|
|
16330
16451
|
status: "needs_review",
|
|
@@ -16334,29 +16455,32 @@ function introEmailFinding(rawText, plans, opts) {
|
|
|
16334
16455
|
fix: `State how long the promo lasts and the price after it: ${introTermsPhrase(p)}.`
|
|
16335
16456
|
};
|
|
16336
16457
|
}
|
|
16458
|
+
function statesIntroEndDate(text) {
|
|
16459
|
+
return INTRO_END_DATE_RES.some((re) => re.test(text));
|
|
16460
|
+
}
|
|
16337
16461
|
function introFirstChargeFinding(rawText, plans) {
|
|
16338
16462
|
const usable = plans.filter((p2) => p2.amount > 0 && p2.interval);
|
|
16339
16463
|
if (!usable.length) return void 0;
|
|
16340
16464
|
const text = normalizeText(rawText);
|
|
16341
16465
|
const stated = findAmounts(text).filter((m) => typeof m.amount === "number" && m.amount > 0);
|
|
16342
|
-
const
|
|
16466
|
+
const major2 = (p2, minor) => minor / 10 ** currencyDigitsOf(p2.currency);
|
|
16343
16467
|
const states = (n) => stated.some((m) => Math.abs(m.amount - n) < 5e-3);
|
|
16344
|
-
const named = usable.filter((p2) => states(
|
|
16468
|
+
const named = usable.filter((p2) => states(major2(p2, p2.amount)));
|
|
16345
16469
|
if (!named.length || !named.every(hasIntro)) return void 0;
|
|
16346
|
-
if (named.some((p2) => states(
|
|
16470
|
+
if (named.some((p2) => states(major2(p2, p2.introAmount)))) return void 0;
|
|
16347
16471
|
if (new RegExp(PERCENT_OFF_RE.source, "i").test(text)) return void 0;
|
|
16348
16472
|
const p = named[0];
|
|
16349
16473
|
const regular = money(p.amount, p.currency);
|
|
16350
16474
|
const nameOf = p.label?.trim() || (p.name?.trim() ? `plan \u201C${p.name.trim()}\u201D` : `the ${regular} ${planEvery(p)} plan`);
|
|
16351
16475
|
return {
|
|
16352
16476
|
status: "fail",
|
|
16353
|
-
quote: stated.find((m) => Math.abs(m.amount -
|
|
16477
|
+
quote: stated.find((m) => Math.abs(m.amount - major2(p, p.amount)) < 5e-3)?.text ?? regular,
|
|
16354
16478
|
reason: "intro_first_charge",
|
|
16355
16479
|
detail: `states ${regular} as the charge after the trial, but ${nameOf} has an introductory price: the first charge after the trial is ${money(p.introAmount, p.currency)} (${introTermsPhrase(p)}) \u2014 state the intro price for that charge, then the regular price`,
|
|
16356
16480
|
fix: `State the first charge after the trial and the price after it: ${introTermsPhrase(p)}.`
|
|
16357
16481
|
};
|
|
16358
16482
|
}
|
|
16359
|
-
var PERCENT_OFF_RE, DURATION_NUM, DURATION_UNIT, FIRST_DURATION_RE, FOR_DURATION_RE, VAGUE_DURATION_RE, TRIAL_DURATION_AROUND_RE, BILLED_LONGER_RE, INTRO_CUE_RE, NOT_A_PRICE_AROUND_RE, TRIAL_ENDING_RE;
|
|
16483
|
+
var PERCENT_OFF_RE, DURATION_NUM, DURATION_UNIT, FIRST_DURATION_RE, FOR_DURATION_RE, VAGUE_DURATION_RE, TRIAL_DURATION_AROUND_RE, BILLED_LONGER_RE, INTRO_CUE_RE, NOT_A_PRICE_AROUND_RE, DATE_RE_SRC, INTRO_WORD, REGULAR_PRICE, gap, INTRO_END_DATE_RES, TRIAL_ENDING_RE;
|
|
16360
16484
|
var init_intro_terms = __esm({
|
|
16361
16485
|
"../rules/src/checks/intro-terms.ts"() {
|
|
16362
16486
|
"use strict";
|
|
@@ -16372,10 +16496,86 @@ var init_intro_terms = __esm({
|
|
|
16372
16496
|
BILLED_LONGER_RE = /\bbilled\s+(?:annually|yearly|every\s+(?:\d{1,2}|twelve|two|three|six)\s+(?:months|years))\b|\bsingle\s+upfront\s+charge\b/i;
|
|
16373
16497
|
INTRO_CUE_RE = /\bintro(?:ductory)?\b|\bpromo(?:tional)?\b|\bdiscount(?:ed)?\b|\bspecial\s+(?:price|offer|rate)\b|\bfirst\s+\w+|\bthen\b|\bafter\s+(?:that|which)\b/i;
|
|
16374
16498
|
NOT_A_PRICE_AROUND_RE = /\b(?:tax(?:es)?|vat|fees?|shipping|delivery\s+fee|credit|refund(?:ed)?|balance|deposit|tip)\b/i;
|
|
16499
|
+
DATE_RE_SRC = "(?:(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|june?|july?|aug(?:ust)?|sep(?:t(?:ember)?)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\\.?\\s+\\d{1,2}(?:st|nd|rd|th)?(?:,?\\s+\\d{4})?|\\d{1,2}\\s+(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\\.?(?:\\s+\\d{4})?|\\d{4}-\\d{2}-\\d{2}|\\d{1,2}/\\d{1,2}/\\d{2,4})";
|
|
16500
|
+
INTRO_WORD = "(?:discount|coupon|promo(?:tion|tional)?(?:\\s+(?:price|rate|code))?|intro(?:ductory)?(?:\\s+(?:price|rate|offer|period))?|special\\s+(?:price|offer|rate)|offer)";
|
|
16501
|
+
REGULAR_PRICE = "(?:regular|full|standard|normal|usual)\\s+(?:price|rate)";
|
|
16502
|
+
gap = (n) => `(?:[^.\\n]|\\.(?=\\d)){0,${n}}?`;
|
|
16503
|
+
INTRO_END_DATE_RES = [
|
|
16504
|
+
new RegExp(`\\b${INTRO_WORD}\\b${gap(80)}\\b(?:ends?|ending|expires?|runs?\\s+out|lasts?\\s+(?:until|through)|is\\s+valid\\s+(?:until|through)|until|through)\\b${gap(100)}${DATE_RE_SRC}`, "i"),
|
|
16505
|
+
new RegExp(`\\b${REGULAR_PRICE}\\b${gap(100)}\\b(?:from|on|starting(?:\\s+on)?|beginning(?:\\s+on)?|as\\s+of|after)\\s+${DATE_RE_SRC}`, "i"),
|
|
16506
|
+
new RegExp(`\\b(?:starting|from|beginning|on|as\\s+of)\\s+${DATE_RE_SRC}${gap(100)}\\b${REGULAR_PRICE}\\b`, "i")
|
|
16507
|
+
];
|
|
16375
16508
|
TRIAL_ENDING_RE = /\btrial\b[^.\n]{0,60}?\b(?:ends?|ending|expires?|is\s+over|runs?\s+out)\b|\b(?:after|when|once)\s+(?:your|the)\s+(?:free\s+)?trial\b/i;
|
|
16376
16509
|
}
|
|
16377
16510
|
});
|
|
16378
16511
|
|
|
16512
|
+
// ../rules/src/checks/charge-claims.ts
|
|
16513
|
+
function sentenceSpan(text, index) {
|
|
16514
|
+
const dot = text.lastIndexOf(". ", index);
|
|
16515
|
+
const start = Math.max(dot >= 0 ? dot + 2 : 0, text.lastIndexOf("\n", index) + 1);
|
|
16516
|
+
const ends = [text.indexOf(". ", index), text.indexOf("\n", index)].filter((n) => n >= 0);
|
|
16517
|
+
return { start: Math.min(start, index), end: ends.length ? Math.min(...ends) + 1 : text.length };
|
|
16518
|
+
}
|
|
16519
|
+
function misstatedRecurringCharge(text, prices, plans, others = []) {
|
|
16520
|
+
const usable = plans.filter((p) => p.amount > 0 && p.interval);
|
|
16521
|
+
if (!usable.length) return void 0;
|
|
16522
|
+
const known = [...usable.map((p) => p.amount), ...others].filter((n) => n > 0);
|
|
16523
|
+
const same = (a, b) => Math.abs(a - b) < 5e-3;
|
|
16524
|
+
const statesPlanPrice = prices.some((r) => r.money.amount !== null && known.some((k) => same(k, r.money.amount)));
|
|
16525
|
+
if (!statesPlanPrice) return void 0;
|
|
16526
|
+
return prices.find((r) => {
|
|
16527
|
+
const amount = r.money.amount;
|
|
16528
|
+
const unit2 = r.frequency.unit;
|
|
16529
|
+
if (amount === null || !(amount > 0) || !unit2) return false;
|
|
16530
|
+
if (known.some((k) => same(k, amount))) return false;
|
|
16531
|
+
const cur = (r.money.currency || "").toLowerCase();
|
|
16532
|
+
const plansHere = usable.filter((p) => !cur || !p.currency || p.currency.toLowerCase() === cur);
|
|
16533
|
+
if (!plansHere.some((p) => frequencyMatchesPlan(unit2, r.frequency.count, p))) return false;
|
|
16534
|
+
if (plansHere.some((p) => {
|
|
16535
|
+
const k = amount / p.amount;
|
|
16536
|
+
return k >= 2 && Math.abs(k - Math.round(k)) < 1e-3;
|
|
16537
|
+
})) return false;
|
|
16538
|
+
if (isPerPeriodEquivalent(r, plansHere)) return false;
|
|
16539
|
+
const at = r.money.index;
|
|
16540
|
+
if (!(at >= 0) || text.slice(at, at + r.money.text.length) !== r.money.text) return false;
|
|
16541
|
+
{
|
|
16542
|
+
const { start, end } = sentenceSpan(text, at);
|
|
16543
|
+
const sentence = text.slice(start, end);
|
|
16544
|
+
const around = text.slice(Math.max(start, at - 24), Math.min(end, at + r.money.text.length + 40));
|
|
16545
|
+
if (NOT_THE_CHARGE_AROUND_RE.test(around) || NOT_A_PRICE_AROUND_RE.test(around)) return false;
|
|
16546
|
+
if (INTRO_CUE_RE.test(sentence) || INTRO_SENTENCE_RE.test(sentence)) return false;
|
|
16547
|
+
const before = text.slice(Math.max(start, at - 32), at);
|
|
16548
|
+
const adjacent = prices.some((o) => {
|
|
16549
|
+
if (o === r || o.money.amount === null || o.money.index < 0 || !known.some((k) => same(k, o.money.amount))) return false;
|
|
16550
|
+
const [a, b] = o.money.index < at ? [o.money, r.money] : [r.money, o.money];
|
|
16551
|
+
return /^[\s()[\]/,:–—-]*$/.test(text.slice(a.index + a.text.length, b.index));
|
|
16552
|
+
});
|
|
16553
|
+
if (!adjacent && !CHARGE_CUE_BEFORE_RE.test(before)) return false;
|
|
16554
|
+
}
|
|
16555
|
+
return true;
|
|
16556
|
+
});
|
|
16557
|
+
}
|
|
16558
|
+
function isPerPeriodEquivalent(r, plans) {
|
|
16559
|
+
const amount = r.money.amount;
|
|
16560
|
+
const unit2 = r.frequency.unit;
|
|
16561
|
+
if (amount === null || !(amount > 0) || !unit2) return false;
|
|
16562
|
+
const cur = (r.money.currency || "").toLowerCase();
|
|
16563
|
+
return plans.some(
|
|
16564
|
+
(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)
|
|
16565
|
+
);
|
|
16566
|
+
}
|
|
16567
|
+
var NOT_THE_CHARGE_AROUND_RE, INTRO_SENTENCE_RE, CHARGE_CUE_BEFORE_RE;
|
|
16568
|
+
var init_charge_claims = __esm({
|
|
16569
|
+
"../rules/src/checks/charge-claims.ts"() {
|
|
16570
|
+
"use strict";
|
|
16571
|
+
init_plan_terms();
|
|
16572
|
+
init_intro_terms();
|
|
16573
|
+
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;
|
|
16574
|
+
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;
|
|
16575
|
+
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;
|
|
16576
|
+
}
|
|
16577
|
+
});
|
|
16578
|
+
|
|
16379
16579
|
// ../rules/src/checks/notice-content.ts
|
|
16380
16580
|
function offlineOnlyCancelIn(text, cancelMethods) {
|
|
16381
16581
|
const offline = contactOnlySentence(text);
|
|
@@ -16414,11 +16614,21 @@ function noticeContent(e, opts = {}) {
|
|
|
16414
16614
|
];
|
|
16415
16615
|
const amount = a.recurringPrices[0]?.money ?? positiveAmounts(a)[0];
|
|
16416
16616
|
const stated = positiveAmounts(a).filter((m) => m.amount !== null);
|
|
16617
|
+
const toMajor = (p, minor) => minor / 10 ** currencyDigitsOf(p.currency);
|
|
16618
|
+
const chargePlans = plans.map((p) => ({ amount: toMajor(p, p.amount), currency: p.currency, interval: p.interval, intervalCount: p.intervalCount }));
|
|
16619
|
+
const misstated = plans.length ? misstatedRecurringCharge(
|
|
16620
|
+
a.text,
|
|
16621
|
+
a.recurringPrices,
|
|
16622
|
+
chargePlans,
|
|
16623
|
+
[...expected, ...plans.flatMap((p) => typeof p.introAmount === "number" && p.introAmount > 0 ? [toMajor(p, p.introAmount)] : [])]
|
|
16624
|
+
) : void 0;
|
|
16417
16625
|
if (amount && expected.length && stated.length && !stated.some((m) => expected.some((x) => Math.abs(x - m.amount) < 5e-3))) wrong.amount = quote(amount);
|
|
16626
|
+
else if (misstated) wrong.amount = quote(misstated.money);
|
|
16418
16627
|
else if (amount) quotes.amount = quote(amount);
|
|
16419
16628
|
const freq = a.recurringPrices[0]?.frequency ?? a.frequencies[0];
|
|
16420
16629
|
const freqs = [...a.recurringPrices.map((r) => r.frequency), ...a.frequencies].filter((f) => f.unit);
|
|
16421
|
-
const
|
|
16630
|
+
const planPriceStated = stated.some((m) => chargePlans.some((p) => Math.abs(p.amount - m.amount) < 5e-3));
|
|
16631
|
+
const charged = a.recurringPrices.filter((r) => !!r.frequency?.unit && !(planPriceStated && isPerPeriodEquivalent(r, chargePlans))).map((r) => r.frequency);
|
|
16422
16632
|
const misses = (f) => !plans.some((p) => frequencyMatchesPlan(f.unit, f.count, p));
|
|
16423
16633
|
const wrongCharged = plans.length ? charged.find(misses) : void 0;
|
|
16424
16634
|
if (wrongCharged) wrong.frequency = quote(wrongCharged);
|
|
@@ -16485,6 +16695,7 @@ var init_notice_content = __esm({
|
|
|
16485
16695
|
init_context();
|
|
16486
16696
|
init_intro_terms();
|
|
16487
16697
|
init_result();
|
|
16698
|
+
init_charge_claims();
|
|
16488
16699
|
ELEMENT_LABELS = {
|
|
16489
16700
|
amount: "the amount charged",
|
|
16490
16701
|
frequency: "how often the subscription is charged",
|
|
@@ -16770,7 +16981,7 @@ function withoutRevisedDrafts(emails) {
|
|
|
16770
16981
|
const draft2 = (e) => !e.subscriptionRef && !e.relatedSignupAt && !e.testRun && !isCheckboxSentEmail(e);
|
|
16771
16982
|
const trial = (e) => /\b(?:free\s+)?trial\b/i.test(`${e.subject ?? ""} ${emailBody(e)}`);
|
|
16772
16983
|
const at = (e) => toDate(e.receivedAt)?.getTime() ?? 0;
|
|
16773
|
-
return emails.filter((e) => !(draft2(e) && sender(e) && emails.some((o) => o !== e &&
|
|
16984
|
+
return emails.filter((e) => !(draft2(e) && sender(e) && emails.some((o) => o !== e && sender(o) === sender(e) && trial(o) === trial(e) && at(o) > at(e))));
|
|
16774
16985
|
}
|
|
16775
16986
|
function latestPerTemplateAndPlan(emails, input) {
|
|
16776
16987
|
const offers = contextOffers(input.ctx);
|
|
@@ -17683,29 +17894,29 @@ function contentGap(required, emails, opts = {}) {
|
|
|
17683
17894
|
}
|
|
17684
17895
|
return null;
|
|
17685
17896
|
}
|
|
17686
|
-
function contentFail(spec, required,
|
|
17687
|
-
const intro =
|
|
17688
|
-
const sentWhat = isPreflightEmail(
|
|
17897
|
+
function contentFail(spec, required, gap2, timing, refs2, data) {
|
|
17898
|
+
const intro = gap2.intro;
|
|
17899
|
+
const sentWhat = isPreflightEmail(gap2.email) ? "is a draft that" : `${timing}, but it`;
|
|
17689
17900
|
const introData = intro ? { contentIntro: { status: intro.status, reason: intro.reason, quote: intro.quote, fix: intro.fix } } : {};
|
|
17690
|
-
if (!
|
|
17901
|
+
if (!gap2.missing.length && intro) {
|
|
17691
17902
|
return makeResult(
|
|
17692
17903
|
spec.checkId,
|
|
17693
17904
|
intro.status,
|
|
17694
|
-
`The ${spec.noun} (${emailLabel(
|
|
17695
|
-
[...emailRefs(
|
|
17905
|
+
`The ${spec.noun} (${emailLabel(gap2.email)}) ${sentWhat} ${intro.detail}`,
|
|
17906
|
+
[...emailRefs(gap2.email), ...refs2],
|
|
17696
17907
|
intro.status === "fail" ? 0.8 : 0.5,
|
|
17697
|
-
{ ...data, contentMissing: [], contentEmailId:
|
|
17908
|
+
{ ...data, contentMissing: [], contentEmailId: gap2.email.id, ...isCheckboxSentEmail(gap2.email) ? { contentByCheckbox: true } : {}, ...introData }
|
|
17698
17909
|
);
|
|
17699
17910
|
}
|
|
17700
17911
|
return makeResult(
|
|
17701
17912
|
spec.checkId,
|
|
17702
17913
|
"fail",
|
|
17703
|
-
`The ${spec.noun} (${emailLabel(
|
|
17914
|
+
`The ${spec.noun} (${emailLabel(gap2.email)}) ${isPreflightEmail(gap2.email) ? "is a draft that" : `${timing}, but it`} does not state ${describeMissing(gap2.missing)}${// Round 20 (pilot 19 item 16, change-guard: "contradicts the plan or is not an online way to cancel"): each wrong
|
|
17704
17915
|
// element says its own reason.
|
|
17705
|
-
|
|
17706
|
-
[...emailRefs(
|
|
17916
|
+
gap2.wrong ? ` correctly (it says ${Object.entries(gap2.wrong).map(([k, v]) => `\u201C${v}\u201D for ${ELEMENT_LABELS[k]}, ${k === "cancellation" ? "which is not an online way to cancel" : "which contradicts the plan"}`).join("; ")})` : ""} \u2014 the notice must tell the subscriber ${describeMissing(required)}${intro ? `; it also ${intro.detail}` : ""}`,
|
|
17917
|
+
[...emailRefs(gap2.email), ...refs2],
|
|
17707
17918
|
0.8,
|
|
17708
|
-
{ ...data, contentMissing:
|
|
17919
|
+
{ ...data, contentMissing: gap2.missing, contentEmailId: gap2.email.id, ...isCheckboxSentEmail(gap2.email) ? { contentByCheckbox: true } : {}, ...gap2.wrong ? { contentWrong: gap2.wrong } : {}, ...introData }
|
|
17709
17920
|
);
|
|
17710
17921
|
}
|
|
17711
17922
|
async function runReminder(spec, input) {
|
|
@@ -17805,8 +18016,8 @@ async function runReminder(spec, input) {
|
|
|
17805
18016
|
const w = conflicting ? "within each state\u2019s window" : why(window);
|
|
17806
18017
|
if (counted.length) {
|
|
17807
18018
|
if (!outside.length && !missing.length && !partial.length) {
|
|
17808
|
-
const
|
|
17809
|
-
if (
|
|
18019
|
+
const gap2 = contentGap(required, matched.flatMap((j) => j.email ? [j.email] : []), contentOpts);
|
|
18020
|
+
if (gap2) return contentFail(spec, required, gap2, "was sent on time", refs2(matched), data);
|
|
17810
18021
|
return makeResult(
|
|
17811
18022
|
id,
|
|
17812
18023
|
"pass",
|
|
@@ -17897,8 +18108,8 @@ async function runReminder(spec, input) {
|
|
|
17897
18108
|
return contentFail(spec, required, newestGap, t ? `was sent about ${t.days} days ahead (required: ${conflicting ? w : why(t.w)})` : "was captured (its timing could not be verified)", configRefs, data);
|
|
17898
18109
|
}
|
|
17899
18110
|
if (inWindow.length) {
|
|
17900
|
-
const
|
|
17901
|
-
if (
|
|
18111
|
+
const gap2 = contentGap(required, inWindow.map((t) => t.e), contentOpts);
|
|
18112
|
+
if (gap2) return contentFail(spec, required, gap2, `was sent about ${inWindow[0].days} days ahead (required: ${conflicting ? w : why(inWindow[0].w)})`, configRefs, data);
|
|
17902
18113
|
return makeResult(
|
|
17903
18114
|
id,
|
|
17904
18115
|
"pass",
|
|
@@ -17910,8 +18121,8 @@ async function runReminder(spec, input) {
|
|
|
17910
18121
|
}
|
|
17911
18122
|
if (nearWindow.length) {
|
|
17912
18123
|
const n = nearWindow[0];
|
|
17913
|
-
const
|
|
17914
|
-
if (
|
|
18124
|
+
const gap2 = contentGap(required, nearWindow.map((t) => t.e), contentOpts);
|
|
18125
|
+
if (gap2) return contentFail(spec, required, gap2, `was sent about ${n.days} days ahead (required: ${conflicting ? w : why(n.w)})`, configRefs, data);
|
|
17915
18126
|
data.timingNearWindow = { days: n.days, required: conflicting ? w : why(n.w), roundingRule: DAYS_ROUNDING_RULE, ...n.range ? { range: n.range } : {} };
|
|
17916
18127
|
data.timingOutsideWindow = { days: n.days, required: conflicting ? w : why(n.w) };
|
|
17917
18128
|
return makeResult(
|
|
@@ -19207,8 +19418,8 @@ function gatedByOwnBox(renewalBoxes, path) {
|
|
|
19207
19418
|
return renewalBoxes.some((b) => b.required === true && !b.checkedByDefault && consentGate(b, path) === "gated");
|
|
19208
19419
|
}
|
|
19209
19420
|
function boxNearCta(box, cta) {
|
|
19210
|
-
const
|
|
19211
|
-
return
|
|
19421
|
+
const gap2 = box.y >= cta.y + cta.height ? box.y - (cta.y + cta.height) : cta.y >= box.y + box.height ? cta.y - (box.y + box.height) : 0;
|
|
19422
|
+
return gap2 <= 200;
|
|
19212
19423
|
}
|
|
19213
19424
|
var NO_CHECKOUT_CAPTURE_MESSAGE, NO_PURCHASE_BUTTON, OTHER_PLAN_CARD_NOTE, heuristicCache, PROCESSOR_LINE_NOTE, RENEWAL_CONTEXT_RE, autorenewLanguage, autorenewOverViews, FOOTNOTE_RE, NON_CHARGE_AMOUNT_RE, INTRO_PRICE_RE, RENEWAL_PRICE_RE, PRODUCT_TITLE_RE, CHARGED_NOW_RE, LATER_CHARGE_RE, price, frequency, CANCEL_LINK_RE, cancellationPolicy, TRIAL_DAYS_SLACK, trialTerms2, termLength, PLACEMENT_RANK, proximity, conspicuous, RENEWAL_CONSENT_RE, CONSENT_CHECKBOX_RE, TOS_RE, SUBSCRIPTION_CTA_RE, MARKETING_RE, RENEWAL_TERMS_RE, NOTIFICATION_OPT_IN_RE, AGREEMENT_RE, SEPARATE_CONSENT_JURISDICTIONS, EXPRESS_CONSENT_COUNSEL_QUESTION, CA_SEPARATE_CONSENT, consentAffirmative, consentOverViews, notPrechecked, SIGNUP_CHECKS;
|
|
19214
19425
|
var init_signup = __esm({
|
|
@@ -19223,6 +19434,7 @@ var init_signup = __esm({
|
|
|
19223
19434
|
init_offer_view();
|
|
19224
19435
|
init_plan_terms();
|
|
19225
19436
|
init_cancellation();
|
|
19437
|
+
init_charge_claims();
|
|
19226
19438
|
init_intro_terms();
|
|
19227
19439
|
init_intro_terms();
|
|
19228
19440
|
init_result();
|
|
@@ -19388,6 +19600,29 @@ var init_signup = __esm({
|
|
|
19388
19600
|
const pl = view.plan?.plan;
|
|
19389
19601
|
return !!pl && hasIntro(pl) && r.money.amount !== null && Math.abs(r.money.amount - pl.introAmount / 10 ** currencyDigits(pl.currency || "usd")) < 5e-3;
|
|
19390
19602
|
};
|
|
19603
|
+
if (!tied && plans.length) {
|
|
19604
|
+
const sitePrices = withAccountPrices(referencePlans(input.ctx), input.ctx).map((p) => p.amount / 10 ** currencyDigits((p.currency || "usd").toLowerCase()));
|
|
19605
|
+
const intros = [...plans.map((p) => p.plan), ...view.plan ? [view.plan.plan] : []].flatMap(
|
|
19606
|
+
(pl) => hasIntro(pl) ? [pl.introAmount / 10 ** currencyDigits((pl.currency || "usd").toLowerCase())] : []
|
|
19607
|
+
);
|
|
19608
|
+
const misstated = misstatedRecurringCharge(
|
|
19609
|
+
near2.text,
|
|
19610
|
+
parsed,
|
|
19611
|
+
plans.map((p) => ({ amount: p.amount, currency: p.currency, interval: p.plan.interval, intervalCount: p.plan.intervalCount })),
|
|
19612
|
+
[...sitePrices, ...intros]
|
|
19613
|
+
);
|
|
19614
|
+
if (misstated && !NON_CHARGE_AMOUNT_RE.test(quote(misstated) ?? "") && !INTRO_PRICE_RE.test(quote(misstated) ?? "")) {
|
|
19615
|
+
const said = misstated.money.text;
|
|
19616
|
+
const own = quote(parsed.find(recurringMatches) ?? rp) ?? "";
|
|
19617
|
+
const firm = decisive && input.ctx.facts?.partialPlans !== true && !otherPlanCard(view, input);
|
|
19618
|
+
return unit(
|
|
19619
|
+
firm ? "fail" : "needs_review",
|
|
19620
|
+
`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`,
|
|
19621
|
+
firm ? 0.85 : 0.6,
|
|
19622
|
+
{ quote: said, meta: { ...meta(misstated.money.amount, misstated.money.currency), priceMismatch: true, secondCharge: true } }
|
|
19623
|
+
);
|
|
19624
|
+
}
|
|
19625
|
+
}
|
|
19391
19626
|
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
19627
|
if (foreign) {
|
|
19393
19628
|
const own = parsed.find(recurringMatches) ?? rp;
|
|
@@ -19689,8 +19924,18 @@ var init_signup = __esm({
|
|
|
19689
19924
|
const offers = contextOffers(input.ctx);
|
|
19690
19925
|
const loose = !view.plan || view.plan.how === "only_plan";
|
|
19691
19926
|
const cardOffer = loose && (view.offer || view.checkoutConfig) && offers.length > 1 ? offerForView(view, offers) : void 0;
|
|
19692
|
-
const
|
|
19927
|
+
const paramsTrial = view.checkoutConfig?.trialDays;
|
|
19928
|
+
const planTrial = typeof paramsTrial === "number" ? paramsTrial : cardOffer ? cardOffer.trialDays : view.plan ? offerTrialDays(offers, view.plan.plan, input.ctx.facts) : void 0;
|
|
19693
19929
|
const planName = cardOffer ? offerName(cardOffer) : view.plan?.label;
|
|
19930
|
+
if (mentioned && paramsTrial === 0) {
|
|
19931
|
+
const said = [...a.trialLengths, ...p.trialLengths][0] ?? [...a.trialMentions, ...p.trialMentions][0];
|
|
19932
|
+
return unit(
|
|
19933
|
+
"fail",
|
|
19934
|
+
`the text promises a free trial${said ? ` (\u201C${quote(said)}\u201D)` : ""}, but the ${view.checkoutConfig?.kind === "payment_link" ? "Payment Link" : "Checkout params"} run none (no subscription_data.trial_period_days), so Stripe charges the full price today \u2014 add the trial to subscription_data, or remove the promise from the text`,
|
|
19935
|
+
0.85,
|
|
19936
|
+
{ ...said ? { quote: quote(said) } : {}, meta: { planTrialDays: 0, paramsTrialDays: 0, trialMismatch: true } }
|
|
19937
|
+
);
|
|
19938
|
+
}
|
|
19694
19939
|
if (!mentioned) {
|
|
19695
19940
|
if (planTrial === 0 && offers.some((o) => o.trialDays > 0)) {
|
|
19696
19941
|
const withTrial = offers.filter((o) => o.trialDays > 0).map(offerName);
|
|
@@ -19715,7 +19960,7 @@ var init_signup = __esm({
|
|
|
19715
19960
|
const meta = {};
|
|
19716
19961
|
if (length && "days" in length && length.days !== null) meta.disclosedTrialDays = length.days;
|
|
19717
19962
|
if (input.facts.trialDays) meta.configuredTrialDays = input.facts.trialDays;
|
|
19718
|
-
const ownPlanOnly = (!!view.plan || !!cardOffer) && (!!view.checkoutConfig || !!view.offer) && planTrial !== void 0;
|
|
19963
|
+
const ownPlanOnly = (typeof paramsTrial === "number" || !!view.plan || !!cardOffer) && (!!view.checkoutConfig || !!view.offer) && planTrial !== void 0;
|
|
19719
19964
|
const trialPlans = [
|
|
19720
19965
|
...(input.ctx.config?.plans ?? []).map((pl) => ({ trialDays: pl.trialDays, source: configPlanSource(input.ctx) })),
|
|
19721
19966
|
...(input.ctx.referencePlans ?? []).map((pl) => ({ trialDays: pl.trialDays, source: pl.source }))
|
|
@@ -22025,6 +22270,24 @@ var init_brand_scope = __esm({
|
|
|
22025
22270
|
}
|
|
22026
22271
|
});
|
|
22027
22272
|
|
|
22273
|
+
// ../rules/src/set-off-fix.ts
|
|
22274
|
+
function setOffFix(text) {
|
|
22275
|
+
const own = /legible \((\d+(?:\.\d+)?)px, weight (\d+)/.exec(text);
|
|
22276
|
+
const ref = /look like the surrounding text \(about (\d+(?:\.\d+)?)px, weight (\d+)/.exec(text);
|
|
22277
|
+
if (!ref) return 'Set the renewal line apart from the text around it in a way Checkbox can measure: larger type (about 15% larger), semibold (weight 600+) where the text around it is not bold, a contrasting colour, or a border (e.g. style="border:1px solid currentColor;padding:8px 12px"). Any one of these is enough.';
|
|
22278
|
+
const refPx = Number(ref[1]);
|
|
22279
|
+
const refWeight = Number(ref[2]);
|
|
22280
|
+
const larger = Math.ceil(refPx * 1.15);
|
|
22281
|
+
const measured = own ? `It is ${own[1]}px, weight ${own[2]}; the text around it is about ${refPx}px, weight ${refWeight}${refWeight >= 600 ? " (already bold, so a bolder weight cannot set it apart)" : ""}.` : `The text around it is about ${refPx}px, weight ${refWeight}.`;
|
|
22282
|
+
const ways = [`${larger}px or larger`, ...refWeight < 600 ? ["semibold (font-weight 600 or more)"] : [], "a contrasting colour (not grey)", 'a border around it (e.g. style="border:1px solid currentColor;padding:8px 12px")'];
|
|
22283
|
+
return `Set the renewal line apart in a way Checkbox can measure. ${measured} Any one of these is enough: ${ways.join("; ")}.`;
|
|
22284
|
+
}
|
|
22285
|
+
var init_set_off_fix = __esm({
|
|
22286
|
+
"../rules/src/set-off-fix.ts"() {
|
|
22287
|
+
"use strict";
|
|
22288
|
+
}
|
|
22289
|
+
});
|
|
22290
|
+
|
|
22028
22291
|
// ../rules/src/status.ts
|
|
22029
22292
|
function tierOf(checkId) {
|
|
22030
22293
|
return CHECK_TIERS[checkId] ?? "required";
|
|
@@ -22121,10 +22384,18 @@ function cancelTestProblem(raw) {
|
|
|
22121
22384
|
}
|
|
22122
22385
|
function policyUnreadableFix(text) {
|
|
22123
22386
|
const n = /\((\d+) characters/.exec(text)?.[1];
|
|
22124
|
-
|
|
22125
|
-
|
|
22387
|
+
const doc = /\b(terms of service|cancellation policy|refund policy|privacy policy)\s*\((https?:\/\/[^\s),]+)/i.exec(text);
|
|
22388
|
+
const kind = doc?.[1]?.toLowerCase();
|
|
22389
|
+
const url = doc?.[2];
|
|
22390
|
+
const page = !kind || kind === "terms of service" ? "Terms page" : `${kind} page`;
|
|
22391
|
+
const named = url ? `Your ${page} (${url})` : `Your ${page}`;
|
|
22392
|
+
if (url && /\/p\/login\//i.test(url)) {
|
|
22393
|
+
return `${url} is a Stripe customer portal sign-in page, not a policy page, so there is no ${kind === "terms of service" || !kind ? "Terms" : kind} text to check there. Keep it as your cancel link (account page, emails); remove it as the ${kind === "terms of service" || !kind ? "Terms" : kind} URL (updateSetupSession ${kind === "refund policy" ? "refund_url" : kind === "cancellation policy" ? "cancellation_url" : "terms_url"}: null) or replace it with the page that states the policy. Checkbox reads it again on the next check.`;
|
|
22126
22394
|
}
|
|
22127
|
-
|
|
22395
|
+
if (n !== void 0 || /almost no text/i.test(text)) {
|
|
22396
|
+
return `${named} has only ${n !== void 0 ? `${n} characters` : "a few words"} of text, so there is nothing for Checkbox to check yet: add sections on how the subscription renews, how to cancel and your refund policy (or, if the page is built with JavaScript, serve that text in the HTML). Checkbox reads it again on the next check.`;
|
|
22397
|
+
}
|
|
22398
|
+
return `Checkbox could not read ${url ? `your ${page} (${url})` : `your ${page}`}: register a public URL that serves the policy text without a login (Site settings, or updateSite terms_url). Checkbox reads it again on the next check.`;
|
|
22128
22399
|
}
|
|
22129
22400
|
function finish(base, reason, checkId, tier = tierOf(checkId)) {
|
|
22130
22401
|
if (reason === "ny_owner_decision") return { ...base, label: NY_EMAIL_ONLY_LABEL, reason, tier };
|
|
@@ -22359,6 +22630,7 @@ var STATUS_LABELS, CHECK_TIERS, LIVE_CHECKS, has, isStripeInput, HOSTED_BEFORE_L
|
|
|
22359
22630
|
var init_status = __esm({
|
|
22360
22631
|
"../rules/src/status.ts"() {
|
|
22361
22632
|
"use strict";
|
|
22633
|
+
init_set_off_fix();
|
|
22362
22634
|
init_notice_content();
|
|
22363
22635
|
init_notices_on();
|
|
22364
22636
|
init_plan_terms();
|
|
@@ -22647,17 +22919,30 @@ var init_status = __esm({
|
|
|
22647
22919
|
{ code: "far_from_button", category: "content", when: has(/far from the purchase button|only far from/), build: () => ({ status: "needs_fix", fix: "Move the renewal line directly above (or right below) the pay button." }) },
|
|
22648
22920
|
{ code: "below_button_small", category: "content", when: has(/directly below the purchase button/), build: () => ({ status: "needs_fix", fix: "Move the renewal line under the price, before the card and email fields and the pay button, at body size." }) },
|
|
22649
22921
|
// Normal round 2 (nextjs, django): "looks like the surrounding text" is a measurement Checkbox cannot finish (borders,
|
|
22650
|
-
// symbols and italics are not captured), so it
|
|
22922
|
+
// symbols and italics are not captured), so it was a question to confirm by eye. Round 33 (pilot 32 item 18,
|
|
22923
|
+
// core-api-only / core-ci-pr / core-discovery: the owner was asked for "a decision" an agent could make in code, and the
|
|
22924
|
+
// recommended "make it semibold" could never pass next to an already bold price line): a fix with the measured values
|
|
22925
|
+
// and the ways Checkbox can measure (larger type, semibold where the text around is not bold, a contrasting colour, a
|
|
22926
|
+
// border). Never compliant.
|
|
22927
|
+
// A disclosure text check (kind "disclosure") only knows the styling the caller describes (placement.bold / boxed):
|
|
22928
|
+
// there it stays the question it was, with the same measurable options.
|
|
22651
22929
|
{
|
|
22652
22930
|
code: "not_set_off",
|
|
22931
|
+
category: "content",
|
|
22932
|
+
when: (c) => c.kind !== "disclosure" && /look like the surrounding text/.test(c.text),
|
|
22933
|
+
build: (c) => ({ status: "needs_fix", fix: setOffFix(c.text) })
|
|
22934
|
+
},
|
|
22935
|
+
{
|
|
22936
|
+
code: "not_set_off_described",
|
|
22653
22937
|
category: "legal_choice",
|
|
22654
|
-
when:
|
|
22655
|
-
build: () => ({
|
|
22938
|
+
when: (c) => c.kind === "disclosure" && /look like the surrounding text/.test(c.text),
|
|
22939
|
+
build: (c) => ({
|
|
22656
22940
|
status: "your_call",
|
|
22941
|
+
fix: setOffFix(c.text),
|
|
22657
22942
|
question: ask(
|
|
22658
|
-
"Confirm by eye: does the renewal line stand out from the text around it (semibold, a
|
|
22659
|
-
"
|
|
22660
|
-
"
|
|
22943
|
+
"Confirm by eye: does the renewal line stand out from the text around it (semibold where the text around it is not bold, larger type, a contrasting colour, or a border)?",
|
|
22944
|
+
"If it does, check again with placement.bold (or placement.boxed) set; if not, apply one of the ways in the fix.",
|
|
22945
|
+
"This check only knows the styling you describe; it cannot see your page."
|
|
22661
22946
|
)
|
|
22662
22947
|
})
|
|
22663
22948
|
},
|
|
@@ -23005,7 +23290,7 @@ var init_status = __esm({
|
|
|
23005
23290
|
{ code: "stripe_checkout_unchecked", category: "missing_evidence", when: has(/checkout session whose parameters checkbox has not checked|register the checkout session parameters/), build: (c) => ({ status: "pending_live_check", pending: pendingFor("", `${c.text} createstripecheckoutconfig`, c.data) }) }
|
|
23006
23291
|
];
|
|
23007
23292
|
STATUS_RULES = RULES.map((r) => ({ code: r.code, category: r.category }));
|
|
23008
|
-
NY_EMAIL_ONLY_LABEL = "Your decision: email only";
|
|
23293
|
+
NY_EMAIL_ONLY_LABEL = "Your decision: email only (not verified)";
|
|
23009
23294
|
NY_EMAIL_ONLY_NOTE = 'You recorded your decision to send renewal and reminder notices by email only. New York asks for notices "in the manner the consumer selected"; whether email that nobody picked meets that is not settled, and Checkbox does not decide it. You can add an Email choice at sign-up at any time.';
|
|
23010
23295
|
STRIPE_HOSTED_UNITS = /* @__PURE__ */ new Set(["checkout_config", "payment_link", "stripe_hosted"]);
|
|
23011
23296
|
PLATFORM_NAMES2 = { paddle: "Paddle", shopify: "Shopify", recharge: "Recharge", chargebee: "Chargebee", recurly: "Recurly" };
|
|
@@ -23807,12 +24092,12 @@ function disclosureParts(text) {
|
|
|
23807
24092
|
function disclosureHtml(text) {
|
|
23808
24093
|
return disclosureParts(text).map((p) => "url" in p ? `<a href="${esc(p.url)}">${esc(p.label)}</a>` : esc(p.text)).join("");
|
|
23809
24094
|
}
|
|
23810
|
-
function htmlDisclosureComponent(texts, sha2562) {
|
|
24095
|
+
function htmlDisclosureComponent(texts, sha2562, opts = {}) {
|
|
23811
24096
|
return `<!-- Checkbox renewal terms (generated from your plan's price, interval and cancellation URL; regenerate instead of editing the text).
|
|
23812
24097
|
Paste INSIDE your sign-up <form>, directly ABOVE the subscribe / purchase button.
|
|
23813
|
-
One line in your page's own font, size and colours: semibold sets it apart from normal-weight text (clear and conspicuous).
|
|
23814
|
-
If the text around it is already bold (a bold price line), put it in a bordered or shaded box instead
|
|
23815
|
-
<div class="checkbox-renewal-terms" data-checkbox-terms="${esc(sha2562)}" style="margin:12px 0;line-height:1.5
|
|
24098
|
+
${opts.boxed ? BOXED_NOTE : ` One line in your page's own font, size and colours: semibold sets it apart from normal-weight text (clear and conspicuous).
|
|
24099
|
+
If the text around it is already bold (a bold price line), put it in a bordered or shaded box instead.`} Keep it at body size and readable contrast; do not make it smaller or lighter than the text around it. -->
|
|
24100
|
+
<div class="checkbox-renewal-terms" data-checkbox-terms="${esc(sha2562)}" style="margin:12px 0;line-height:1.5;${opts.boxed ? BOXED_STYLE : ""}">
|
|
23816
24101
|
<p style="margin:0 0 6px;font-weight:600;">${disclosureHtml(texts.disclosure)}</p>
|
|
23817
24102
|
<label style="display:flex;gap:8px;align-items:flex-start;">
|
|
23818
24103
|
<!-- Never add "checked": consent must be an affirmative act. -->
|
|
@@ -23822,23 +24107,26 @@ function htmlDisclosureComponent(texts, sha2562) {
|
|
|
23822
24107
|
</div>
|
|
23823
24108
|
<button type="submit">${esc(texts.cta)}</button>`;
|
|
23824
24109
|
}
|
|
23825
|
-
function htmlTermsOnly(disclosure, sha2562, onStripe) {
|
|
24110
|
+
function htmlTermsOnly(disclosure, sha2562, onStripe, opts = {}) {
|
|
23826
24111
|
const where2 = onStripe === "payment_link" ? "Payment Link" : "Stripe Checkout";
|
|
23827
24112
|
return `<!-- Checkbox renewal terms (generated from your plan's price, interval and cancellation URL; regenerate instead of editing the text).
|
|
23828
24113
|
Paste directly ABOVE your own subscribe / Join link or button. Keep that link or button as it is.
|
|
23829
24114
|
No consent checkbox here: the ${where2} collects consent (consent_collection.terms_of_service = "required").
|
|
23830
|
-
One line in your page's own font, size and colours: semibold sets it apart from normal-weight text (clear and conspicuous).
|
|
23831
|
-
If the text around it is already bold (a bold price line), put it in a bordered or shaded box instead
|
|
23832
|
-
<div class="checkbox-renewal-terms" data-checkbox-terms="${esc(sha2562)}" style="margin:12px 0;line-height:1.5
|
|
24115
|
+
${opts.boxed ? BOXED_NOTE : ` One line in your page's own font, size and colours: semibold sets it apart from normal-weight text (clear and conspicuous).
|
|
24116
|
+
If the text around it is already bold (a bold price line), put it in a bordered or shaded box instead.`} -->
|
|
24117
|
+
<div class="checkbox-renewal-terms" data-checkbox-terms="${esc(sha2562)}" style="margin:12px 0;line-height:1.5;${opts.boxed ? BOXED_STYLE : ""}">
|
|
23833
24118
|
<p style="margin:0;font-weight:600;">${disclosureHtml(disclosure)}</p>
|
|
23834
24119
|
</div>`;
|
|
23835
24120
|
}
|
|
23836
|
-
var esc, URL_RE2;
|
|
24121
|
+
var esc, URL_RE2, BOXED_STYLE, BOXED_NOTE;
|
|
23837
24122
|
var init_html = __esm({
|
|
23838
24123
|
"../rules/src/fixes/html.ts"() {
|
|
23839
24124
|
"use strict";
|
|
23840
24125
|
esc = (s2) => s2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
23841
24126
|
URL_RE2 = /https?:\/\/[^\s<>"']+[^\s<>"'.,;:!?)]/g;
|
|
24127
|
+
BOXED_STYLE = "border:1px solid currentColor;border-radius:6px;padding:8px 12px;";
|
|
24128
|
+
BOXED_NOTE = ` The text next to your button is already bold, so a semibold line cannot stand out: this version is set off by a border
|
|
24129
|
+
(${BOXED_STYLE.replace(/;$/, "")}), which Checkbox measures. Keep the border, or use 15% larger type instead.`;
|
|
23842
24130
|
}
|
|
23843
24131
|
});
|
|
23844
24132
|
|
|
@@ -23865,6 +24153,10 @@ function fixTextsFor(t, style2) {
|
|
|
23865
24153
|
if (style2 === "full") return fixTexts(t);
|
|
23866
24154
|
return { disclosure: compactDisclosureText(t), consentLabel: COMPACT_CONSENT_LABEL, cta: compactCtaLabel(t) };
|
|
23867
24155
|
}
|
|
24156
|
+
function stripeCompactConsent(t) {
|
|
24157
|
+
const trial = t.trialDays && t.trialDays > 0 ? ` after the ${t.trialDays}-day free trial` : "";
|
|
24158
|
+
return `I agree that my subscription renews automatically at ${formatMoney(t.amount, t.currency)} every ${intervalPhrase(t.interval, t.intervalCount)}${trial} until I cancel.`;
|
|
24159
|
+
}
|
|
23868
24160
|
var COMPACT_CONSENT_LABEL;
|
|
23869
24161
|
var init_compact = __esm({
|
|
23870
24162
|
"../rules/src/fixes/compact.ts"() {
|
|
@@ -23881,9 +24173,10 @@ function shortDisclosureText(t) {
|
|
|
23881
24173
|
const trial = !!t.trialDays && t.trialDays > 0;
|
|
23882
24174
|
const intro = introPhrase(t);
|
|
23883
24175
|
const cancel = `Cancel anytime online at ${t.cancelUrl}`;
|
|
23884
|
-
|
|
23885
|
-
if (
|
|
23886
|
-
|
|
24176
|
+
const tax = " (plus applicable taxes)";
|
|
24177
|
+
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.`;
|
|
24178
|
+
if (intro) return `${intro[0].toUpperCase()}${intro.slice(1)}${tax}. Renews automatically until you cancel. ${cancel}.`;
|
|
24179
|
+
return `Renews automatically every ${every} at ${price3}${tax} until you cancel. ${cancel}.`;
|
|
23887
24180
|
}
|
|
23888
24181
|
var init_short = __esm({
|
|
23889
24182
|
"../rules/src/fixes/short.ts"() {
|
|
@@ -23916,7 +24209,7 @@ function modelAfterCapture(before, target, texts, opts) {
|
|
|
23916
24209
|
const ctaY = cta ? cta.rect.y : 700;
|
|
23917
24210
|
const ctaH = cta ? cta.rect.height : 48;
|
|
23918
24211
|
const stripe = target === "stripe_checkout" || target === "stripe_payment_link";
|
|
23919
|
-
const style2 = stripe ? STRIPE_CUSTOM_TEXT_STYLE : COMPONENT_STYLE;
|
|
24212
|
+
const style2 = stripe ? STRIPE_CUSTOM_TEXT_STYLE : opts.boxed ? { ...COMPONENT_STYLE, boxed: true } : COMPONENT_STYLE;
|
|
23920
24213
|
const disclosureH = textHeight(texts.disclosure, width, style2.fontSizePx);
|
|
23921
24214
|
const labelH = textHeight(texts.consentLabel, width - 28, style2.fontSizePx);
|
|
23922
24215
|
const disclosure = {
|
|
@@ -24048,7 +24341,7 @@ async function verifyFix(input) {
|
|
|
24048
24341
|
const rest = captures.filter((c) => !allCheckout(c));
|
|
24049
24342
|
const unchangedCheckout = latestPages(captures.filter((c) => allCheckout(c) && !matching.some((m) => m.id === c.id))).filter((c) => (c.ctas?.length ?? 0) > 0);
|
|
24050
24343
|
const otherCheckoutPages = new Set(unchangedCheckout.map((c) => c.url.split(/[?#]/)[0])).size;
|
|
24051
|
-
const after = base.map((c) => modelAfterCapture(c, input.target, input.texts, { consentCheckbox: consent }));
|
|
24344
|
+
const after = base.map((c) => modelAfterCapture(c, input.target, input.texts, { consentCheckbox: consent, ...input.boxed ? { boxed: true } : {} }));
|
|
24052
24345
|
const changedIds = new Set(objects.map((o) => o.id));
|
|
24053
24346
|
const afterConfigs = input.ctx.checkoutConfigs?.map(
|
|
24054
24347
|
(c) => changedIds.has(c.id) ? { ...c, capture: modelAfterCapture(c.capture, input.target, input.texts, { consentCheckbox: consent }), consentRequired: consent || c.consentRequired } : c
|
|
@@ -24313,13 +24606,18 @@ async function buildReplacement(plan2, jurisdictions, asOf, now, opts = {}) {
|
|
|
24313
24606
|
const verifyTexts = lockedTexts(verifyTerms);
|
|
24314
24607
|
const page = opts.onStripe && opts.page && (opts.page.ctas?.length ?? 0) > 0 ? [{ ...opts.page, pageKind: "checkout", source: "scanner" }] : [];
|
|
24315
24608
|
const ctx = modelContext(jurisdictions, asOf, planFacts(plan2), { config: planConfig(plan2, asOf), ...page.length ? { captures: page } : {}, ...plan2.merchantName ? { merchantNames: [plan2.merchantName] } : {} });
|
|
24316
|
-
const
|
|
24609
|
+
const v0 = await verifyFix({ ctx, target: "html", texts: verifyTexts, terms: verifyTerms, now, ...opts.boxed ? { boxed: true } : {}, ...opts.onStripe ? { consentCheckbox: false } : {} });
|
|
24610
|
+
const tryBoxed = !opts.boxed && page.length > 0 && !v0.passes && v0.remaining.includes("signup.disclosure.conspicuous");
|
|
24611
|
+
const vb = tryBoxed ? await verifyFix({ ctx, target: "html", texts: verifyTexts, terms: verifyTerms, now, boxed: true, ...opts.onStripe ? { consentCheckbox: false } : {} }) : void 0;
|
|
24612
|
+
const boxedOnPage = !!vb && !vb.remaining.includes("signup.disclosure.conspicuous") && vb.remaining.length < v0.remaining.length;
|
|
24613
|
+
const boxed = opts.boxed === true || boxedOnPage;
|
|
24614
|
+
const v = boxedOnPage ? vb : v0;
|
|
24317
24615
|
const sha = textsSha256(shown);
|
|
24318
24616
|
const placeholders = plan2.cancelUrl ? [] : [CANCEL_URL_PLACEHOLDER];
|
|
24319
24617
|
let short;
|
|
24320
24618
|
if (plan2.cancelUrl) {
|
|
24321
24619
|
const shortText = shortDisclosureText(verifyTerms);
|
|
24322
|
-
const sv = await verifyFix({ ctx, target: "html", texts: { ...verifyTexts, disclosure: shortText }, terms: verifyTerms, now, ...opts.onStripe ? { consentCheckbox: false } : {} });
|
|
24620
|
+
const sv = await verifyFix({ ctx, target: "html", texts: { ...verifyTexts, disclosure: shortText }, terms: verifyTerms, now, ...boxed ? { boxed: true } : {}, ...opts.onStripe ? { consentCheckbox: false } : {} });
|
|
24323
24621
|
if (sv.passes && v.passes && shortText !== shown.disclosure) short = shortText;
|
|
24324
24622
|
}
|
|
24325
24623
|
if (opts.onStripe) {
|
|
@@ -24330,9 +24628,9 @@ async function buildReplacement(plan2, jurisdictions, asOf, now, opts = {}) {
|
|
|
24330
24628
|
...short ? { short_disclosure_text: short } : {},
|
|
24331
24629
|
consent_label: shown.consentLabel,
|
|
24332
24630
|
button_text: shown.cta,
|
|
24333
|
-
html: htmlTermsOnly(shown.disclosure, sha, opts.onStripe),
|
|
24631
|
+
html: htmlTermsOnly(shown.disclosure, sha, opts.onStripe, boxed ? { boxed: true } : {}),
|
|
24334
24632
|
consent_on: opts.onStripe,
|
|
24335
|
-
note: `html is the renewal terms only (no checkbox, no button): paste it directly above your own purchase link and keep that link. Consent is collected on ${where2}: it needs consent_collection.terms_of_service = "required" and the renewal terms in custom_text (check it with ${check2}). consent_label and button_text are for a page that collects consent itself
|
|
24633
|
+
note: `html is the renewal terms only (no checkbox, no button): paste it directly above your own purchase link and keep that link. Consent is collected on ${where2}: it needs consent_collection.terms_of_service = "required" and the renewal terms in custom_text (check it with ${check2}). consent_label and button_text are for a page that collects consent itself.${boxed ? " The text next to your button is already bold, so a semibold renewal line cannot stand out: this html sets it off with a border, which Checkbox measures (or use type 15% larger than the text around it)." : ""}`,
|
|
24336
24634
|
placeholders,
|
|
24337
24635
|
verified: v.passes,
|
|
24338
24636
|
remaining_checks: [.../* @__PURE__ */ new Set([...v.remaining, "signup.consent.affirmative"])],
|
|
@@ -24344,7 +24642,7 @@ async function buildReplacement(plan2, jurisdictions, asOf, now, opts = {}) {
|
|
|
24344
24642
|
...short ? { short_disclosure_text: short } : {},
|
|
24345
24643
|
consent_label: shown.consentLabel,
|
|
24346
24644
|
button_text: shown.cta,
|
|
24347
|
-
html: htmlDisclosureComponent(shown, sha),
|
|
24645
|
+
html: htmlDisclosureComponent(shown, sha, boxed ? { boxed: true } : {}),
|
|
24348
24646
|
placeholders,
|
|
24349
24647
|
verified: v.passes,
|
|
24350
24648
|
remaining_checks: v.remaining,
|
|
@@ -24595,7 +24893,57 @@ var init_stripe = __esm({
|
|
|
24595
24893
|
}
|
|
24596
24894
|
});
|
|
24597
24895
|
|
|
24896
|
+
// ../rules/src/preflight/price-hint.ts
|
|
24897
|
+
function intervalHintOf(ref) {
|
|
24898
|
+
if (typeof ref !== "string" || !ref.trim() || ref.length > 200) return void 0;
|
|
24899
|
+
const s2 = ref.trim();
|
|
24900
|
+
if (GENERATED.test(s2)) return void 0;
|
|
24901
|
+
const tokens = s2.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
24902
|
+
const hits = /* @__PURE__ */ new Set();
|
|
24903
|
+
for (const t of tokens) for (const [k, re] of WORDS2) if (re.test(t)) hits.add(k);
|
|
24904
|
+
return hits.size === 1 ? [...hits][0] : void 0;
|
|
24905
|
+
}
|
|
24906
|
+
function priceIntervalConflict(ids, plan2) {
|
|
24907
|
+
const interval = plan2?.interval;
|
|
24908
|
+
if (!interval || !ids.length) return void 0;
|
|
24909
|
+
const count = Number(plan2?.interval_count ?? 1) || 1;
|
|
24910
|
+
for (const id of ids) {
|
|
24911
|
+
const hint = intervalHintOf(id);
|
|
24912
|
+
if (!hint) continue;
|
|
24913
|
+
const planUnit = interval === "month" && count === 12 ? "year" : interval;
|
|
24914
|
+
if (hint !== planUnit) {
|
|
24915
|
+
const name = plan2?.plan_name ?? plan2?.name;
|
|
24916
|
+
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}`;
|
|
24917
|
+
}
|
|
24918
|
+
}
|
|
24919
|
+
return void 0;
|
|
24920
|
+
}
|
|
24921
|
+
var WORDS2, GENERATED;
|
|
24922
|
+
var init_price_hint = __esm({
|
|
24923
|
+
"../rules/src/preflight/price-hint.ts"() {
|
|
24924
|
+
"use strict";
|
|
24925
|
+
WORDS2 = [
|
|
24926
|
+
["year", /^(?:annual|annually|yearly|year|years|yr|yrly|12mo|12m|1y|1yr)$/],
|
|
24927
|
+
["month", /^(?:monthly|month|months|mo|mth|1m|1mo)$/],
|
|
24928
|
+
["week", /^(?:weekly|week|weeks|wk)$/],
|
|
24929
|
+
["day", /^(?:daily|day|days)$/]
|
|
24930
|
+
];
|
|
24931
|
+
GENERATED = /^(?:price|plan|prod)_1[A-Za-z0-9]{12,}$/;
|
|
24932
|
+
}
|
|
24933
|
+
});
|
|
24934
|
+
|
|
24598
24935
|
// ../rules/src/preflight/stripe.ts
|
|
24936
|
+
function trialPromise(texts) {
|
|
24937
|
+
for (const [field, text] of texts) {
|
|
24938
|
+
if (!text) continue;
|
|
24939
|
+
const a = analyzeTerms(text);
|
|
24940
|
+
if (!a.trialMentions.length && !a.trialLengths.length) continue;
|
|
24941
|
+
const days = a.trialLengths.map((t) => "days" in t ? t.days : null).filter((d) => typeof d === "number" && d > 0);
|
|
24942
|
+
const span = a.trialLengths[0] ?? a.trialMentions[0];
|
|
24943
|
+
return { field, quote: span.text.trim(), days };
|
|
24944
|
+
}
|
|
24945
|
+
return void 0;
|
|
24946
|
+
}
|
|
24599
24947
|
function clone(v) {
|
|
24600
24948
|
return JSON.parse(JSON.stringify(v));
|
|
24601
24949
|
}
|
|
@@ -24687,6 +25035,25 @@ function readParams(params, merchant, kind, asOf) {
|
|
|
24687
25035
|
if (trial !== void 0 && trial > 0 && custom && /[$€£]\s?\d|\b(?:charged?|billed)\b/i.test(custom) && !/\b(?:trial|free|days?)\b/i.test(custom)) {
|
|
24688
25036
|
caveats.push({ checks: ["signup.disclosure.trial_terms"], status: "fail", reason: `there is a ${trial}-day free trial, but your custom_text.submit states the charge without mentioning it, so a shopper reads it as charged today (Stripe's summary alone shows the trial) \u2014 state the ${trial}-day free trial and what is charged when it ends`, param: "params.custom_text.submit" });
|
|
24689
25037
|
}
|
|
25038
|
+
const promisedTrial = trialPromise([["custom_text.submit", msg(ctAll.submit)], ["custom_text.terms_of_service_acceptance", isObj(params.consent_collection) && params.consent_collection.terms_of_service === "required" ? msg(ctAll.terms_of_service_acceptance) : void 0], ["product description", description]]);
|
|
25039
|
+
if (promisedTrial && !(trial !== void 0 && trial > 0)) {
|
|
25040
|
+
caveats.push({
|
|
25041
|
+
checks: ["signup.disclosure.trial_terms"],
|
|
25042
|
+
status: "fail",
|
|
25043
|
+
reason: `the ${promisedTrial.field} promises a free trial (\u201C${promisedTrial.quote}\u201D), but the params run none (no subscription_data.trial_period_days or trial_end), so Stripe charges the full price today \u2014 add the trial to subscription_data, or remove the promise from the text`,
|
|
25044
|
+
param: promisedTrial.field === "product description" ? "params.line_items" : `params.${promisedTrial.field}.message`
|
|
25045
|
+
});
|
|
25046
|
+
} else if (promisedTrial && trial !== void 0 && trial > 0) {
|
|
25047
|
+
const wrongDays = promisedTrial.days.find((d) => Math.abs(d - trial) > TRIAL_DAYS_SLACK);
|
|
25048
|
+
if (wrongDays !== void 0) {
|
|
25049
|
+
caveats.push({
|
|
25050
|
+
checks: ["signup.disclosure.trial_terms"],
|
|
25051
|
+
status: "fail",
|
|
25052
|
+
reason: `the ${promisedTrial.field} promises a ${wrongDays}-day free trial (\u201C${promisedTrial.quote}\u201D), but the params run a ${trial}-day trial \u2014 state the ${trial}-day free trial subscribers get, or change subscription_data.trial_period_days`,
|
|
25053
|
+
param: promisedTrial.field === "product description" ? "params.line_items" : `params.${promisedTrial.field}.message`
|
|
25054
|
+
});
|
|
25055
|
+
}
|
|
25056
|
+
}
|
|
24690
25057
|
const uiMode = typeof params.ui_mode === "string" ? params.ui_mode : void 0;
|
|
24691
25058
|
if (uiMode === "custom" || uiMode === "elements") {
|
|
24692
25059
|
caveats.push({
|
|
@@ -24964,6 +25331,7 @@ function correct(params, shape, checks, forLink, style2 = "full") {
|
|
|
24964
25331
|
out.custom_text = set;
|
|
24965
25332
|
} else {
|
|
24966
25333
|
for (const [k, v] of Object.entries(set)) {
|
|
25334
|
+
if (JSON.stringify(ct[k]) === JSON.stringify(v)) continue;
|
|
24967
25335
|
patch.push({ op: k in ct && ct[k] !== null ? "replace" : "add", path: `/custom_text/${k}`, value: v });
|
|
24968
25336
|
ct[k] = v;
|
|
24969
25337
|
}
|
|
@@ -24987,10 +25355,6 @@ function correct(params, shape, checks, forLink, style2 = "full") {
|
|
|
24987
25355
|
}
|
|
24988
25356
|
return { patch, corrected: out, texts, needText, needConsent };
|
|
24989
25357
|
}
|
|
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
25358
|
async function bestCorrection(params, shape, checks, forLink, judgeAfter) {
|
|
24995
25359
|
const none = () => ({ fix: correct(params, shape, [], forLink), after: checks });
|
|
24996
25360
|
const first2 = correct(params, shape, checks, forLink, "compact");
|
|
@@ -25071,6 +25435,12 @@ function withResolvedPrices(input, knownPrices, amountSource = "merchant.price")
|
|
|
25071
25435
|
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
25436
|
};
|
|
25073
25437
|
}
|
|
25438
|
+
function pricePairingConflict(input, resolved) {
|
|
25439
|
+
if (!resolved.warnings.some((w) => w.code === "price_amount_unverified")) return void 0;
|
|
25440
|
+
const conflict = priceIntervalConflict(lineItemPriceIds(input.params), input.merchant);
|
|
25441
|
+
if (!conflict) return void 0;
|
|
25442
|
+
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" };
|
|
25443
|
+
}
|
|
25074
25444
|
async function checkStripeCheckoutSession(input0, opts = {}) {
|
|
25075
25445
|
const { jurisdictions, asOf, now } = common(input0, opts.now);
|
|
25076
25446
|
const resolvedPrices = withResolvedPrices(input0, opts.knownPrices, opts.amountSource);
|
|
@@ -25086,7 +25456,8 @@ async function checkStripeCheckoutSession(input0, opts = {}) {
|
|
|
25086
25456
|
const shape = readParams(params, input.merchant, "session", asOf);
|
|
25087
25457
|
const url = "https://checkout.stripe.com/c/pay/cs_test_preflight";
|
|
25088
25458
|
const checks = await judge2(shape, url, jurisdictions, asOf, now, "preflight-stripe-before");
|
|
25089
|
-
const
|
|
25459
|
+
const pairing = pricePairingConflict(input0, resolvedPrices);
|
|
25460
|
+
const { fix, after } = pairing ? { fix: correct(params, shape, [], false), after: checks } : await bestCorrection(
|
|
25090
25461
|
params,
|
|
25091
25462
|
shape,
|
|
25092
25463
|
checks,
|
|
@@ -25101,12 +25472,12 @@ async function checkStripeCheckoutSession(input0, opts = {}) {
|
|
|
25101
25472
|
verification: { checks: d.checks, passes: d.passes, remaining_checks: d.remaining, note: HOSTED_STYLING_NOTE },
|
|
25102
25473
|
placeholders
|
|
25103
25474
|
};
|
|
25104
|
-
const warnings = [...resolvedPrices.warnings, ...shape.warnings];
|
|
25475
|
+
const warnings = [...resolvedPrices.warnings, ...pairing ? [pairing] : [], ...shape.warnings];
|
|
25105
25476
|
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
25477
|
const channel2 = noticeChannelField(params);
|
|
25107
25478
|
if (channel2) warnings.push(channel2);
|
|
25108
25479
|
return {
|
|
25109
|
-
...baseReport("stripe_checkout_session", checks, jurisdictions, asOf, warnings, nextAction("session", checks, correction), void 0, { cancelUrl: shape.plan?.cancelUrl }),
|
|
25480
|
+
...baseReport("stripe_checkout_session", checks, jurisdictions, asOf, warnings, pairing ? PAIRING_ACTION(pairing) : nextAction("session", checks, correction), void 0, { cancelUrl: shape.plan?.cancelUrl }),
|
|
25110
25481
|
kind: "stripe_checkout_session",
|
|
25111
25482
|
correction
|
|
25112
25483
|
};
|
|
@@ -25124,7 +25495,8 @@ async function checkStripePaymentLink(input0, opts = {}) {
|
|
|
25124
25495
|
const id = typeof params.id === "string" ? params.id : void 0;
|
|
25125
25496
|
const url = typeof params.url === "string" && /^https:\/\/buy\.stripe\.com\//.test(params.url) ? params.url : "https://buy.stripe.com/test_preflight";
|
|
25126
25497
|
const checks = await judge2(shape, url, jurisdictions, asOf, now, "preflight-link-before");
|
|
25127
|
-
const
|
|
25498
|
+
const pairing = pricePairingConflict(input0, resolvedPrices);
|
|
25499
|
+
const { fix, after } = pairing ? { fix: correct(params, shape, [], true), after: checks } : await bestCorrection(
|
|
25128
25500
|
params,
|
|
25129
25501
|
shape,
|
|
25130
25502
|
checks,
|
|
@@ -25163,11 +25535,11 @@ async function checkStripePaymentLink(input0, opts = {}) {
|
|
|
25163
25535
|
...update_params ? { update_params } : {},
|
|
25164
25536
|
needs_new_link_for_consent: needsNew
|
|
25165
25537
|
};
|
|
25166
|
-
const warnings = [...resolvedPrices.warnings, ...shape.warnings];
|
|
25538
|
+
const warnings = [...resolvedPrices.warnings, ...pairing ? [pairing] : [], ...shape.warnings];
|
|
25167
25539
|
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
25540
|
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
25541
|
return {
|
|
25170
|
-
...baseReport("stripe_payment_link", checks, jurisdictions, asOf, warnings, nextAction("link", checks, correction, extra), void 0, { cancelUrl: shape.plan?.cancelUrl }),
|
|
25542
|
+
...baseReport("stripe_payment_link", checks, jurisdictions, asOf, warnings, pairing ? PAIRING_ACTION(pairing) : nextAction("link", checks, correction, extra), void 0, { cancelUrl: shape.plan?.cancelUrl }),
|
|
25171
25543
|
kind: "stripe_payment_link",
|
|
25172
25544
|
correction
|
|
25173
25545
|
};
|
|
@@ -25227,7 +25599,10 @@ function modelCheckoutConfig(params, opts) {
|
|
|
25227
25599
|
// travels with its plan, so the site's checks and copy know the promo without a separate declaration.
|
|
25228
25600
|
...plan2.introAmount !== void 0 && plan2.introAmount >= 0 && plan2.introAmount < plan2.amount ? { introAmount: plan2.introAmount, ...plan2.introPeriods && plan2.introPeriods > 1 ? { introPeriods: plan2.introPeriods } : {} } : {}
|
|
25229
25601
|
},
|
|
25230
|
-
consentRequired: shape.consentRequired
|
|
25602
|
+
consentRequired: shape.consentRequired,
|
|
25603
|
+
// Round 33: the trial the params themselves run (subscription_data), 0 when none; a trial taken only from a Price
|
|
25604
|
+
// or merchant terms is not one Stripe runs for these params.
|
|
25605
|
+
trialDays: plan2.trialDays ?? 0
|
|
25231
25606
|
};
|
|
25232
25607
|
}
|
|
25233
25608
|
function noticeChannelField(params) {
|
|
@@ -25246,7 +25621,7 @@ function noticeChannelField(params) {
|
|
|
25246
25621
|
param: `params.custom_fields[${i}]`
|
|
25247
25622
|
};
|
|
25248
25623
|
}
|
|
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;
|
|
25624
|
+
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
25625
|
var init_stripe2 = __esm({
|
|
25251
25626
|
"../rules/src/preflight/stripe.ts"() {
|
|
25252
25627
|
"use strict";
|
|
@@ -25254,11 +25629,13 @@ var init_stripe2 = __esm({
|
|
|
25254
25629
|
init_compact();
|
|
25255
25630
|
init_plan_terms();
|
|
25256
25631
|
init_text();
|
|
25632
|
+
init_signup();
|
|
25257
25633
|
init_verify();
|
|
25258
25634
|
init_stripe();
|
|
25259
25635
|
init_common();
|
|
25260
25636
|
init_disclosure();
|
|
25261
25637
|
init_model();
|
|
25638
|
+
init_price_hint();
|
|
25262
25639
|
TEXT_CHECKS = [
|
|
25263
25640
|
"signup.disclosure.autorenew_language",
|
|
25264
25641
|
"signup.disclosure.price",
|
|
@@ -25277,6 +25654,7 @@ var init_stripe2 = __esm({
|
|
|
25277
25654
|
STRIPE_GREY = { fontSizePx: 13, fontWeight: 400, color: "rgba(26, 26, 26, 0.7)", backgroundColor: "rgb(255, 255, 255)" };
|
|
25278
25655
|
DAY_MS3 = 864e5;
|
|
25279
25656
|
open = (c) => !!c && (c.status === "fail" || c.status === "needs_review");
|
|
25657
|
+
PAIRING_ACTION = (w) => ({ type: "human_review", message: `${w.message}` });
|
|
25280
25658
|
STRIPE_HOSTED_STYLING_NOTE = HOSTED_STYLING_NOTE;
|
|
25281
25659
|
}
|
|
25282
25660
|
});
|
|
@@ -25286,6 +25664,8 @@ function stripeParamsVerdict(r) {
|
|
|
25286
25664
|
if (r.verdict === "not_applicable") return { verdict: "not_applicable", open: [] };
|
|
25287
25665
|
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
25666
|
if (r.verdict === "fail" || r.compliance_status === "needs_fix" || open2.some((c) => c.status === "fail")) return { verdict: "fail", open: open2 };
|
|
25667
|
+
const pairing = (r.warnings ?? []).find((w) => w.code === "price_plan_conflict");
|
|
25668
|
+
if (pairing) return { verdict: "needs_review", open: open2, reason: pairing.message };
|
|
25289
25669
|
const coupon = (r.warnings ?? []).find((w) => w.code === "coupon_unverified");
|
|
25290
25670
|
if (open2.length) return { verdict: "needs_review", open: open2, ...coupon ? { reason: coupon.message } : {} };
|
|
25291
25671
|
if (coupon) return { verdict: "needs_review", open: [], reason: coupon.message };
|
|
@@ -25686,7 +26066,8 @@ ${html ?? ""}`, kind);
|
|
|
25686
26066
|
text: t.text,
|
|
25687
26067
|
html: htmlOf(t.text),
|
|
25688
26068
|
merge_tags: t.tags,
|
|
25689
|
-
|
|
26069
|
+
// Round 32: only the placeholders the template really contains (a cancellation confirmation has no {{cancel_url}}).
|
|
26070
|
+
placeholders: [CANCEL_URL_PLACEHOLDER, SUPPORT_CONTACT_PLACEHOLDER].filter((p) => t.text.includes(p) || t.subject.includes(p)),
|
|
25690
26071
|
verified: remaining.length === 0,
|
|
25691
26072
|
remaining_checks: remaining
|
|
25692
26073
|
};
|
|
@@ -25956,7 +26337,8 @@ async function checkPageHtml(input, opts = {}) {
|
|
|
25956
26337
|
});
|
|
25957
26338
|
}
|
|
25958
26339
|
const consentOnStripe = onStripe ?? (checkoutType === "payment_link" || checkoutType === "stripe_checkout" ? checkoutType : checkoutType ? void 0 : stripeButtonsKind(capture));
|
|
25959
|
-
const
|
|
26340
|
+
const boldAround = checks.some((c) => c.check_id === "signup.disclosure.conspicuous" && /already bold, so a bolder weight cannot set them apart/.test(c.explanation ?? ""));
|
|
26341
|
+
const repOpts = { ...consentOnStripe ? { onStripe: consentOnStripe } : {}, ...boldAround ? { boxed: true } : {} };
|
|
25960
26342
|
const built = plan2 ? await buildReplacement(plan2, jurisdictions, asOf, now, { ...repOpts, page: capture }) : void 0;
|
|
25961
26343
|
const differs = plan2 && !multi ? pagePriceDiffers(run2.findings) : [];
|
|
25962
26344
|
if (plan2 && differs.length) {
|
|
@@ -26039,7 +26421,8 @@ function signupStripeObjects(input) {
|
|
|
26039
26421
|
capture: { ...m.capture, siteId: input.siteId },
|
|
26040
26422
|
consentRequired: m.consentRequired,
|
|
26041
26423
|
kind: "checkout_config",
|
|
26042
|
-
...m.plan ? { planId: m.plan.id } : {}
|
|
26424
|
+
...m.plan ? { planId: m.plan.id } : {},
|
|
26425
|
+
...typeof m.trialDays === "number" ? { trialDays: m.trialDays } : {}
|
|
26043
26426
|
});
|
|
26044
26427
|
}
|
|
26045
26428
|
if (m.plan) referencePlans2.push({ ...m.plan, source: "checkout_config", ...row.name ? { label: row.name } : {} });
|
|
@@ -26072,7 +26455,8 @@ function signupStripeObjects(input) {
|
|
|
26072
26455
|
consentRequired: m.consentRequired,
|
|
26073
26456
|
kind: "payment_link",
|
|
26074
26457
|
...link.url ? { url: link.url } : {},
|
|
26075
|
-
...m.plan ? { planId: m.plan.id } : {}
|
|
26458
|
+
...m.plan ? { planId: m.plan.id } : {},
|
|
26459
|
+
...typeof m.trialDays === "number" ? { trialDays: m.trialDays } : {}
|
|
26076
26460
|
});
|
|
26077
26461
|
}
|
|
26078
26462
|
if (m.plan) referencePlans2.push({ ...m.plan, source: "checkout_config", label: `Payment Link \u201C${name}\u201D` });
|
|
@@ -26114,6 +26498,7 @@ __export(preflight_exports, {
|
|
|
26114
26498
|
checkoutSessionsOf: () => checkoutSessionsOf,
|
|
26115
26499
|
emailTemplate: () => emailTemplate,
|
|
26116
26500
|
extractStaticPage: () => extractStaticPage,
|
|
26501
|
+
intervalHintOf: () => intervalHintOf,
|
|
26117
26502
|
lineItemPriceIds: () => lineItemPriceIds,
|
|
26118
26503
|
modelCheckoutConfig: () => modelCheckoutConfig,
|
|
26119
26504
|
modelContext: () => modelContext,
|
|
@@ -26121,6 +26506,7 @@ __export(preflight_exports, {
|
|
|
26121
26506
|
parseHtml: () => parseHtml,
|
|
26122
26507
|
parseJurisdictionList: () => parseJurisdictionList,
|
|
26123
26508
|
paymentLinksOf: () => paymentLinksOf,
|
|
26509
|
+
priceIntervalConflict: () => priceIntervalConflict,
|
|
26124
26510
|
resolvePlan: () => resolvePlan,
|
|
26125
26511
|
resolvePriceIds: () => resolvePriceIds,
|
|
26126
26512
|
runChecks: () => runChecks,
|
|
@@ -26128,7 +26514,10 @@ __export(preflight_exports, {
|
|
|
26128
26514
|
signupStripeObjects: () => signupStripeObjects,
|
|
26129
26515
|
stripeParamsVerdict: () => stripeParamsVerdict,
|
|
26130
26516
|
supportContactLine: () => supportContactLine,
|
|
26131
|
-
|
|
26517
|
+
testRunSessionIds: () => testRunSessionIds,
|
|
26518
|
+
trialPromise: () => trialPromise,
|
|
26519
|
+
withIntroOffers: () => withIntroOffers,
|
|
26520
|
+
withTestRunSessions: () => withTestRunSessions
|
|
26132
26521
|
});
|
|
26133
26522
|
var init_preflight = __esm({
|
|
26134
26523
|
"../rules/src/preflight/index.ts"() {
|
|
@@ -26136,6 +26525,7 @@ var init_preflight = __esm({
|
|
|
26136
26525
|
init_common();
|
|
26137
26526
|
init_disclosure();
|
|
26138
26527
|
init_stripe2();
|
|
26528
|
+
init_price_hint();
|
|
26139
26529
|
init_stripe_verdict();
|
|
26140
26530
|
init_email();
|
|
26141
26531
|
init_html_page();
|
|
@@ -26321,7 +26711,8 @@ var init_check = __esm({
|
|
|
26321
26711
|
}
|
|
26322
26712
|
const hf = str(f, "html-file");
|
|
26323
26713
|
if (hf) html = await readInput(ctx, hf);
|
|
26324
|
-
if (!subject
|
|
26714
|
+
if (!subject && html) subject = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html)?.[1]?.replace(/\s+/g, " ").trim() || void 0;
|
|
26715
|
+
if (!subject) throw new UsageError('--subject is required (or a "Subject: \u2026" first line in --file, or an HTML <title>)');
|
|
26325
26716
|
if (!text && !html) throw new UsageError("Give the email body with --text, --file or --html-file");
|
|
26326
26717
|
const terms2 = planFrom(f);
|
|
26327
26718
|
const pp = str(f, "previous-price");
|
|
@@ -26483,7 +26874,7 @@ function loadConfig(path) {
|
|
|
26483
26874
|
function validateConfig(value) {
|
|
26484
26875
|
const is = new Issues();
|
|
26485
26876
|
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"]);
|
|
26877
|
+
is.keys("", value, ["$schema", "merchant", "jurisdictions", "plans", "notices", "merge_tags", "checks", "coverage"]);
|
|
26487
26878
|
const cov = value.coverage;
|
|
26488
26879
|
if (cov !== void 0 && cov !== false) {
|
|
26489
26880
|
if (!isObj2(cov)) is.add("coverage", `must be false or an object like { "ignore": ["scripts/**"] } (got ${show(cov)})`);
|
|
@@ -26551,6 +26942,15 @@ function validateConfig(value) {
|
|
|
26551
26942
|
}
|
|
26552
26943
|
}
|
|
26553
26944
|
}
|
|
26945
|
+
const mergeTags = (at, m2) => {
|
|
26946
|
+
if (m2 === void 0) return;
|
|
26947
|
+
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)})`);
|
|
26948
|
+
for (const [k, v] of Object.entries(m2)) {
|
|
26949
|
+
if (!/^[A-Za-z_][\w.-]{0,60}$/.test(k)) is.add(`${at}.${k}`, "is not a merge tag name (letters, digits, _ . -; the name inside {{ }})");
|
|
26950
|
+
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)})`);
|
|
26951
|
+
}
|
|
26952
|
+
};
|
|
26953
|
+
mergeTags("merge_tags", value.merge_tags);
|
|
26554
26954
|
const checks = value.checks;
|
|
26555
26955
|
if (!isObj2(checks)) {
|
|
26556
26956
|
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 +27035,8 @@ function validateConfig(value) {
|
|
|
26635
27035
|
entries(
|
|
26636
27036
|
"emails",
|
|
26637
27037
|
(at, e) => {
|
|
26638
|
-
is.keys(at, e, ["name", "plan", "kind", "subject", "file", "text", "html", "module", "args", "previous_price"]);
|
|
27038
|
+
is.keys(at, e, ["name", "plan", "kind", "subject", "file", "text", "html", "module", "args", "previous_price", "merge_tags"]);
|
|
27039
|
+
mergeTags(`${at}.merge_tags`, e.merge_tags);
|
|
26639
27040
|
is.oneOf(`${at}.kind`, e.kind, EMAIL_KINDS2, true);
|
|
26640
27041
|
is.exactlyOne(at, e, ["file", "text", "html", "module"], [["text", "html"]]);
|
|
26641
27042
|
if (e.file !== void 0) {
|
|
@@ -26668,7 +27069,7 @@ function entryLine(raw, name) {
|
|
|
26668
27069
|
const i = lines.findIndex((l) => needle.test(l));
|
|
26669
27070
|
return i >= 0 ? i + 1 : 1;
|
|
26670
27071
|
}
|
|
26671
|
-
var CONFIG_FILES, SCHEMA_URL, INTERVALS4, EMAIL_KINDS2, PAGE_KINDS2, ConfigError, isObj2, show, TODO, Issues, PLAN_KEYS;
|
|
27072
|
+
var CONFIG_FILES, SCHEMA_URL, INTERVALS4, EMAIL_KINDS2, PAGE_KINDS2, MERGE_TAG_FIELDS, ConfigError, isObj2, show, TODO, Issues, PLAN_KEYS;
|
|
26672
27073
|
var init_config3 = __esm({
|
|
26673
27074
|
"src/ci/config.ts"() {
|
|
26674
27075
|
"use strict";
|
|
@@ -26677,6 +27078,7 @@ var init_config3 = __esm({
|
|
|
26677
27078
|
INTERVALS4 = ["day", "week", "month", "year"];
|
|
26678
27079
|
EMAIL_KINDS2 = ["acknowledgment", "trial_reminder", "renewal_reminder", "price_change", "annual_terms", "charge_notice", "cancellation_confirmation"];
|
|
26679
27080
|
PAGE_KINDS2 = ["checkout", "pricing"];
|
|
27081
|
+
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
27082
|
ConfigError = class extends Error {
|
|
26681
27083
|
constructor(file, issues) {
|
|
26682
27084
|
super(`${file}: ${issues.length === 1 ? issues[0] : `${issues.length} problems:
|
|
@@ -26761,7 +27163,7 @@ function parseObjectLiteral(source, env2 = {}, consts = {}) {
|
|
|
26761
27163
|
const p = new Parser(source, env2, consts);
|
|
26762
27164
|
if (p.peek() !== "{") return { value: null, unresolved: ["<not an object literal>"] };
|
|
26763
27165
|
const v = p.object("");
|
|
26764
|
-
return { value: v === UNKNOWN ? null : v, unresolved: [...new Set(p.unresolved)], ...Object.keys(p.fromEnv).length ? { fromEnv: p.fromEnv } : {} };
|
|
27166
|
+
return { value: v === UNKNOWN ? null : v, unresolved: [...new Set(p.unresolved)], ...Object.keys(p.fromEnv).length ? { fromEnv: p.fromEnv } : {}, ...p.spreads.length ? { spreads: p.spreads } : {} };
|
|
26765
27167
|
}
|
|
26766
27168
|
var UNKNOWN, Parser;
|
|
26767
27169
|
var init_literal = __esm({
|
|
@@ -26777,6 +27179,7 @@ var init_literal = __esm({
|
|
|
26777
27179
|
i = 0;
|
|
26778
27180
|
unresolved = [];
|
|
26779
27181
|
fromEnv = {};
|
|
27182
|
+
spreads = [];
|
|
26780
27183
|
ws() {
|
|
26781
27184
|
for (; ; ) {
|
|
26782
27185
|
while (this.i < this.s.length && /\s/.test(this.s[this.i])) this.i++;
|
|
@@ -26913,7 +27316,9 @@ var init_literal = __esm({
|
|
|
26913
27316
|
if (!ch) return UNKNOWN;
|
|
26914
27317
|
if (this.s.startsWith("...", this.i)) {
|
|
26915
27318
|
this.i += 3;
|
|
27319
|
+
const from = this.i;
|
|
26916
27320
|
this.skipExpr();
|
|
27321
|
+
if (!path) this.spreads.push(this.s.slice(from, this.i).trim());
|
|
26917
27322
|
this.unresolved.push(`${path ? `${path}.` : ""}...`);
|
|
26918
27323
|
} else {
|
|
26919
27324
|
let key = null;
|
|
@@ -27030,6 +27435,12 @@ function detectBrand(root, layoutFile, pkgName) {
|
|
|
27030
27435
|
}
|
|
27031
27436
|
return void 0;
|
|
27032
27437
|
}
|
|
27438
|
+
function detectMailer(deps, framework, hasPackageJson) {
|
|
27439
|
+
const found = MAILER_PACKAGES.find((m) => m in deps);
|
|
27440
|
+
if (found) return found;
|
|
27441
|
+
if (!hasPackageJson && framework === "html") return "none";
|
|
27442
|
+
return null;
|
|
27443
|
+
}
|
|
27033
27444
|
function readJson(path) {
|
|
27034
27445
|
try {
|
|
27035
27446
|
return JSON.parse(readFileSync5(path, "utf8"));
|
|
@@ -27089,12 +27500,49 @@ function walk2(root, visit) {
|
|
|
27089
27500
|
}
|
|
27090
27501
|
}
|
|
27091
27502
|
}
|
|
27503
|
+
function hasAppRouteFile(dir, depth = 0) {
|
|
27504
|
+
if (depth > 3) return false;
|
|
27505
|
+
let names = [];
|
|
27506
|
+
try {
|
|
27507
|
+
names = readdirSync2(dir);
|
|
27508
|
+
} catch {
|
|
27509
|
+
return false;
|
|
27510
|
+
}
|
|
27511
|
+
if (names.some((n) => /^(?:page|route|layout)\.(?:tsx|jsx|ts|js|mdx)$/.test(n))) return true;
|
|
27512
|
+
return names.some((n) => !n.startsWith(".") && n !== "node_modules" && (() => {
|
|
27513
|
+
try {
|
|
27514
|
+
return statSync(join4(dir, n)).isDirectory();
|
|
27515
|
+
} catch {
|
|
27516
|
+
return false;
|
|
27517
|
+
}
|
|
27518
|
+
})() && hasAppRouteFile(join4(dir, n), depth + 1));
|
|
27519
|
+
}
|
|
27520
|
+
function hasTsFiles(dir, depth = 0) {
|
|
27521
|
+
if (depth > 3) return false;
|
|
27522
|
+
let names = [];
|
|
27523
|
+
try {
|
|
27524
|
+
names = readdirSync2(dir);
|
|
27525
|
+
} catch {
|
|
27526
|
+
return false;
|
|
27527
|
+
}
|
|
27528
|
+
if (names.some((n) => /\.tsx?$/.test(n))) return true;
|
|
27529
|
+
return names.some((n) => !n.startsWith(".") && (() => {
|
|
27530
|
+
try {
|
|
27531
|
+
return statSync(join4(dir, n)).isDirectory();
|
|
27532
|
+
} catch {
|
|
27533
|
+
return false;
|
|
27534
|
+
}
|
|
27535
|
+
})() && hasTsFiles(join4(dir, n), depth + 1));
|
|
27536
|
+
}
|
|
27092
27537
|
function detectFramework(root, deps) {
|
|
27093
27538
|
const has2 = (d) => d in deps;
|
|
27094
27539
|
if (existsSync4(join4(root, "layout", "theme.liquid")) && existsSync4(join4(root, "config", "settings_schema.json"))) return { framework: "shopify-theme", layoutFile: "layout/theme.liquid" };
|
|
27095
27540
|
if (has2("next")) {
|
|
27096
27541
|
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
27542
|
if (app) return { framework: "nextjs-app", layoutFile: app };
|
|
27543
|
+
const appDir = ["src/app", "app"].find((d) => existsSync4(join4(root, d)) && hasAppRouteFile(join4(root, d)));
|
|
27544
|
+
const pagesExists = existsSync4(join4(root, "pages")) || existsSync4(join4(root, "src", "pages"));
|
|
27545
|
+
if (appDir && !pagesExists) return { framework: "nextjs-app", layoutFile: `${appDir}/layout.${existsSync4(join4(root, "tsconfig.json")) || hasTsFiles(join4(root, appDir)) ? "tsx" : "js"}` };
|
|
27098
27546
|
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
27547
|
const pagesDir = existsSync4(join4(root, "src", "pages")) ? "src/pages" : "pages";
|
|
27100
27548
|
return { framework: "nextjs-pages", layoutFile: doc ?? `${pagesDir}/_document.${existsSync4(join4(root, "tsconfig.json")) ? "tsx" : "js"}` };
|
|
@@ -27181,11 +27629,11 @@ function routeOf(rel2) {
|
|
|
27181
27629
|
}
|
|
27182
27630
|
return null;
|
|
27183
27631
|
}
|
|
27184
|
-
function findStripeKey(env2, processEnv) {
|
|
27632
|
+
function findStripeKey(env2, processEnv, envFile = {}) {
|
|
27185
27633
|
const sources = [];
|
|
27186
27634
|
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
27635
|
sources.push([`${k} (environment)`, processEnv[k]]);
|
|
27188
|
-
sources.push([`${k} (.env)`, env2[k]]);
|
|
27636
|
+
sources.push([`${k} (${envFile[k] ?? ".env"})`, env2[k]]);
|
|
27189
27637
|
}
|
|
27190
27638
|
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
27639
|
const hint = (key) => `${key.slice(0, 8)}\u2026${key.slice(-4)}`;
|
|
@@ -27285,11 +27733,15 @@ function detectProject(root, processEnv = {}) {
|
|
|
27285
27733
|
const { framework, layoutFile } = detectFramework(root, deps);
|
|
27286
27734
|
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
27735
|
const env2 = {};
|
|
27736
|
+
const envFile = {};
|
|
27288
27737
|
for (const f of [".env", ".env.production", ".env.local", ".env.development", ".env.development.local"]) {
|
|
27289
27738
|
const t = readText(join4(root, f));
|
|
27290
|
-
if (t)
|
|
27739
|
+
if (!t) continue;
|
|
27740
|
+
const vars = parseDotenv(t);
|
|
27741
|
+
Object.assign(env2, vars);
|
|
27742
|
+
for (const k of Object.keys(vars)) envFile[k] = f;
|
|
27291
27743
|
}
|
|
27292
|
-
|
|
27744
|
+
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
27745
|
const checkoutCode = [];
|
|
27294
27746
|
const payButtons = [];
|
|
27295
27747
|
const checkoutSessions = [];
|
|
@@ -27319,8 +27771,9 @@ function detectProject(root, processEnv = {}) {
|
|
|
27319
27771
|
checkoutSessions.push({ file: rel2, line: line2, ...parsed.value ? { params: redactSecretsDeep(parsed.value) } : {}, unresolved: parsed.unresolved });
|
|
27320
27772
|
}
|
|
27321
27773
|
});
|
|
27774
|
+
if (billingPlatform === "custom" && checkoutCode.some((c) => c.kind.startsWith("stripe_"))) billingPlatform = "stripe";
|
|
27322
27775
|
const site = detectSiteUrl(root, env2, pkg);
|
|
27323
|
-
const stripeKey = findStripeKey(env2, processEnv);
|
|
27776
|
+
const stripeKey = findStripeKey(env2, processEnv, envFile);
|
|
27324
27777
|
const brand = detectBrand(root, layoutFile, typeof pkg?.name === "string" ? pkg.name : void 0);
|
|
27325
27778
|
return {
|
|
27326
27779
|
root,
|
|
@@ -27341,16 +27794,18 @@ function detectProject(root, processEnv = {}) {
|
|
|
27341
27794
|
...stripeKey.refused ? { stripeKeyRefused: stripeKey.refused } : {},
|
|
27342
27795
|
...brand ? { brand } : {},
|
|
27343
27796
|
plans: plansFromSessions(checkoutSessions),
|
|
27344
|
-
env: env2
|
|
27797
|
+
env: env2,
|
|
27798
|
+
mailer: detectMailer(deps, framework, !!pkg)
|
|
27345
27799
|
};
|
|
27346
27800
|
}
|
|
27347
|
-
var GENERIC_NAMES, IGNORE_DIRS, CODE_EXT, MAX_FILES, MAX_FILE_BYTES, first, SITE_URL_KEYS, POLICY_ROUTES, CHECKOUT_PATTERNS, PAY_BUTTON;
|
|
27801
|
+
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
27802
|
var init_detect = __esm({
|
|
27349
27803
|
"src/commands/init/detect.ts"() {
|
|
27350
27804
|
"use strict";
|
|
27351
27805
|
init_src2();
|
|
27352
27806
|
init_literal();
|
|
27353
27807
|
GENERIC_NAMES = /^(?:web|app|site|website|frontend|client|my-app|my-site|demo|example|store|shop|starter|template|project|monorepo|root)$/i;
|
|
27808
|
+
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
27809
|
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
27810
|
CODE_EXT = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|liquid|html|ejs|hbs|erb)$/i;
|
|
27356
27811
|
MAX_FILES = 4e3;
|
|
@@ -27366,6 +27821,7 @@ var init_detect = __esm({
|
|
|
27366
27821
|
CHECKOUT_PATTERNS = [
|
|
27367
27822
|
{ kind: "stripe_checkout_session", re: /checkout\.sessions\.create\s*\(/ },
|
|
27368
27823
|
{ kind: "stripe_payment_link", re: /paymentLinks\.create\s*\(|https:\/\/buy\.stripe\.com\// },
|
|
27824
|
+
{ kind: "stripe_checkout_link", re: /https:\/\/checkout\.stripe\.com\// },
|
|
27369
27825
|
{ kind: "stripe_pricing_table", re: /<stripe-pricing-table\b/ },
|
|
27370
27826
|
{ kind: "stripe_subscription_api", re: /subscriptions\.create\s*\(/ },
|
|
27371
27827
|
{ kind: "stripe_elements", re: /<(PaymentElement|CardElement|ExpressCheckoutElement)\b|elements\.create\(\s*['"]payment['"]/ },
|
|
@@ -27380,6 +27836,47 @@ var init_detect = __esm({
|
|
|
27380
27836
|
// src/ci/scaffold.ts
|
|
27381
27837
|
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
|
|
27382
27838
|
import { basename, dirname as dirname4, join as join5, relative as relative2 } from "node:path";
|
|
27839
|
+
function countTodoValues(v) {
|
|
27840
|
+
if (typeof v === "string") return /^TODO\b/.test(v) ? 1 : 0;
|
|
27841
|
+
if (Array.isArray(v)) return v.reduce((n, x) => n + countTodoValues(x), 0);
|
|
27842
|
+
if (v && typeof v === "object") return Object.values(v).reduce((n, x) => n + countTodoValues(x), 0);
|
|
27843
|
+
return 0;
|
|
27844
|
+
}
|
|
27845
|
+
function mergeTagsIn(text) {
|
|
27846
|
+
const out = [];
|
|
27847
|
+
for (const m of text.matchAll(/\{\{\{?\s*([A-Za-z_][\w.-]*)\s*\}?\}\}|\*\|([A-Za-z_][\w.-]*)\|\*/g)) {
|
|
27848
|
+
const t = m[1] ?? m[2];
|
|
27849
|
+
if (!out.includes(t)) out.push(t);
|
|
27850
|
+
}
|
|
27851
|
+
return out;
|
|
27852
|
+
}
|
|
27853
|
+
function mergeTagField(tag) {
|
|
27854
|
+
const t = tag.toLowerCase().replace(/[.-]/g, "_").replace(/^(?:subscription|sub)_/, "");
|
|
27855
|
+
if (/^(?:plan_)?(?:price|amount|cost|recurring_price|price_formatted|formatted_price|renewal_price|renewal_amount)$/.test(t)) return "plan.price";
|
|
27856
|
+
if (/^(?:plan_)?(?:interval|billing_interval|period|billing_period|frequency|billing_cycle|cycle|interval_name)$/.test(t)) return "plan.interval";
|
|
27857
|
+
if (/^(?:plan|plan_name|product|product_name|plan_title|tier)$/.test(t)) return "plan.name";
|
|
27858
|
+
if (/^(?:plan_)?(?:trial_days|trial_length|trial_period_days)$/.test(t)) return "plan.trial_days";
|
|
27859
|
+
if (/^(?:cancel_url|cancellation_url|manage_url|billing_url|account_url|cancel_link)$/.test(t)) return "merchant.cancel_url";
|
|
27860
|
+
if (/^(?:company|company_name|merchant|merchant_name|brand|brand_name|business_name|app_name)$/.test(t)) return "merchant.name";
|
|
27861
|
+
if (/^(?:support_email|support_contact|support|help_email|contact_email)$/.test(t)) return "merchant.support_contact";
|
|
27862
|
+
return void 0;
|
|
27863
|
+
}
|
|
27864
|
+
function pageSources(root) {
|
|
27865
|
+
const out = [];
|
|
27866
|
+
walk3(root, (rel2) => {
|
|
27867
|
+
if (out.length >= 4) return;
|
|
27868
|
+
let m = /^(?:src\/)?app\/((?:\([^/]+\)\/)*)([^/()]+)\/page\.(?:tsx|jsx|ts|js|mdx)$/.exec(rel2);
|
|
27869
|
+
if (m && PAGE_ROUTE.test(m[2])) {
|
|
27870
|
+
out.push({ source: rel2, file: `.next/server/app/${m[2]}.html`, route: m[2] });
|
|
27871
|
+
return;
|
|
27872
|
+
}
|
|
27873
|
+
m = /^(?:src\/)?pages\/([^/]+?)(?:\/index)?\.(tsx|jsx|ts|js|astro)$/.exec(rel2);
|
|
27874
|
+
if (m && PAGE_ROUTE.test(m[1])) {
|
|
27875
|
+
out.push({ source: rel2, file: m[2] === "astro" ? `dist/${m[1]}/index.html` : `.next/server/pages/${m[1]}.html`, route: m[1] });
|
|
27876
|
+
}
|
|
27877
|
+
});
|
|
27878
|
+
return out;
|
|
27879
|
+
}
|
|
27383
27880
|
function walk3(root, visit) {
|
|
27384
27881
|
let n = 0;
|
|
27385
27882
|
const go = (dir, depth) => {
|
|
@@ -27420,8 +27917,8 @@ function plansFromLiterals(text, file) {
|
|
|
27420
27917
|
if (!interval) continue;
|
|
27421
27918
|
const price3 = /\bprice\s*:\s*(\d+(?:\.\d{1,2})?)\b/.exec(body)?.[1];
|
|
27422
27919
|
const minor = /\b(?:amount|unit_amount|price_cents|cents)\s*:\s*(\d+)\b/.exec(body)?.[1];
|
|
27423
|
-
const
|
|
27424
|
-
if (
|
|
27920
|
+
const major2 = price3 !== void 0 ? Number(price3) : minor !== void 0 ? Number(minor) / 100 : void 0;
|
|
27921
|
+
if (major2 === void 0 || !(major2 > 0)) continue;
|
|
27425
27922
|
const name = /\b(?:name|title|label)\s*:\s*['"]([^'"]{1,80})['"]/.exec(body)?.[1] ?? m[1]?.trim();
|
|
27426
27923
|
if (!name) continue;
|
|
27427
27924
|
const currency = /\bcurrency\s*:\s*['"]([a-z]{3})['"]/i.exec(body)?.[1]?.toLowerCase() ?? "usd";
|
|
@@ -27429,7 +27926,8 @@ function plansFromLiterals(text, file) {
|
|
|
27429
27926
|
const trial = /\btrial(?:_period)?_?days\s*:\s*(\d+)/i.exec(body)?.[1];
|
|
27430
27927
|
if (out.some((p) => p.name === name)) continue;
|
|
27431
27928
|
const key = m[1]?.trim() && /^[A-Za-z_$][\w$]*$/.test(m[1].trim()) ? m[1].trim() : void 0;
|
|
27432
|
-
|
|
27929
|
+
const ids = [...new Set([...body.matchAll(/['"`](price_[A-Za-z0-9_]+)['"`]/g)].map((x) => x[1]))];
|
|
27930
|
+
out.push({ name, ...key ? { key } : {}, price: major2, 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
27931
|
}
|
|
27434
27932
|
return out;
|
|
27435
27933
|
}
|
|
@@ -27480,10 +27978,84 @@ function pageSource(root, built) {
|
|
|
27480
27978
|
const hits = bases.flatMap((b) => [".tsx", ".jsx", ".ts", ".js", ".svelte", ".vue", ".astro", ".mdx"].map((e) => `${b}${e}`)).filter((f) => existsSync5(join5(root, f)));
|
|
27481
27979
|
return hits.length === 1 ? hits[0] : void 0;
|
|
27482
27980
|
}
|
|
27981
|
+
function priceExpressions(root, file, line2) {
|
|
27982
|
+
let text;
|
|
27983
|
+
try {
|
|
27984
|
+
text = readFileSync6(join5(root, file), "utf8");
|
|
27985
|
+
} catch {
|
|
27986
|
+
return [];
|
|
27987
|
+
}
|
|
27988
|
+
const from = text.split("\n").slice(line2 - 1).join("\n");
|
|
27989
|
+
const at = from.search(/checkout\.sessions\.create|paymentLinks\.create/);
|
|
27990
|
+
const open2 = at < 0 ? -1 : from.indexOf("(", at);
|
|
27991
|
+
if (open2 < 0) return [];
|
|
27992
|
+
let depth = 0;
|
|
27993
|
+
let end = from.length;
|
|
27994
|
+
for (let i = open2; i < from.length; i++) {
|
|
27995
|
+
const c = from[i];
|
|
27996
|
+
if (c === "(" || c === "{" || c === "[") depth++;
|
|
27997
|
+
else if (c === ")" || c === "}" || c === "]") {
|
|
27998
|
+
depth--;
|
|
27999
|
+
if (depth === 0) {
|
|
28000
|
+
end = i;
|
|
28001
|
+
break;
|
|
28002
|
+
}
|
|
28003
|
+
}
|
|
28004
|
+
}
|
|
28005
|
+
const call = from.slice(open2, end);
|
|
28006
|
+
const li = call.indexOf("line_items");
|
|
28007
|
+
if (li < 0) return [];
|
|
28008
|
+
const items = call.slice(li);
|
|
28009
|
+
const out = [];
|
|
28010
|
+
for (const m of items.matchAll(/\bprice\s*:\s*/g)) {
|
|
28011
|
+
let d = 0;
|
|
28012
|
+
let j = m.index + m[0].length;
|
|
28013
|
+
const start = j;
|
|
28014
|
+
for (; j < items.length; j++) {
|
|
28015
|
+
const c = items[j];
|
|
28016
|
+
if (c === "(" || c === "{" || c === "[") d++;
|
|
28017
|
+
else if (c === ")" || c === "}" || c === "]") {
|
|
28018
|
+
if (d === 0) break;
|
|
28019
|
+
d--;
|
|
28020
|
+
} else if ((c === "," || c === "\n") && d === 0) break;
|
|
28021
|
+
}
|
|
28022
|
+
const expr = items.slice(start, j).trim();
|
|
28023
|
+
if (expr) out.push(expr);
|
|
28024
|
+
if (out.length >= 5) break;
|
|
28025
|
+
}
|
|
28026
|
+
return out;
|
|
28027
|
+
}
|
|
28028
|
+
function pairCall(root, s2, plans, priceIds, calls) {
|
|
28029
|
+
if (plans.length === 1) return { plan: String(plans[0].name) };
|
|
28030
|
+
if (!plans.length || plans.some((p) => String(p.name).startsWith("TODO"))) return {};
|
|
28031
|
+
const sold = s2.params ? plansSoldBy(s2.params, plans) : [];
|
|
28032
|
+
if (sold.length === 1) return { plan: String(sold[0].name) };
|
|
28033
|
+
const items = Array.isArray(s2.params?.line_items) ? s2.params.line_items : [];
|
|
28034
|
+
const literalIds = items.map((li) => li && typeof li.price === "string" ? li.price : void 0).filter((x) => !!x);
|
|
28035
|
+
const exprs = priceExpressions(root, s2.file, s2.line).filter((e) => !/^['"`][^'"`]*['"`]$/.test(e));
|
|
28036
|
+
for (const id of literalIds) {
|
|
28037
|
+
const owner = plans.filter((p) => priceIds.get(String(p.name))?.includes(id));
|
|
28038
|
+
if (owner.length === 1) return { plan: String(owner[0].name), why: `${id} is the Price id of that plan in the plans module` };
|
|
28039
|
+
}
|
|
28040
|
+
const runtime = exprs.find((e) => /\?|\|\||&&|\[[^\]'"`]*\]|\(/.test(e));
|
|
28041
|
+
if (runtime && !literalIds.length) return calls === 1 ? { several: true } : { ref: runtime };
|
|
28042
|
+
for (const ref of [...literalIds, ...exprs]) {
|
|
28043
|
+
const hint = intervalHintOf(ref);
|
|
28044
|
+
if (!hint) continue;
|
|
28045
|
+
const match = plans.filter((p) => {
|
|
28046
|
+
const count = Number(p.interval_count ?? 1) || 1;
|
|
28047
|
+
const unit2 = p.interval === "month" && count === 12 ? "year" : count === 1 ? p.interval : void 0;
|
|
28048
|
+
return unit2 === hint;
|
|
28049
|
+
});
|
|
28050
|
+
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` };
|
|
28051
|
+
}
|
|
28052
|
+
return { ref: literalIds[0] ?? exprs[0] };
|
|
28053
|
+
}
|
|
27483
28054
|
function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
27484
28055
|
const det = detectProject(root, env2);
|
|
27485
28056
|
const todos = [];
|
|
27486
28057
|
const found = [];
|
|
28058
|
+
const notes2 = [];
|
|
27487
28059
|
const names = /* @__PURE__ */ new Set();
|
|
27488
28060
|
const merchant = {};
|
|
27489
28061
|
const fact = (k) => typeof facts[k] === "string" && facts[k].trim() ? facts[k].trim() : void 0;
|
|
@@ -27511,8 +28083,21 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27511
28083
|
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
28084
|
return m ? String(m.name) : void 0;
|
|
27513
28085
|
};
|
|
28086
|
+
const literals = [];
|
|
28087
|
+
walk3(root, (rel2) => {
|
|
28088
|
+
if (!PLAN_FILE.test(rel2) || literals.length >= 8) return;
|
|
28089
|
+
let text = "";
|
|
28090
|
+
try {
|
|
28091
|
+
text = readFileSync6(join5(root, rel2), "utf8");
|
|
28092
|
+
} catch {
|
|
28093
|
+
return;
|
|
28094
|
+
}
|
|
28095
|
+
for (const p of plansFromLiterals(text, rel2)) if (!literals.some((x) => x.name === p.name)) literals.push(p);
|
|
28096
|
+
});
|
|
28097
|
+
const minorOf = (p) => typeof p.amount === "number" ? p.amount : typeof p.price === "number" ? Math.round(p.price * 100) : NaN;
|
|
28098
|
+
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
28099
|
const plans = det.plans.map((p, i) => ({
|
|
27515
|
-
name: p.name ?? initName(p) ?? `Plan ${i + 1}`,
|
|
28100
|
+
name: p.name ?? initName(p) ?? literalFor(p)?.name ?? `Plan ${i + 1}`,
|
|
27516
28101
|
amount: p.amount,
|
|
27517
28102
|
currency: p.currency,
|
|
27518
28103
|
interval: p.interval,
|
|
@@ -27520,31 +28105,28 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27520
28105
|
...p.trial_days ? { trial_days: p.trial_days } : {}
|
|
27521
28106
|
}));
|
|
27522
28107
|
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
28108
|
if (!plans.length && Array.isArray(facts.plans)) {
|
|
27525
28109
|
for (const [i, p] of facts.plans.entries()) {
|
|
27526
28110
|
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 } : {} });
|
|
28111
|
+
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
28112
|
found.push(`plan ${String(plans[plans.length - 1].name)} (from checkbox init --plan)`);
|
|
27529
28113
|
}
|
|
27530
28114
|
}
|
|
27531
28115
|
if (!plans.length) {
|
|
27532
|
-
|
|
27533
|
-
|
|
27534
|
-
|
|
27535
|
-
|
|
27536
|
-
|
|
27537
|
-
|
|
27538
|
-
|
|
27539
|
-
|
|
27540
|
-
|
|
27541
|
-
|
|
27542
|
-
|
|
27543
|
-
|
|
27544
|
-
|
|
27545
|
-
|
|
27546
|
-
}
|
|
27547
|
-
});
|
|
28116
|
+
for (const p of literals) {
|
|
28117
|
+
const { source, key: _key, price_ids: _ids, ...plan2 } = p;
|
|
28118
|
+
plans.push(plan2);
|
|
28119
|
+
found.push(`plan ${p.name} ${p.price} ${p.currency}/${p.interval} (${source})`);
|
|
28120
|
+
}
|
|
28121
|
+
}
|
|
28122
|
+
const planKeys = /* @__PURE__ */ new Map();
|
|
28123
|
+
const planPriceIds = /* @__PURE__ */ new Map();
|
|
28124
|
+
for (const p of plans) {
|
|
28125
|
+
const lit = literals.find((x) => x.name === p.name) ?? literalFor(p);
|
|
28126
|
+
if (lit?.key) planKeys.set(String(p.name), lit.key);
|
|
28127
|
+
if (lit?.price_ids?.length) planPriceIds.set(String(p.name), lit.price_ids);
|
|
28128
|
+
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));
|
|
28129
|
+
if (known) planPriceIds.set(String(p.name), [...planPriceIds.get(String(p.name)) ?? [], ...known.stripe_price_ids]);
|
|
27548
28130
|
}
|
|
27549
28131
|
if (!plans.length) {
|
|
27550
28132
|
plans.push({ name: "TODO: plan name", price: "TODO: e.g. 19.99", currency: "usd", interval: "month" });
|
|
@@ -27588,11 +28170,20 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27588
28170
|
} else if (s2.unresolved.length) found.push(`${s2.file}:${s2.line}: Checkout Session (${s2.unresolved.length} value(s) are expressions, read as-is)`);
|
|
27589
28171
|
else found.push(`${s2.file}:${s2.line}: Checkout Session`);
|
|
27590
28172
|
entry.strict = true;
|
|
27591
|
-
const
|
|
27592
|
-
if (
|
|
27593
|
-
|
|
28173
|
+
const pair = pairCall(root, s2, plans, planPriceIds, det.checkoutSessions.length);
|
|
28174
|
+
if (pair.plan) {
|
|
28175
|
+
if (plans.length > 1) {
|
|
28176
|
+
names.delete(String(entry.name));
|
|
28177
|
+
entry.name = unique(names, `${String(entry.name).replace(/\)$/, "")}: ${pair.plan})`);
|
|
28178
|
+
}
|
|
28179
|
+
stripe.push({ ...entry, plan: pair.plan });
|
|
28180
|
+
if (pair.why) found.push(`${s2.file}:${s2.line}: sells plan "${pair.plan}" (${pair.why})`);
|
|
28181
|
+
} else if (pair.several) {
|
|
27594
28182
|
for (const p of plans) stripe.push({ ...entry, name: unique(names, `${String(entry.name).replace(/\)$/, "")}: ${String(p.name)})`), plan: String(p.name) });
|
|
27595
28183
|
names.delete(String(entry.name));
|
|
28184
|
+
} else if (plans.length > 1 && !plans.some((p) => String(p.name).startsWith("TODO"))) {
|
|
28185
|
+
stripe.push({ ...entry, plan: `TODO: which plan does this call sell? one of ${oneOf}` });
|
|
28186
|
+
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
28187
|
} else stripe.push(entry);
|
|
27597
28188
|
}
|
|
27598
28189
|
const emails = [];
|
|
@@ -27602,12 +28193,22 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27602
28193
|
if (IN_EMAIL_DIR.test(rel2)) {
|
|
27603
28194
|
const kind = emailKindOf(rel2);
|
|
27604
28195
|
if (/\.(?:html?|txt|md)$/i.test(rel2) && kind) {
|
|
27605
|
-
|
|
27606
|
-
|
|
28196
|
+
let text = "";
|
|
28197
|
+
try {
|
|
28198
|
+
text = readFileSync6(join5(root, rel2), "utf8");
|
|
28199
|
+
} catch {
|
|
28200
|
+
}
|
|
28201
|
+
const tags = Object.fromEntries(mergeTagsIn(text).flatMap((t) => mergeTagField(t) ? [[t, mergeTagField(t)]] : []));
|
|
28202
|
+
const planTagged = Object.values(tags).some((v) => v.startsWith("plan."));
|
|
28203
|
+
const hint = intervalHintOf(basename(rel2).replace(/\.[^.]+$/, ""));
|
|
28204
|
+
const byName = hint && plans.length > 1 ? plans.filter((p) => p.interval === hint && (Number(p.interval_count ?? 1) || 1) === 1) : [];
|
|
28205
|
+
const plan2 = plans.length > 1 && planTagged ? { plan: "all" } : byName.length === 1 ? { plan: String(byName[0].name) } : planFor();
|
|
28206
|
+
emails.push({ name: unique(names, `Email: ${basename(rel2)}`), kind, file: rel2, ...plan2, ...Object.keys(tags).length ? { merge_tags: tags } : {} });
|
|
28207
|
+
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
28208
|
return;
|
|
27608
28209
|
}
|
|
27609
28210
|
if (/\.[jt]sx$|\.mjml$/i.test(rel2) && kind) {
|
|
27610
|
-
|
|
28211
|
+
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
28212
|
return;
|
|
27612
28213
|
}
|
|
27613
28214
|
}
|
|
@@ -27631,8 +28232,22 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27631
28232
|
}
|
|
27632
28233
|
}
|
|
27633
28234
|
});
|
|
27634
|
-
|
|
27635
|
-
|
|
28235
|
+
for (const ps of pageSources(root)) {
|
|
28236
|
+
if (pages.some((p) => p.source === ps.source || p.file === ps.file)) continue;
|
|
28237
|
+
let text = "";
|
|
28238
|
+
try {
|
|
28239
|
+
text = readFileSync6(join5(root, ps.source), "utf8");
|
|
28240
|
+
} catch {
|
|
28241
|
+
}
|
|
28242
|
+
const kind = /checkout|subscribe/i.test(ps.route) ? "checkout" : "pricing";
|
|
28243
|
+
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}`);
|
|
28244
|
+
const showsAll = plans.length > 1 && plans.every(shows);
|
|
28245
|
+
pages.push({ name: unique(names, `Page: ${ps.route}`), file: ps.file, source: ps.source, kind, ...showsAll ? { plan: "all" } : planFor("page") });
|
|
28246
|
+
found.push(`${ps.source}: ${kind} page (checked as the built ${ps.file}; the workflow builds it first)`);
|
|
28247
|
+
}
|
|
28248
|
+
const inPages = (f) => pages.some((p) => p.source === f);
|
|
28249
|
+
if (!disclosures.length && det.payButtons.length && !det.payButtons.some((b) => inPages(b.file))) {
|
|
28250
|
+
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
28251
|
}
|
|
27637
28252
|
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
28253
|
const checks = {};
|
|
@@ -27642,20 +28257,24 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27642
28257
|
if (emails.length) checks.emails = emails;
|
|
27643
28258
|
checks.notice_schedule = true;
|
|
27644
28259
|
if (!stripe.length && !disclosures.length && !pages.length && !emails.length) {
|
|
27645
|
-
|
|
28260
|
+
notes2.push('No checkout code, email templates or pages were found: add entries under "checks" (https://checkbox.site/docs/guides/ci#config)');
|
|
27646
28261
|
}
|
|
27647
28262
|
return {
|
|
27648
28263
|
config: { $schema: SCHEMA_URL, merchant, jurisdictions: "all", plans, checks },
|
|
27649
28264
|
todos,
|
|
27650
|
-
found
|
|
28265
|
+
found,
|
|
28266
|
+
notes: notes2,
|
|
28267
|
+
todoValues: countTodoValues({ merchant, plans, checks })
|
|
27651
28268
|
};
|
|
27652
28269
|
}
|
|
27653
|
-
var IGNORE, MAX_FILES2, EMAIL_NAMES, IN_EMAIL_DIR, PAGE_FILE, DISCLOSURE_TEXT, unique, PLAN_FILE;
|
|
28270
|
+
var PAGE_ROUTE, IGNORE, MAX_FILES2, EMAIL_NAMES, IN_EMAIL_DIR, PAGE_FILE, DISCLOSURE_TEXT, unique, PLAN_FILE;
|
|
27654
28271
|
var init_scaffold = __esm({
|
|
27655
28272
|
"src/ci/scaffold.ts"() {
|
|
27656
28273
|
"use strict";
|
|
27657
28274
|
init_detect();
|
|
28275
|
+
init_preflight();
|
|
27658
28276
|
init_config3();
|
|
28277
|
+
PAGE_ROUTE = /^(pricing|checkout|subscribe|plans)$/i;
|
|
27659
28278
|
IGNORE = /* @__PURE__ */ new Set(["node_modules", ".git", ".next", ".nuxt", ".svelte-kit", ".output", ".vercel", ".netlify", ".turbo", ".cache", "coverage", ".astro", "vendor", ".venv", "__pycache__", ".github"]);
|
|
27660
28279
|
MAX_FILES2 = 5e3;
|
|
27661
28280
|
EMAIL_NAMES = [
|
|
@@ -27846,8 +28465,8 @@ function loaderFlags(file, dir, nodeVersion = process.versions.node, features =
|
|
|
27846
28465
|
if (hasTsx(dir)) return ["--import", "tsx"];
|
|
27847
28466
|
if (/\.[jt]sx$/.test(file)) return declaresTsx(dir) ? `${file}: ${NOT_INSTALLED}` : `${file}: JSX modules need tsx (npm install -D tsx), or export the rendered { subject, html } from a .js / .ts file`;
|
|
27848
28467
|
if (features.typescript) return [];
|
|
27849
|
-
const [
|
|
27850
|
-
if (
|
|
28468
|
+
const [major2, minor] = nodeVersion.split(".").map(Number);
|
|
28469
|
+
if (major2 > 22 || major2 === 22 && minor >= 6) return ["--experimental-strip-types"];
|
|
27851
28470
|
return declaresTsx(dir) ? `${file}: ${NOT_INSTALLED}` : `${file}: TypeScript modules need Node 22.6+ (type stripping) or tsx in the project (npm install -D tsx); or point "file" at a JSON export`;
|
|
27852
28471
|
}
|
|
27853
28472
|
function runModule(dir, ref, args = [], env2 = process.env, timeoutMs = 3e4) {
|
|
@@ -27917,7 +28536,23 @@ function paramsFromSource(text, file, call = 1, env2 = {}, consts = {}) {
|
|
|
27917
28536
|
if (!lit) throw new InputError(`${file}:${line2}: the params are not an object literal; use "module" (a function returning the params) or "file" instead`);
|
|
27918
28537
|
const parsed = parseObjectLiteral(lit, env2, consts);
|
|
27919
28538
|
if (!parsed.value) throw new InputError(`${file}:${line2}: could not read the params object`);
|
|
27920
|
-
|
|
28539
|
+
const unresolved = [...parsed.unresolved, ...spreadUnread(parsed.value, parsed.spreads ?? [], consts)];
|
|
28540
|
+
return { params: parsed.value, line: line2, unresolved, ...parsed.fromEnv ? { fromEnv: parsed.fromEnv } : {} };
|
|
28541
|
+
}
|
|
28542
|
+
function spreadUnread(params, spreads, consts = {}) {
|
|
28543
|
+
const out = [];
|
|
28544
|
+
for (const expr of spreads) {
|
|
28545
|
+
const name = expr.replace(/\s+/g, "").replace(/\?\./g, ".");
|
|
28546
|
+
const seen = Object.prototype.hasOwnProperty.call(consts, name) ? consts[name] : expr;
|
|
28547
|
+
const visible = /[{]/.test(seen);
|
|
28548
|
+
for (const k of SPREAD_VERDICT_KEYS) {
|
|
28549
|
+
if (params[k] !== void 0) continue;
|
|
28550
|
+
if (visible && !new RegExp(`\\b${k}\\b`).test(seen)) continue;
|
|
28551
|
+
const entry = `${k} (may come from ...${expr.length > 40 ? `${expr.slice(0, 37)}\u2026` : expr})`;
|
|
28552
|
+
if (!out.includes(entry)) out.push(entry);
|
|
28553
|
+
}
|
|
28554
|
+
}
|
|
28555
|
+
return out;
|
|
27921
28556
|
}
|
|
27922
28557
|
function lineOfAny(text, needles) {
|
|
27923
28558
|
const lower2 = text.toLowerCase();
|
|
@@ -28028,7 +28663,8 @@ async function stripeJob(loaded, e, o, root) {
|
|
|
28028
28663
|
if (r.unresolved.length) {
|
|
28029
28664
|
job.partial = true;
|
|
28030
28665
|
job.unresolved = r.unresolved;
|
|
28031
|
-
|
|
28666
|
+
const listed = r.unresolved.some((p) => / \(may come from \.\.\./.test(p)) ? r.unresolved.filter((p) => p !== "...") : r.unresolved;
|
|
28667
|
+
job.warnings.push(`Read statically; ${listed.length} value(s) are expressions Checkbox cannot evaluate (${listed.slice(0, 4).join(", ")}${listed.length > 4 ? ", \u2026" : ""}). Give the price in "plans" (and "plan" here), or use "module" to return the exact params.`);
|
|
28032
28668
|
}
|
|
28033
28669
|
}
|
|
28034
28670
|
if (!params || typeof params !== "object" || Array.isArray(params)) throw new InputError("the Checkout Session params must be a JSON object");
|
|
@@ -28161,6 +28797,53 @@ function pageJob(loaded, e, o, root) {
|
|
|
28161
28797
|
}
|
|
28162
28798
|
return job;
|
|
28163
28799
|
}
|
|
28800
|
+
function planMoney(plan2) {
|
|
28801
|
+
if (!plan2) return void 0;
|
|
28802
|
+
const cur = (plan2.currency ?? "usd").toUpperCase();
|
|
28803
|
+
const major2 = 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;
|
|
28804
|
+
if (major2 === void 0) return void 0;
|
|
28805
|
+
try {
|
|
28806
|
+
return new Intl.NumberFormat("en-US", { style: "currency", currency: cur }).format(major2);
|
|
28807
|
+
} catch {
|
|
28808
|
+
return `${major2.toFixed(2)} ${cur}`;
|
|
28809
|
+
}
|
|
28810
|
+
}
|
|
28811
|
+
function fillMergeTags(msg2, tags, config, plan2) {
|
|
28812
|
+
const field = (v) => {
|
|
28813
|
+
if (!MERGE_TAG_FIELDS.includes(v)) return v;
|
|
28814
|
+
switch (v) {
|
|
28815
|
+
case "plan.price":
|
|
28816
|
+
case "plan.amount":
|
|
28817
|
+
return planMoney(plan2);
|
|
28818
|
+
case "plan.interval":
|
|
28819
|
+
return plan2 ? (plan2.interval_count ?? 1) > 1 ? `${plan2.interval_count} ${plan2.interval}s` : plan2.interval : void 0;
|
|
28820
|
+
case "plan.name":
|
|
28821
|
+
return plan2?.name;
|
|
28822
|
+
case "plan.trial_days":
|
|
28823
|
+
return plan2?.trial_days !== void 0 ? String(plan2.trial_days) : void 0;
|
|
28824
|
+
case "plan.currency":
|
|
28825
|
+
return (plan2?.currency ?? "usd").toUpperCase();
|
|
28826
|
+
case "merchant.name":
|
|
28827
|
+
return config.merchant?.name;
|
|
28828
|
+
case "merchant.cancel_url":
|
|
28829
|
+
return config.merchant?.cancel_url;
|
|
28830
|
+
case "merchant.support_contact":
|
|
28831
|
+
return config.merchant?.support_contact;
|
|
28832
|
+
}
|
|
28833
|
+
return void 0;
|
|
28834
|
+
};
|
|
28835
|
+
const values = /* @__PURE__ */ new Map();
|
|
28836
|
+
for (const [k, v] of Object.entries(tags)) {
|
|
28837
|
+
const val = field(v);
|
|
28838
|
+
if (val !== void 0) values.set(k.toLowerCase(), val);
|
|
28839
|
+
}
|
|
28840
|
+
const esc3 = (t) => t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
28841
|
+
const fill2 = (t, html) => t?.replace(/\{\{\{\s*([\w.-]+)\s*\}\}\}|\{\{\s*([\w.-]+)\s*\}\}|\*\|([\w.-]+)\|\*/g, (m, a, b, c) => {
|
|
28842
|
+
const v = values.get(String(a ?? b ?? c).toLowerCase());
|
|
28843
|
+
return v === void 0 ? m : html ? esc3(v) : v;
|
|
28844
|
+
});
|
|
28845
|
+
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) } : {} };
|
|
28846
|
+
}
|
|
28164
28847
|
async function emailJob(loaded, e, o, root, planName = e.plan, name = e.name) {
|
|
28165
28848
|
const { config, dir } = loaded;
|
|
28166
28849
|
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 +28864,8 @@ async function emailJob(loaded, e, o, root, planName = e.plan, name = e.name) {
|
|
|
28181
28864
|
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
28865
|
} else throw new InputError(`${e.module} must return { subject, text | html } or a string`);
|
|
28183
28866
|
} else msg2 = { subject: e.subject, ...e.text ? { text: e.text } : {}, ...e.html ? { html: e.html } : {} };
|
|
28867
|
+
const tags = { ...config.merge_tags ?? {}, ...e.merge_tags ?? {} };
|
|
28868
|
+
if (Object.keys(tags).length) msg2 = fillMergeTags(msg2, tags, config, pickPlan(config, planName));
|
|
28184
28869
|
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
28870
|
if (!msg2.text && !msg2.html) throw new InputError("the email body is empty");
|
|
28186
28871
|
const terms2 = { ...planTerms(config, pickPlan(config, planName)) };
|
|
@@ -28246,7 +28931,7 @@ async function planJobs(loaded, o, root) {
|
|
|
28246
28931
|
}
|
|
28247
28932
|
return jobs;
|
|
28248
28933
|
}
|
|
28249
|
-
var InputError, NOT_INSTALLED, CHILD, jurisdictionsOf;
|
|
28934
|
+
var InputError, NOT_INSTALLED, CHILD, SPREAD_VERDICT_KEYS, jurisdictionsOf;
|
|
28250
28935
|
var init_inputs = __esm({
|
|
28251
28936
|
"src/ci/inputs.ts"() {
|
|
28252
28937
|
"use strict";
|
|
@@ -28274,6 +28959,7 @@ if (typeof v === 'function') v = await v(...JSON.parse(argsJson));
|
|
|
28274
28959
|
process.stdout.write('\\n__CHECKBOX_JSON__' + JSON.stringify(v === undefined ? null : v) + '\\n');
|
|
28275
28960
|
process.exit(0);
|
|
28276
28961
|
`;
|
|
28962
|
+
SPREAD_VERDICT_KEYS = ["mode", "consent_collection", "custom_text", "subscription_data", "discounts"];
|
|
28277
28963
|
jurisdictionsOf = (config) => config.jurisdictions ? { jurisdictions: config.jurisdictions } : {};
|
|
28278
28964
|
}
|
|
28279
28965
|
});
|
|
@@ -28606,6 +29292,7 @@ var init_timing = __esm({
|
|
|
28606
29292
|
var notice_schedule_exports = {};
|
|
28607
29293
|
__export(notice_schedule_exports, {
|
|
28608
29294
|
computeNoticeSchedule: () => computeNoticeSchedule,
|
|
29295
|
+
noticesKindsNote: () => noticesKindsNote,
|
|
28609
29296
|
singleDayNote: () => singleDayNote
|
|
28610
29297
|
});
|
|
28611
29298
|
function singleDayNote(requirementIds, days) {
|
|
@@ -28714,7 +29401,12 @@ function computeNoticeSchedule(input, now = /* @__PURE__ */ new Date()) {
|
|
|
28714
29401
|
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
29402
|
else notRequired.push({ kind: "annual_terms", reason: "No applicable requirement demands a yearly terms reminder under these jurisdictions." });
|
|
28716
29403
|
}
|
|
28717
|
-
if (!input.price_change) {
|
|
29404
|
+
if (!input.price_change && plan2.introAmount !== void 0 && plan2.introAmount < plan2.amount) {
|
|
29405
|
+
const stepUp = addInterval(firstCharge, plan2.interval, plan2.intervalCount * Math.max(1, plan2.introPeriods ?? 1));
|
|
29406
|
+
const t = timing("price_change", stepUp);
|
|
29407
|
+
if (t.governed) push("price_change", `Intro price ends: the regular price is first charged`, stepUp, t);
|
|
29408
|
+
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." });
|
|
29409
|
+
} else if (!input.price_change) {
|
|
28718
29410
|
notRequired.push({ kind: "price_change", reason: "No price change planned. Pass price_change (effective_date, new_price) to schedule its advance notice." });
|
|
28719
29411
|
} else {
|
|
28720
29412
|
const eff = parseDate(input.price_change.effective_date, "price_change.effective_date", start);
|
|
@@ -28741,6 +29433,24 @@ function computeNoticeSchedule(input, now = /* @__PURE__ */ new Date()) {
|
|
|
28741
29433
|
}
|
|
28742
29434
|
};
|
|
28743
29435
|
}
|
|
29436
|
+
function noticesKindsNote(kinds, settings) {
|
|
29437
|
+
const scheduled = [...new Set(kinds)];
|
|
29438
|
+
if (!scheduled.length) return null;
|
|
29439
|
+
if (!settings || settings.mode !== "checkbox") {
|
|
29440
|
+
return {
|
|
29441
|
+
code: "notices_you_send",
|
|
29442
|
+
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)} }.`,
|
|
29443
|
+
param: "site"
|
|
29444
|
+
};
|
|
29445
|
+
}
|
|
29446
|
+
const off = scheduled.filter((k) => !settings.enabledKinds.includes(k));
|
|
29447
|
+
if (!off.length) return null;
|
|
29448
|
+
return {
|
|
29449
|
+
code: "notices_kinds_off",
|
|
29450
|
+
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).`,
|
|
29451
|
+
param: "site"
|
|
29452
|
+
};
|
|
29453
|
+
}
|
|
28744
29454
|
var CITATION, BY_ID2, day, unix;
|
|
28745
29455
|
var init_notice_schedule = __esm({
|
|
28746
29456
|
"../../apps/api/src/v1/checks/notice-schedule.ts"() {
|
|
@@ -28781,10 +29491,41 @@ function operationFor2(kind) {
|
|
|
28781
29491
|
function onlineEngine(client) {
|
|
28782
29492
|
return { mode: "online", run: (kind, body) => client.sdk.call(operationFor2(kind), body) };
|
|
28783
29493
|
}
|
|
29494
|
+
function resultOf(status, exit, strict) {
|
|
29495
|
+
if (exit === 0) return status === "needs_review" ? "passed with items to review (they do not fail this run)" : "passed";
|
|
29496
|
+
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})`;
|
|
29497
|
+
if (status === "skipped") return `failed: nothing was checked (exit ${exit})`;
|
|
29498
|
+
if (status === "fail") return `failed: fixes needed (exit ${exit})`;
|
|
29499
|
+
return `failed: could not run every check (exit ${exit})`;
|
|
29500
|
+
}
|
|
28784
29501
|
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).');
|
|
29502
|
+
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).');
|
|
29503
|
+
}
|
|
29504
|
+
function pricingPageFixes(fixes, replacements = []) {
|
|
29505
|
+
return concreteCardFixes(pricingPageFixesRaw(fixes), replacements);
|
|
28786
29506
|
}
|
|
28787
|
-
function
|
|
29507
|
+
function concreteCardFixes(fixes, replacements) {
|
|
29508
|
+
if (!replacements.length) return fixes;
|
|
29509
|
+
const lineFor = (card) => {
|
|
29510
|
+
const name = card.split(", ")[0].trim().toLowerCase();
|
|
29511
|
+
const r = replacements.find((x) => x.plan.replace(/\s*\([^)]*\)\s*$/, "").trim().toLowerCase() === name);
|
|
29512
|
+
return r?.short_disclosure_text || r?.disclosure_text || void 0;
|
|
29513
|
+
};
|
|
29514
|
+
const covered = /* @__PURE__ */ new Set();
|
|
29515
|
+
const out = fixes.map((f) => {
|
|
29516
|
+
const m = /^Plan card “([^”]+)”: /.exec(f);
|
|
29517
|
+
if (!m || !/"<price> every <interval>[^"]*"/.test(f)) return f;
|
|
29518
|
+
const line2 = lineFor(m[1]);
|
|
29519
|
+
if (!line2) return f;
|
|
29520
|
+
if (/cancel anytime online/i.test(line2)) covered.add(m[1]);
|
|
29521
|
+
return f.replace(/"<price> every <interval>[^"]*"/, `"${line2}"`);
|
|
29522
|
+
});
|
|
29523
|
+
return out.filter((f) => {
|
|
29524
|
+
const m = /^Plan card “([^”]+)”: Add "Cancel anytime online" to the renewal line/.exec(f);
|
|
29525
|
+
return !(m && covered.has(m[1]));
|
|
29526
|
+
});
|
|
29527
|
+
}
|
|
29528
|
+
function pricingPageFixesRaw(fixes) {
|
|
28788
29529
|
const reworded = fixes.map(
|
|
28789
29530
|
(f) => f.replace(/,? before the card and email fields and the (?:pay|purchase) button/g, ", next to the plan's button").replace(/,? before the card and email fields/g, ", next to the plan's button")
|
|
28790
29531
|
);
|
|
@@ -28833,8 +29574,10 @@ function fixesOf(r, onlyNeedsFix = false) {
|
|
|
28833
29574
|
const plain = cards.length > 1 ? cardClauses(c.explanation).filter((x) => /look like the surrounding text/.test(x.clause)).map((x) => x.card) : [];
|
|
28834
29575
|
if (plain.length && plain.length < cards.length && /renewal line/i.test(base)) {
|
|
28835
29576
|
cards = cards.filter((x) => !plain.includes(x));
|
|
28836
|
-
const
|
|
28837
|
-
|
|
29577
|
+
for (const x of cardClauses(c.explanation).filter((y) => plain.includes(y.card))) {
|
|
29578
|
+
const standOut = `Plan card \u201C${x.card}\u201D: ${setOffFix(x.clause)}`;
|
|
29579
|
+
if (!out.includes(standOut)) out.push(standOut);
|
|
29580
|
+
}
|
|
28838
29581
|
}
|
|
28839
29582
|
const text = cards.length && base ? `Plan card ${cards.map((x) => `\u201C${x}\u201D`).join(", ")}: ${base}` : base;
|
|
28840
29583
|
if (text && !out.includes(text)) out.push(text);
|
|
@@ -28862,22 +29605,29 @@ function withPlaceholders(params, placeholders) {
|
|
|
28862
29605
|
}
|
|
28863
29606
|
return copy;
|
|
28864
29607
|
}
|
|
28865
|
-
function correctedOf(type, r, partial = false, placeholders) {
|
|
29608
|
+
function correctedOf(type, r, partial = false, placeholders, pricingPage = false) {
|
|
28866
29609
|
if (type === "stripe_checkout" && r.correction?.patch?.length && partial) {
|
|
28867
29610
|
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
29611
|
}
|
|
28869
29612
|
if (type === "stripe_checkout" && r.correction?.patch?.length) {
|
|
28870
29613
|
return { format: "json", title: `Corrected params${r.correction.verification?.passes ? " (verified by the rules engine)" : ""}${placeholders ? "; keep your own values where it says {{\u2026}}" : ""}`, body: JSON.stringify(withPlaceholders(r.correction.corrected_params, placeholders), null, 2) };
|
|
28871
29614
|
}
|
|
28872
|
-
const
|
|
28873
|
-
if (type === "page" &&
|
|
29615
|
+
const allPlans = (r.plan_replacements ?? []).filter((p) => p.disclosure_text);
|
|
29616
|
+
if (type === "page" && allPlans.length >= 2 && ciStatusOf(r) !== "pass") {
|
|
29617
|
+
const named = (r.checks ?? []).filter((c) => (c.status === "fail" || c.status === "needs_review") && !(c.compliance_status && OK_STATUSES.has(c.compliance_status))).flatMap((c) => failingCards(c.explanation)).map((x) => x.split(", ")[0].trim().toLowerCase());
|
|
29618
|
+
const failing = allPlans.filter((p) => named.includes(p.plan.replace(/\s*\([^)]*\)\s*$/, "").trim().toLowerCase()));
|
|
29619
|
+
const perPlan = failing.length ? failing : allPlans;
|
|
28874
29620
|
const all = perPlan.every((p) => p.verified);
|
|
28875
29621
|
return {
|
|
28876
29622
|
format: "text",
|
|
28877
|
-
title: `Suggested replacement per plan, each directly above its own plan's button${all ? " (each passes the checks, verified by the rules engine)" : ""}`,
|
|
28878
|
-
|
|
28879
|
-
|
|
28880
|
-
|
|
29623
|
+
title: perPlan.length === 1 ? `Suggested replacement for ${perPlan[0].plan.replace(/\s*\([^)]*\)\s*$/, "")}, directly above its button${all ? " (passes the checks, verified by the rules engine)" : ""}` : `Suggested replacement per plan, each directly above its own plan's button${all ? " (each passes the checks, verified by the rules engine)" : ""}`,
|
|
29624
|
+
// Round 32 (pilot 31, ci-pr-agent): a pricing page links to the checkout, where consent is asked (Stripe Checkout
|
|
29625
|
+
// shows its own checkbox), so no consent checkbox label is suggested for it.
|
|
29626
|
+
body: `${perPlan.map((p) => `${p.plan}:
|
|
29627
|
+
${p.disclosure_text}${p.consent_label && !pricingPage ? `
|
|
29628
|
+
Consent checkbox label: ${p.consent_label}` : ""}`).join("\n\n")}${perPlan.some((p) => p.html && BOXED_HTML.test(p.html)) ? `
|
|
29629
|
+
|
|
29630
|
+
${BOXED_LINE}` : ""}`
|
|
28881
29631
|
};
|
|
28882
29632
|
}
|
|
28883
29633
|
if ((type === "disclosure" || type === "page") && r.replacement?.disclosure_text && ciStatusOf(r) !== "pass") {
|
|
@@ -28889,11 +29639,14 @@ Consent checkbox label: ${p.consent_label}` : ""}`).join("\n\n")
|
|
|
28889
29639
|
|
|
28890
29640
|
Fuller version: ${r.replacement.disclosure_text}` : r.replacement.disclosure_text}${r.replacement.consent_label ? `
|
|
28891
29641
|
|
|
28892
|
-
Consent checkbox label: ${r.replacement.consent_label}` : ""}`
|
|
29642
|
+
Consent checkbox label: ${r.replacement.consent_label}` : ""}${r.replacement.html && BOXED_HTML.test(r.replacement.html) ? `
|
|
29643
|
+
|
|
29644
|
+
${BOXED_LINE}` : ""}`
|
|
28893
29645
|
};
|
|
28894
29646
|
}
|
|
28895
29647
|
if (type === "email" && r.template && ciStatusOf(r) !== "pass" && r.verdict !== "not_applicable") {
|
|
28896
|
-
|
|
29648
|
+
const toFill = r.template.placeholders ?? [];
|
|
29649
|
+
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
29650
|
|
|
28898
29651
|
${r.template.text}` };
|
|
28899
29652
|
}
|
|
@@ -28937,13 +29690,20 @@ function noticeCoverage(config, planName, schedule, opts = {}) {
|
|
|
28937
29690
|
const required = /* @__PURE__ */ new Map();
|
|
28938
29691
|
for (const n of schedule.notices ?? []) {
|
|
28939
29692
|
const k = NOTICE_TO_EMAIL[n.kind];
|
|
28940
|
-
if (k && !required.has(k)) required.set(k, { occasion: n.occasion, days: n.days_before });
|
|
28941
|
-
}
|
|
29693
|
+
if (k && !required.has(k)) required.set(k, { occasion: n.occasion, days: n.days_before, citations: n.citations ?? [], jurisdictions: n.jurisdictions ?? [] });
|
|
29694
|
+
}
|
|
29695
|
+
const law = (w) => {
|
|
29696
|
+
if (!w.citations.length) return "";
|
|
29697
|
+
const where2 = w.jurisdictions.map((j) => j.replace(/^US-/, "")).filter((j) => j !== "FED");
|
|
29698
|
+
const fed = w.jurisdictions.includes("US-FED");
|
|
29699
|
+
const places = [...fed ? ["federal"] : [], ...where2];
|
|
29700
|
+
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(", ")})` : ""}.`;
|
|
29701
|
+
};
|
|
28942
29702
|
const missing = [...required].filter(([k]) => !have.has(k));
|
|
28943
29703
|
if (!required.size) return { status: "not_applicable", label: "No reminder emails required", fixes: [] };
|
|
28944
29704
|
const sent2 = [...required.keys()].filter((k) => byCheckbox.has(k)).length;
|
|
28945
29705
|
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)" : ""}
|
|
29706
|
+
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
29707
|
return { status: "needs_review", label: `${missing.length} required email${missing.length === 1 ? "" : "s"} not in checkbox.json`, fix: fixes[0], fixes };
|
|
28948
29708
|
}
|
|
28949
29709
|
function unverifiedOffer(r) {
|
|
@@ -28957,6 +29717,11 @@ function unverifiedOffer(r) {
|
|
|
28957
29717
|
if (coupon && open2.length) return coupon.message;
|
|
28958
29718
|
return void 0;
|
|
28959
29719
|
}
|
|
29720
|
+
function sitePlansDifferInCi(message) {
|
|
29721
|
+
const m = /\(you sent (.+?); (site_\w+) has (.+?)\), then check again/.exec(message);
|
|
29722
|
+
if (!m) return message;
|
|
29723
|
+
return `The site's plans in Checkbox (${m[3]}) are older than checkbox.json (${m[1]}). This run checks against checkbox.json, what this code sells. Once this change is deployed, update the site's plans (updateSetupSession plans[], or \`checkbox init --plan \u2026\`) so the site's own checks and its compliance plan use them; until then the plan's tasks may quote the old terms.`;
|
|
29724
|
+
}
|
|
28960
29725
|
async function runJob(job, engine, config, opts = {}) {
|
|
28961
29726
|
const base = { name: job.name, type: job.type, ...job.plan ? { plan: job.plan } : {}, target: { ...job.target }, ...job.fromSource ? { fromSource: true } : {}, warnings: [...job.warnings] };
|
|
28962
29727
|
if (job.error) return { ...base, status: "error", label: "Could not run", message: job.error, fixes: [] };
|
|
@@ -28970,22 +29735,41 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
28970
29735
|
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
29736
|
}
|
|
28972
29737
|
if (job.type === "notice_schedule") return { ...base, ...noticeCoverage(config, job.plan, report, opts), report };
|
|
29738
|
+
const pairing = (report.warnings ?? []).find((w) => w.code === "price_plan_conflict");
|
|
29739
|
+
if (pairing) {
|
|
29740
|
+
const names = (config.plans ?? []).map((p) => JSON.stringify(p.name)).join(", ");
|
|
29741
|
+
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 };
|
|
29742
|
+
}
|
|
28973
29743
|
const alerts = job.warnings.filter((w) => /^The params and checkbox\.json disagree/.test(w));
|
|
28974
29744
|
for (const w of report.warnings ?? []) {
|
|
28975
|
-
const text = ciVocabulary(w.message);
|
|
29745
|
+
const text = w.code === "site_plans_differ" ? sitePlansDifferInCi(w.message) : ciVocabulary(w.message);
|
|
28976
29746
|
base.warnings.push(text);
|
|
28977
29747
|
if (ALERT_CODES.has(w.code)) alerts.push(text);
|
|
28978
29748
|
}
|
|
28979
29749
|
if (alerts.length) Object.assign(base, { alerts });
|
|
29750
|
+
const onlyPlansDiffer = (report.warnings ?? []).some((w) => w.code === "site_plans_differ") && report.verdict !== "fail" && report.compliance_status !== "needs_fix" && (report.checks ?? []).length > 0 && (report.checks ?? []).every((c) => c.status === "pass" || c.status === "not_applicable" || !!c.compliance_status && OK_STATUSES.has(c.compliance_status));
|
|
29751
|
+
if (onlyPlansDiffer) {
|
|
29752
|
+
const pending = Number(report.compliance?.counts?.pending_live_check) || 0;
|
|
29753
|
+
report = { ...report, verdict: "pass", compliance_status: pending ? "pending_live_check" : "compliant", compliance: { ...report.compliance ?? {}, label: pending ? `Checks pass \xB7 ${pending} live check${pending === 1 ? "" : "s"} pending` : "Checks pass" } };
|
|
29754
|
+
}
|
|
28980
29755
|
const status = ciStatusOf(report);
|
|
28981
29756
|
const params = job.type === "stripe_checkout" ? job.body?.params ?? void 0 : void 0;
|
|
28982
29757
|
let fixes = (status === "pass" || status === "not_applicable" ? [] : fixesOf(report)).map((f) => ciVocabulary(job.type === "stripe_checkout" ? stripeConsentFix(f, params) : f));
|
|
29758
|
+
const tagsLeft = job.type === "email" && status !== "pass" ? (report.warnings ?? []).find((w) => w.code === "unresolved_placeholders") : void 0;
|
|
29759
|
+
if (tagsLeft) {
|
|
29760
|
+
const tags = [...new Set(tagsLeft.message.match(/\{\{\{?\s*[\w.-]+\s*\}?\}\}|\*\|[\w.-]+\|\*/g) ?? [])];
|
|
29761
|
+
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"}"`);
|
|
29762
|
+
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.`);
|
|
29763
|
+
}
|
|
28983
29764
|
const pricingPage = job.type === "page" && job.body?.page_kind === "pricing";
|
|
28984
|
-
|
|
28985
|
-
|
|
28986
|
-
const
|
|
29765
|
+
const cardLines = (report.plan_replacements ?? []).filter((p) => p.disclosure_text);
|
|
29766
|
+
if (pricingPage) fixes = pricingPageFixes(fixes, cardLines);
|
|
29767
|
+
const unreadText = (job.unresolved ?? []).filter((p) => /^(?:custom_text|consent_collection|discounts|subscription_data|allow_promotion_codes|payment_method_collection)\b|^mode\b/.test(p));
|
|
29768
|
+
const unread = job.type === "stripe_checkout" && unreadText.length > 0 && (status === "fail" || status === "needs_review" || status === "not_applicable" && unreadText.some((p) => /^mode\b/.test(p)) || status === "pass" && unreadText.some((p) => /^(?:custom_text|consent_collection|discounts|subscription_data(?:\.trial| \(may come from))/.test(p)));
|
|
29769
|
+
const spreadOnly = unreadText.length > 0 && unreadText.every((p) => / \(may come from \.\.\./.test(p));
|
|
28987
29770
|
if (unread) {
|
|
28988
|
-
|
|
29771
|
+
const what = spreadOnly ? `${unreadText.slice(0, 3).map((p) => p.replace(/ \((may come from [^)]*)\)$/, " $1")).join(", ")}: Checkbox does not follow a spread statically` : `${unreadText.slice(0, 3).join(", ")} ${unreadText.length === 1 ? "is an expression" : "are expressions"} Checkbox cannot read statically (e.g. a ternary or PLANS.monthly.*)`;
|
|
29772
|
+
fixes.splice(0, fixes.length, `${what}, so the checks that depend on ${unreadText.length === 1 ? "it were" : "them were"} not checked (no fix is given for a value Checkbox did not read). Use "module" (a function returning the exact params for each plan) or "file" instead of "source"${spreadOnly ? ", or write these keys in the params literal" : ""}.`);
|
|
28989
29773
|
}
|
|
28990
29774
|
const literal = [];
|
|
28991
29775
|
if (unread) {
|
|
@@ -29024,7 +29808,7 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
29024
29808
|
);
|
|
29025
29809
|
}
|
|
29026
29810
|
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);
|
|
29811
|
+
const corrected = unread ? void 0 : correctedOf(job.type, report, job.partial, job.placeholders, pricingPage);
|
|
29028
29812
|
if (corrected?.format === "json" && job.placeholders) {
|
|
29029
29813
|
const unset = job.warnings.filter((w) => /(?:was|were) not set when Checkbox ran/.test(w));
|
|
29030
29814
|
if (unset.length) Object.assign(base, { alerts: [...unset, ...base.alerts ?? []] });
|
|
@@ -29036,7 +29820,7 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
29036
29820
|
...base,
|
|
29037
29821
|
status: finalStatus,
|
|
29038
29822
|
...blocking ? { blocking: true } : {},
|
|
29039
|
-
label: unread ? `${literal.length ? `${literal.length} fix${literal.length === 1 ? "" : "es"} needed \xB7 ` : ""}Not checked: ${unreadText[0]} is an expression` : declaredCoupon ? "Needs review \xB7 coupon declared, not verified" : unreadCoupon ? "Needs review \xB7 coupon not read" : countedLabel(report.compliance?.label || DEFAULT_LABEL[status], report, pricingPage ? (f) => pricingPageFixes(f.map(ciVocabulary)) : void 0),
|
|
29823
|
+
label: unread ? `${literal.length ? `${literal.length} fix${literal.length === 1 ? "" : "es"} needed \xB7 ` : ""}Not checked: ${spreadOnly ? unreadText[0].replace(/ \((may come from [^)]*)\)$/, " $1") : `${unreadText[0]} is an expression`}` : declaredCoupon ? "Needs review \xB7 coupon declared, not verified" : unreadCoupon ? "Needs review \xB7 coupon not read" : countedLabel(report.compliance?.label || DEFAULT_LABEL[status], report, pricingPage ? (f) => pricingPageFixes(f.map(ciVocabulary), cardLines) : void 0),
|
|
29040
29824
|
...fixes[0] ? { fix: fixes[0] } : {},
|
|
29041
29825
|
fixes,
|
|
29042
29826
|
...corrected ? { corrected } : {},
|
|
@@ -29104,10 +29888,11 @@ async function runCi(jobs, engine, opts) {
|
|
|
29104
29888
|
checks,
|
|
29105
29889
|
...versioned?.rules_version ? { rules_version: versioned.rules_version } : {},
|
|
29106
29890
|
...versioned?.catalog_version ? { catalog_version: versioned.catalog_version } : {},
|
|
29107
|
-
disclaimer: CI_DISCLAIMER
|
|
29891
|
+
disclaimer: CI_DISCLAIMER,
|
|
29892
|
+
result: resultOf(status, exit, opts.strict)
|
|
29108
29893
|
};
|
|
29109
29894
|
}
|
|
29110
|
-
var ENGINE_KIND, CI_DISCLAIMER, RANK2, OK_STATUSES,
|
|
29895
|
+
var ENGINE_KIND, STRIPE_OBJECTS_OFFLINE_NOTICE, CI_DISCLAIMER, RANK2, OK_STATUSES, BOXED_HTML, BOXED_LINE, NOTICE_TO_EMAIL, EMAIL_LABEL, DEFAULT_LABEL, ALERT_CODES;
|
|
29111
29896
|
var init_runner = __esm({
|
|
29112
29897
|
"src/ci/runner.ts"() {
|
|
29113
29898
|
"use strict";
|
|
@@ -29116,6 +29901,7 @@ var init_runner = __esm({
|
|
|
29116
29901
|
init_src();
|
|
29117
29902
|
init_types2();
|
|
29118
29903
|
init_stripe_verdict();
|
|
29904
|
+
init_set_off_fix();
|
|
29119
29905
|
init_config3();
|
|
29120
29906
|
ENGINE_KIND = {
|
|
29121
29907
|
stripe_checkout: "stripe_checkout_session",
|
|
@@ -29124,10 +29910,12 @@ var init_runner = __esm({
|
|
|
29124
29910
|
email: "email",
|
|
29125
29911
|
notice_schedule: "notice_schedule"
|
|
29126
29912
|
};
|
|
29913
|
+
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
29914
|
CI_DISCLAIMER = "Automated check against Checkbox's catalog of automatic-renewal laws; not legal advice.";
|
|
29128
29915
|
RANK2 = { fail: 0, needs_review: 1 };
|
|
29129
29916
|
OK_STATUSES = /* @__PURE__ */ new Set(["compliant", "pending_live_check", "verified_live", "not_applicable"]);
|
|
29130
|
-
|
|
29917
|
+
BOXED_HTML = /border:1px solid currentColor/;
|
|
29918
|
+
BOXED_LINE = 'Style: the text next to the buttons is already bold, so a semibold line cannot stand out. Put each line in a border (style="border:1px solid currentColor;border-radius:6px;padding:8px 12px"), or make it 15% larger than the text around it: Checkbox measures both.';
|
|
29131
29919
|
NOTICE_TO_EMAIL = {
|
|
29132
29920
|
acknowledgment: "acknowledgment",
|
|
29133
29921
|
trial_ending: "trial_reminder",
|
|
@@ -29172,21 +29960,29 @@ function renderHuman(run2, c, opts = {}) {
|
|
|
29172
29960
|
const lines = [];
|
|
29173
29961
|
const versions = run2.rules_version ? ` \xB7 rules ${run2.rules_version} \xB7 catalog ${run2.catalog_version}` : "";
|
|
29174
29962
|
lines.push(`${c.bold("Checkbox CI")} ${run2.config} \xB7 ${run2.mode}${versions} \xB7 ${run2.checks.length} check${run2.checks.length === 1 ? "" : "s"}`);
|
|
29963
|
+
const unreadPrices = [];
|
|
29175
29964
|
const width = Math.min(40, Math.max(...run2.checks.map((x) => x.name.length), 10));
|
|
29176
29965
|
for (const x of run2.checks) {
|
|
29177
29966
|
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
29967
|
lines.push(` ${icon(x.status, c)} ${x.name.padEnd(width)} \u2192 ${label}`);
|
|
29179
29968
|
if (x.message) lines.push(c.dim(` ${wrap(x.message, 110, " ")}`));
|
|
29180
29969
|
if (x.fix) lines.push(` fix: ${wrap(x.fix, 110, " ")}${c.dim(` (${where(x)})`)}`);
|
|
29181
|
-
|
|
29970
|
+
for (const f of x.fixes.slice(1, opts.verbose ? void 0 : 4)) 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, " ")}`);
|
|
29971
|
+
if (!opts.verbose && x.fixes.length > 4) lines.push(c.dim(` (+${x.fixes.length - 4} more with --verbose)`));
|
|
29182
29972
|
const alerts = x.alerts ?? [];
|
|
29183
29973
|
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, " ")}`));
|
|
29974
|
+
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, " ")}`));
|
|
29975
|
+
for (const w of x.warnings) {
|
|
29976
|
+
const id = PRICE_UNREAD.exec(w)?.[1];
|
|
29977
|
+
if (id && !unreadPrices.includes(id)) unreadPrices.push(id);
|
|
29978
|
+
}
|
|
29185
29979
|
if (x.corrected && opts.verbose) lines.push(c.dim(` ${x.corrected.title}:`), ...x.corrected.body.split("\n").map((l) => c.dim(` ${l}`)));
|
|
29186
29980
|
}
|
|
29981
|
+
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
29982
|
const tone = run2.status === "pass" ? c.green : run2.status === "needs_review" && run2.exit_code === 0 ? c.yellow : c.red;
|
|
29188
29983
|
if (run2.status === "skipped") lines.push("", c.red(wrap(nothingCheckedMessage(run2), 110, "")));
|
|
29189
|
-
lines.push("",
|
|
29984
|
+
if (run2.offline_notice) lines.push("", c.yellow(wrap(`Not checked offline: ${run2.offline_notice}`, 110, " ")));
|
|
29985
|
+
lines.push("", `${tone(overallLine(run2))} \xB7 exit ${run2.exit_code}${run2.result ? ` (${run2.result.replace(/ \(exit \d+\)$/, "")})` : ""}`);
|
|
29190
29986
|
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
29987
|
lines.push(c.dim(run2.disclaimer));
|
|
29192
29988
|
return lines;
|
|
@@ -29198,8 +29994,9 @@ function annotations(run2) {
|
|
|
29198
29994
|
const level = x.status === "fail" || x.status === "error" ? "error" : x.status === "needs_review" ? run2.strict || x.blocking ? "error" : "warning" : alerts.length ? "warning" : void 0;
|
|
29199
29995
|
if (!level) continue;
|
|
29200
29996
|
const props = [`file=${escProp(x.target.file)}`, ...x.target.line ? [`line=${x.target.line}`] : [], `title=${escProp(`Checkbox: ${x.name} \u2014 ${x.label}`)}`];
|
|
29997
|
+
const removed = x.anchored?.removed?.length ? `This change removed ${x.anchored.removed.length === 1 ? "a line" : `${x.anchored.removed.length} lines`} here: ${x.anchored.removed.slice(0, 2).map((l) => `"${l.trim().slice(0, 120)}"`).join(", ")}${x.anchored.removed.length > 2 ? ", \u2026" : ""}` : "";
|
|
29201
29998
|
const body = x.status === "error" ? x.fix ? `${x.message ?? ""}
|
|
29202
|
-
${x.fix}` : `Could not run: ${x.message ?? ""}` : [x.status === "pass" && !x.fix ? "" : x.fix ?? x.label, ...x.fixes.slice(1, 4).map((f) => `\u2022 ${f}`), ...alerts.map((a) => `Warning: ${a}`), x.corrected ? `${x.corrected.title}: see the job summary.` : ""].filter(Boolean).join("\n");
|
|
29999
|
+
${x.fix}` : `Could not run: ${x.message ?? ""}` : [removed, x.status === "pass" && !x.fix ? "" : x.fix ?? x.label, ...x.fixes.slice(1, 4).map((f) => `\u2022 ${f}`), ...alerts.map((a) => `Warning: ${a}`), x.corrected ? `${x.corrected.title}: see the job summary.` : ""].filter(Boolean).join("\n");
|
|
29203
30000
|
out.push(`::${level} ${props.join(",")}::${escData(body)}`);
|
|
29204
30001
|
}
|
|
29205
30002
|
if (run2.status === "skipped") out.push(`::error file=${escProp(run2.config)},title=${escProp("Checkbox: nothing was checked")}::${escData(nothingCheckedMessage(run2))}`);
|
|
@@ -29211,6 +30008,8 @@ function stepSummary(run2) {
|
|
|
29211
30008
|
const versions = run2.rules_version ? ` \xB7 rules ${run2.rules_version} \xB7 catalog ${run2.catalog_version}` : "";
|
|
29212
30009
|
lines.push(`**${overallLine(run2)}** \u2014 \`${run2.config}\` \xB7 ${run2.mode}${versions}`, "");
|
|
29213
30010
|
if (run2.status === "skipped") lines.push(`\u274C ${nothingCheckedMessage(run2)}`, "");
|
|
30011
|
+
if (run2.result) lines.push(`Result: ${run2.result}.`, "");
|
|
30012
|
+
if (run2.offline_notice) lines.push(`\u26A0\uFE0F ${run2.offline_notice}`, "");
|
|
29214
30013
|
lines.push("| Check | Status | Fix | File |", "|---|---|---|---|");
|
|
29215
30014
|
for (const x of run2.checks) {
|
|
29216
30015
|
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 : "";
|
|
@@ -29219,7 +30018,7 @@ function stepSummary(run2) {
|
|
|
29219
30018
|
}
|
|
29220
30019
|
const detailed = run2.checks.filter((x) => x.corrected || x.fixes.length > 1);
|
|
29221
30020
|
for (const x of detailed) {
|
|
29222
|
-
lines.push("", `<details><summary><b>${escapeHtml(x.name)}</b>: ${escapeHtml(x.corrected ? x.corrected.title.toLowerCase() : `${x.fixes.length} fixes`)}</summary>`, "");
|
|
30021
|
+
lines.push("", `<details><summary><b>${escapeHtml(x.name)}</b>: ${escapeHtml(x.corrected ? x.corrected.title.charAt(0).toLowerCase() + x.corrected.title.slice(1) : `${x.fixes.length} fixes`)}</summary>`, "");
|
|
29223
30022
|
if (x.fixes.length) lines.push(...x.fixes.map((f) => `- ${f}`), "");
|
|
29224
30023
|
if (x.corrected) {
|
|
29225
30024
|
const fence = x.corrected.body.includes("```") ? "````" : "```";
|
|
@@ -29233,7 +30032,7 @@ function stepSummary(run2) {
|
|
|
29233
30032
|
function escapeHtml(s2) {
|
|
29234
30033
|
return s2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
29235
30034
|
}
|
|
29236
|
-
var TYPE_LABEL, where, escData, escProp, cell, STATUS_MD;
|
|
30035
|
+
var TYPE_LABEL, where, PRICE_UNREAD, escData, escProp, cell, STATUS_MD;
|
|
29237
30036
|
var init_report = __esm({
|
|
29238
30037
|
"src/ci/report.ts"() {
|
|
29239
30038
|
"use strict";
|
|
@@ -29247,6 +30046,7 @@ var init_report = __esm({
|
|
|
29247
30046
|
notice_schedule: "Notice schedule"
|
|
29248
30047
|
};
|
|
29249
30048
|
where = (c) => `${c.target.file}${c.target.line && c.target.line > 1 ? `:${c.target.line}` : ""}`;
|
|
30049
|
+
PRICE_UNREAD = /^line_items use (\S+), which Checkbox could not read\b/;
|
|
29250
30050
|
escData = (s2) => s2.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
29251
30051
|
escProp = (s2) => escData(s2).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
29252
30052
|
cell = (s2) => s2.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
|
|
@@ -29291,6 +30091,7 @@ function renderPlan(p, c, opts = {}) {
|
|
|
29291
30091
|
else if (t.status === "manual" && t.question) lines.push(` ${c.yellow("ask the human:")} ${t.question.text} ${c.dim(`\u2192 checkbox answer ${t.id} <${t.question.options?.join("|") ?? "yes|no"}>`)}`);
|
|
29292
30092
|
else if ((t.status === "done" || t.status === "wont_fix") && t.closed_reason) lines.push(c.dim(` closed: ${t.closed_reason.replace(/_/g, " ")}`));
|
|
29293
30093
|
else if (t.blocked_by.length) lines.push(c.dim(` after ${t.blocked_by.join(", ")}`));
|
|
30094
|
+
else if ((t.status === "open" || t.status === "in_progress") && opts.stale?.(t)) lines.push(` ${c.yellow("stale, re-verify after the site's plans are updated:")} ${opts.stale(t)}`);
|
|
29294
30095
|
else if (t.status === "open" || t.status === "in_progress") lines.push(c.dim(` ${t.where.file_hint ?? t.where.url ?? t.where.stripe_object ?? t.where.kind} \xB7 ${t.change.type} \xB7 checkbox fix ${t.id}`));
|
|
29295
30096
|
}
|
|
29296
30097
|
const hidden = p.tasks.length - tasks.length;
|
|
@@ -29302,7 +30103,7 @@ function statusColor(s2, c) {
|
|
|
29302
30103
|
return s2 === "compliant" ? c.green("all observed checks passing") : s2 === "action_required" ? c.red(s2) : c.yellow(s2);
|
|
29303
30104
|
}
|
|
29304
30105
|
function renderVerification(v, c) {
|
|
29305
|
-
const mark = v.status === "passed" ? c.green("passed") : v.status === "failed" ? c.red("failed") : v.status === "passed_with_gaps" ? c.yellow("passed with gaps (not verified)") : v.status === "passed_with_notes" ? c.yellow("passed with notes (a check outside your control needs review)") : v.status === "not_checked" ? c.yellow("not checked yet (missing evidence, nothing to change)") : v.status === "pending_live_check" ? c.yellow("live check pending (content passes; waiting for a real send)") : c.yellow("pending");
|
|
30106
|
+
const mark = v.status === "passed" ? c.green("passed") : v.status === "failed" ? c.red("failed") : v.status === "passed_with_gaps" ? c.yellow("passed with gaps (not verified)") : v.status === "passed_with_notes" ? c.yellow("passed with notes (a check outside your control needs review)") : v.status === "not_checked" ? c.yellow("not checked yet (missing evidence, nothing to change)") : v.status === "pending_live_check" ? c.yellow("live check pending (content passes; waiting for a real send)") : v.status === "needs_review" ? c.yellow("needs review (nothing failed; a check is still undecided: see next step)") : v.status === "superseded" ? c.yellow("superseded (newer evidence settled the task)") : c.yellow("pending");
|
|
29306
30107
|
const firstCheck = !v.task && v.next_action?.type === "first_check_complete";
|
|
29307
30108
|
const lines = [`${c.bold(`Verification ${v.id}`)}${v.task ? ` for ${v.task}` : ""}: ${firstCheck ? c.cyan("first check complete") : mark} (${v.method})`];
|
|
29308
30109
|
if (v.precheck) lines.push(` pre-check (${v.precheck.source}): ${v.precheck.verdict} \u2014 ${v.precheck.note}`);
|
|
@@ -29330,7 +30131,7 @@ function workflowYaml(needs = {}) {
|
|
|
29330
30131
|
const node = !!(needs.install || needs.build);
|
|
29331
30132
|
const steps = [" - uses: actions/checkout@v4"];
|
|
29332
30133
|
if (node) {
|
|
29333
|
-
steps.push(" - uses: actions/setup-node@v4", " with:", " node-version:
|
|
30134
|
+
steps.push(" - uses: actions/setup-node@v4", " with:", " node-version: 22");
|
|
29334
30135
|
if (needs.install) steps.push(` # ${needs.why?.install ?? 'checkbox.json "module" entries import your code and its dependencies.'}`, ` - run: ${needs.install}`);
|
|
29335
30136
|
if (needs.build) steps.push(` # ${needs.why?.build ?? 'checkbox.json "pages" read built HTML, which a fresh clone does not have.'}`, ` - run: ${needs.build}`);
|
|
29336
30137
|
} else {
|
|
@@ -29426,9 +30227,13 @@ var init_workflow = __esm({
|
|
|
29426
30227
|
// src/commands/ci/index.ts
|
|
29427
30228
|
var ci_exports = {};
|
|
29428
30229
|
__export(ci_exports, {
|
|
30230
|
+
anchorToChanges: () => anchorToChanges,
|
|
29429
30231
|
attachSiteForCoupons: () => attachSiteForCoupons,
|
|
29430
30232
|
couponIdsIn: () => couponIdsIn,
|
|
29431
30233
|
default: () => ci_default,
|
|
30234
|
+
diffHunks: () => diffHunks,
|
|
30235
|
+
siteFactsFor: () => siteFactsFor,
|
|
30236
|
+
termsChanged: () => termsChanged,
|
|
29432
30237
|
touchesConfig: () => touchesConfig,
|
|
29433
30238
|
undiffableInputs: () => undiffableInputs
|
|
29434
30239
|
});
|
|
@@ -29436,6 +30241,48 @@ import { execFileSync as execFileSync2 } from "node:child_process";
|
|
|
29436
30241
|
import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync2, realpathSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
29437
30242
|
import { createInterface } from "node:readline";
|
|
29438
30243
|
import { dirname as dirname7, join as join9, relative as relative5, resolve as resolve7 } from "node:path";
|
|
30244
|
+
function termsChanged(cwd, base, jobs) {
|
|
30245
|
+
if (jobs.some((j) => j.type === "stripe_checkout" && (j.sold || j.warnings.some((w) => /^The params and checkbox\.json disagree/.test(w))))) return true;
|
|
30246
|
+
try {
|
|
30247
|
+
const args = ["diff", "-U0", "--no-color", ...base ? [`${base}...HEAD`] : ["HEAD"], "--", ".", ":(exclude)*.md", ":(exclude)**/*.test.*", ":(exclude)**/*.lock", ":(exclude)package-lock.json", ":(exclude)pnpm-lock.yaml"];
|
|
30248
|
+
const out = execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 20 * 1024 * 1024 });
|
|
30249
|
+
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));
|
|
30250
|
+
} catch {
|
|
30251
|
+
return false;
|
|
30252
|
+
}
|
|
30253
|
+
}
|
|
30254
|
+
function diffHunks(cwd, base, file) {
|
|
30255
|
+
let out;
|
|
30256
|
+
try {
|
|
30257
|
+
out = execFileSync2("git", ["diff", "-U0", "--no-color", "--no-ext-diff", `${base}...HEAD`, "--", file], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 20 * 1024 * 1024 });
|
|
30258
|
+
} catch {
|
|
30259
|
+
return [];
|
|
30260
|
+
}
|
|
30261
|
+
const hunks = [];
|
|
30262
|
+
for (const l of out.split("\n")) {
|
|
30263
|
+
const h = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(l);
|
|
30264
|
+
if (h) hunks.push({ start: Number(h[1]), count: h[2] === void 0 ? 1 : Number(h[2]), removed: [] });
|
|
30265
|
+
else if (hunks.length && l.startsWith("-") && !l.startsWith("---")) hunks[hunks.length - 1].removed.push(l.slice(1));
|
|
30266
|
+
}
|
|
30267
|
+
return hunks;
|
|
30268
|
+
}
|
|
30269
|
+
function anchorToChanges(checks, cwd, workspace, base) {
|
|
30270
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
30271
|
+
for (const x of checks) {
|
|
30272
|
+
if (!(x.status === "fail" || x.status === "needs_review" || x.status === "error") || !x.target.line || x.failure) continue;
|
|
30273
|
+
const file = resolve7(workspace, x.target.file);
|
|
30274
|
+
if (!cache2.has(file)) cache2.set(file, diffHunks(cwd, base, file));
|
|
30275
|
+
const hunks = cache2.get(file);
|
|
30276
|
+
if (!hunks.length) continue;
|
|
30277
|
+
const line2 = x.target.line;
|
|
30278
|
+
if (hunks.some((h) => h.count > 0 && line2 >= h.start && line2 < h.start + h.count)) continue;
|
|
30279
|
+
const dist = (h) => h.count > 0 ? Math.min(Math.abs(line2 - h.start), Math.abs(line2 - (h.start + h.count - 1))) : Math.abs(line2 - h.start);
|
|
30280
|
+
const near2 = [...hunks].sort((a, b) => dist(a) - dist(b))[0];
|
|
30281
|
+
if (dist(near2) > 40) continue;
|
|
30282
|
+
x.anchored = { from: line2, ...near2.removed.length && near2.count === 0 ? { removed: near2.removed } : {} };
|
|
30283
|
+
x.target = { ...x.target, line: Math.max(1, near2.count > 0 ? near2.start : near2.start + 1) };
|
|
30284
|
+
}
|
|
30285
|
+
}
|
|
29439
30286
|
function gitChanged(cwd, base) {
|
|
29440
30287
|
try {
|
|
29441
30288
|
const root = execFileSync2("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
@@ -29505,6 +30352,36 @@ async function confirm(question) {
|
|
|
29505
30352
|
rl.close();
|
|
29506
30353
|
}
|
|
29507
30354
|
}
|
|
30355
|
+
async function siteFactsFor(ctx, setupSession) {
|
|
30356
|
+
if (!setupSession) return {};
|
|
30357
|
+
try {
|
|
30358
|
+
if (!ctx.credentials()) return {};
|
|
30359
|
+
const client = ctx.client();
|
|
30360
|
+
const sess = await Promise.race([client.sdk.setupSessions.retrieve(setupSession), new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 8e3))]);
|
|
30361
|
+
const out = {};
|
|
30362
|
+
const b = sess?.business ?? {};
|
|
30363
|
+
if (b.merchant_name) out.merchant_name = b.merchant_name;
|
|
30364
|
+
if (b.site_url) out.site_url = b.site_url;
|
|
30365
|
+
const cancel = b.cancellation_url ?? b.account_url;
|
|
30366
|
+
if (cancel) out.account_url = cancel;
|
|
30367
|
+
const plans = (sess?.site_plans ?? []).filter((p) => typeof p.amount === "number" && p.amount > 0 && typeof p.interval === "string");
|
|
30368
|
+
if (plans.length) {
|
|
30369
|
+
out.plans = plans.map((p) => ({
|
|
30370
|
+
// A name that is only the price ("$29.00 / month") is left out: the plans module names the plan better.
|
|
30371
|
+
...p.name && !/^\W?\s*[\d.,]+/.test(p.name) ? { name: p.name } : {},
|
|
30372
|
+
amount: p.amount,
|
|
30373
|
+
currency: p.currency ?? "usd",
|
|
30374
|
+
interval: p.interval,
|
|
30375
|
+
...p.interval_count && p.interval_count > 1 ? { interval_count: p.interval_count } : {},
|
|
30376
|
+
...p.trial_days ? { trial_days: p.trial_days } : {},
|
|
30377
|
+
...p.stripe_price_ids?.length ? { stripe_price_ids: p.stripe_price_ids } : {}
|
|
30378
|
+
}));
|
|
30379
|
+
}
|
|
30380
|
+
return out;
|
|
30381
|
+
} catch {
|
|
30382
|
+
return {};
|
|
30383
|
+
}
|
|
30384
|
+
}
|
|
29508
30385
|
async function runInit(ctx, argv) {
|
|
29509
30386
|
const p = parseArgs(argv, { boolean: ["yes", "force", "dry-run", "workflow"], string: ["config"], alias: { y: "yes" } });
|
|
29510
30387
|
if (p.positionals.length) throw new UsageError(`Unexpected argument "${p.positionals[0]}"`);
|
|
@@ -29515,13 +30392,24 @@ async function runInit(ctx, argv) {
|
|
|
29515
30392
|
const withWorkflow = bool(p.flags, "workflow") !== false;
|
|
29516
30393
|
const projects = ctx.config.load().init?.projects ?? {};
|
|
29517
30394
|
let facts = {};
|
|
30395
|
+
let setupSession;
|
|
29518
30396
|
for (let dir = resolve7(ctx.cwd); ; dir = dirname7(dir)) {
|
|
29519
|
-
|
|
29520
|
-
|
|
30397
|
+
const proj = projects[dir];
|
|
30398
|
+
if (proj?.facts || proj?.setup_session) {
|
|
30399
|
+
facts = proj.facts ?? {};
|
|
30400
|
+
setupSession = proj.setup_session;
|
|
29521
30401
|
break;
|
|
29522
30402
|
}
|
|
29523
30403
|
if (dirname7(dir) === dir) break;
|
|
29524
30404
|
}
|
|
30405
|
+
const fromSite = await siteFactsFor(ctx, setupSession);
|
|
30406
|
+
facts = { ...fromSite, ...facts, ...fromSite.plans && !facts.plans ? { plans: fromSite.plans } : {} };
|
|
30407
|
+
if (Array.isArray(facts.plans) && Array.isArray(fromSite.plans)) {
|
|
30408
|
+
facts.plans = facts.plans.map((p2) => {
|
|
30409
|
+
const m = fromSite.plans.find((x) => x.amount === p2.amount && x.interval === p2.interval);
|
|
30410
|
+
return m && !p2.stripe_price_ids ? { ...p2, stripe_price_ids: m.stripe_price_ids } : p2;
|
|
30411
|
+
});
|
|
30412
|
+
}
|
|
29525
30413
|
const s2 = scaffoldConfig(ctx.cwd, ctx.env, facts);
|
|
29526
30414
|
const actionFor = (f) => existsSync8(f) ? force ? "overwrite" : "keep" : "create";
|
|
29527
30415
|
const files = [{ path: configPath, content: `${JSON.stringify(s2.config, null, 2)}
|
|
@@ -29553,8 +30441,13 @@ async function runInit(ctx, argv) {
|
|
|
29553
30441
|
found: s2.found,
|
|
29554
30442
|
...withWorkflow ? { workflow_steps: workflowSteps } : {},
|
|
29555
30443
|
todos: s2.todos,
|
|
30444
|
+
// Round 32 (pilot 31, ci-pr-agent: "Fill in the 2 TODO item(s)" with 1 TODO in the file): suggestions are notes;
|
|
30445
|
+
// the count is the number of "TODO" values in the file.
|
|
30446
|
+
todo_values: s2.todoValues,
|
|
30447
|
+
notes: s2.notes,
|
|
30448
|
+
...Object.keys(fromSite).length ? { from_site: Object.keys(fromSite) } : {},
|
|
29556
30449
|
config: s2.config,
|
|
29557
|
-
next_action: s2.
|
|
30450
|
+
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
30451
|
};
|
|
29559
30452
|
ctx.out.result(result, () => {
|
|
29560
30453
|
const lines = [];
|
|
@@ -29564,6 +30457,7 @@ async function runInit(ctx, argv) {
|
|
|
29564
30457
|
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
30458
|
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
30459
|
if (s2.todos.length) lines.push("", "To do:", ...s2.todos.map((t) => ` - ${t}`));
|
|
30460
|
+
if (s2.notes.length) lines.push("", "Also:", ...s2.notes.map((t) => ` - ${t}`));
|
|
29567
30461
|
lines.push("", `${ctx.out.c.cyan("Next:")} ${result.next_action.message}`);
|
|
29568
30462
|
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
30463
|
return lines;
|
|
@@ -29728,13 +30622,18 @@ ${message}
|
|
|
29728
30622
|
const engine = client ? onlineEngine(client) : offlineEngine();
|
|
29729
30623
|
const notices = client && declaredByCheckbox(loaded.config).length ? await readNotices(ctx, client, str(p.flags, "site")) : void 0;
|
|
29730
30624
|
const noticesLine = Math.max(1, loaded.raw.split("\n").findIndex((l) => l.includes('"sent_by_checkbox"')) + 1);
|
|
29731
|
-
const
|
|
30625
|
+
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 } } : {} });
|
|
30626
|
+
const baseRef = str(p.flags, "base") ?? (ctx.env.GITHUB_BASE_REF ? `origin/${ctx.env.GITHUB_BASE_REF}` : void 0);
|
|
30627
|
+
if (baseRef) anchorToChanges(run0.checks, ctx.cwd, workspace, baseRef);
|
|
30628
|
+
const run2 = !client && termsChanged(ctx.cwd, baseRef, jobs) ? { ...run0, offline_notice: STRIPE_OBJECTS_OFFLINE_NOTICE } : run0;
|
|
29732
30629
|
ctx.out.result(run2, () => renderHuman(run2, ctx.out.c, { verbose: bool(p.flags, "verbose") === true }));
|
|
29733
30630
|
if (gha) {
|
|
29734
30631
|
for (const a of annotations(run2)) emit(a);
|
|
29735
30632
|
writeGithubFile(ctx.env.GITHUB_STEP_SUMMARY, stepSummary(run2));
|
|
30633
|
+
if (run2.offline_notice) emit(`::notice title=Checkbox%3A Stripe not checked offline::${run2.offline_notice}`);
|
|
29736
30634
|
writeGithubFile(ctx.env.GITHUB_OUTPUT, `status=${run2.status}
|
|
29737
30635
|
exit_code=${run2.exit_code}
|
|
30636
|
+
result=${run2.result}
|
|
29738
30637
|
`);
|
|
29739
30638
|
}
|
|
29740
30639
|
return run2.exit_code;
|
|
@@ -29794,6 +30693,14 @@ function findAnchor(src, target) {
|
|
|
29794
30693
|
}
|
|
29795
30694
|
}
|
|
29796
30695
|
}
|
|
30696
|
+
if (!text && !id && tag) {
|
|
30697
|
+
const all = [...src.matchAll(tagRe)];
|
|
30698
|
+
if (all.length === 1) return openAt(all[0].index + 1, `the only <${tag}> in this file`);
|
|
30699
|
+
if (tag === "button" || tag === "input") {
|
|
30700
|
+
const submit = all.filter((m) => /^[^>]*\btype\s*=\s*(?:"submit"|'submit'|\{\s*["']submit["']\s*\})/i.test(src.slice(m.index, src.indexOf(">", m.index) + 1)));
|
|
30701
|
+
if (submit.length === 1) return openAt(submit[0].index + 1, `the only <${tag} type="submit"> in this file`);
|
|
30702
|
+
}
|
|
30703
|
+
}
|
|
29797
30704
|
return null;
|
|
29798
30705
|
}
|
|
29799
30706
|
function lineStart(src, idx) {
|
|
@@ -29916,12 +30823,20 @@ function diffLines(a, b) {
|
|
|
29916
30823
|
ops.push(...a.slice(a.length - suf).map((line2) => ({ t: " ", line: line2 })));
|
|
29917
30824
|
return ops;
|
|
29918
30825
|
}
|
|
30826
|
+
function gitLines(text) {
|
|
30827
|
+
if (text === "") return [];
|
|
30828
|
+
const lines = text.split("\n");
|
|
30829
|
+
if (text.endsWith("\n")) lines.pop();
|
|
30830
|
+
else lines[lines.length - 1] += NO_EOL;
|
|
30831
|
+
return lines;
|
|
30832
|
+
}
|
|
29919
30833
|
function unifiedDiff(before, after, path, context = 3) {
|
|
29920
30834
|
if (before === after) return "";
|
|
29921
|
-
const a = before
|
|
29922
|
-
const b = after
|
|
30835
|
+
const a = gitLines(before);
|
|
30836
|
+
const b = gitLines(after);
|
|
29923
30837
|
const ops = diffLines(a, b);
|
|
29924
30838
|
const out = [`--- a/${path}`, `+++ b/${path}`];
|
|
30839
|
+
const show2 = (o) => o.line.endsWith(NO_EOL) ? [`${o.t}${o.line.slice(0, -NO_EOL.length)}`, NO_EOL_MARKER] : [`${o.t}${o.line}`];
|
|
29925
30840
|
let i = 0;
|
|
29926
30841
|
while (i < ops.length) {
|
|
29927
30842
|
if (ops[i].t === " ") {
|
|
@@ -29943,26 +30858,50 @@ function unifiedDiff(before, after, path, context = 3) {
|
|
|
29943
30858
|
const bStart = ops.slice(0, start).filter((o) => o.t !== "-").length + 1;
|
|
29944
30859
|
const aLen = slice.filter((o) => o.t !== "+").length;
|
|
29945
30860
|
const bLen = slice.filter((o) => o.t !== "-").length;
|
|
29946
|
-
out.push(`@@ -${aLen ? aStart : aStart - 1},${aLen} +${bLen ? bStart : bStart - 1},${bLen} @@`, ...slice.
|
|
30861
|
+
out.push(`@@ -${aLen ? aStart : aStart - 1},${aLen} +${bLen ? bStart : bStart - 1},${bLen} @@`, ...slice.flatMap(show2));
|
|
29947
30862
|
i = stop;
|
|
29948
30863
|
start = stop;
|
|
29949
30864
|
}
|
|
29950
30865
|
return `${out.join("\n")}
|
|
29951
30866
|
`;
|
|
29952
30867
|
}
|
|
29953
|
-
var escapeRe2, VOID_TAGS;
|
|
30868
|
+
var escapeRe2, VOID_TAGS, NO_EOL, NO_EOL_MARKER;
|
|
29954
30869
|
var init_patch = __esm({
|
|
29955
30870
|
"src/commands/plan/patch.ts"() {
|
|
29956
30871
|
"use strict";
|
|
29957
30872
|
escapeRe2 = (s2) => s2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
29958
30873
|
VOID_TAGS = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"];
|
|
30874
|
+
NO_EOL = "\0no-eol";
|
|
30875
|
+
NO_EOL_MARKER = "\";
|
|
29959
30876
|
}
|
|
29960
30877
|
});
|
|
29961
30878
|
|
|
29962
30879
|
// src/commands/plan/fix.ts
|
|
29963
30880
|
import { existsSync as existsSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
|
|
29964
30881
|
import { relative as relative6, resolve as resolve8 } from "node:path";
|
|
29965
|
-
function
|
|
30882
|
+
function declaredPlans(cwd) {
|
|
30883
|
+
const path = findConfig(cwd);
|
|
30884
|
+
if (!path) return [];
|
|
30885
|
+
try {
|
|
30886
|
+
return loadConfig(path).config.plans ?? [];
|
|
30887
|
+
} catch {
|
|
30888
|
+
return [];
|
|
30889
|
+
}
|
|
30890
|
+
}
|
|
30891
|
+
function staleAgainstDeclared(content, plans) {
|
|
30892
|
+
if (!plans.length || !content) return void 0;
|
|
30893
|
+
const text = content.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ");
|
|
30894
|
+
const said = [];
|
|
30895
|
+
for (const m of text.matchAll(/(\d{1,3})[- ]days? free,? then \$([\d,]+(?:\.\d{1,2})?) (?:every|per|a|\/) ?(month|year|week|day)/gi)) {
|
|
30896
|
+
const trial = Number(m[1]);
|
|
30897
|
+
const price3 = Number(m[2].replace(/,/g, ""));
|
|
30898
|
+
const interval = m[3].toLowerCase();
|
|
30899
|
+
const same = plans.filter((p) => p.interval === interval && major(p) !== void 0 && Math.abs(major(p) - price3) < 5e-3);
|
|
30900
|
+
if (same.length && !same.some((p) => (p.trial_days ?? 0) === trial)) said.push(`it says \u201C${m[0]}\u201D, but checkbox.json declares ${same.map((p) => `"${p.name}" with ${p.trial_days ? `a ${p.trial_days}-day trial` : "no trial"}`).join(", ")}`);
|
|
30901
|
+
}
|
|
30902
|
+
return said.length ? said.join("; ") : void 0;
|
|
30903
|
+
}
|
|
30904
|
+
function fixForTask(task, cwd, file, plans = declaredPlans(cwd)) {
|
|
29966
30905
|
const base = {
|
|
29967
30906
|
object: "task_fix",
|
|
29968
30907
|
task: task.id,
|
|
@@ -29973,6 +30912,22 @@ function fixForTask(task, cwd, file) {
|
|
|
29973
30912
|
verify: task.verify
|
|
29974
30913
|
};
|
|
29975
30914
|
const verifyCmd = `checkbox verify ${task.id} --wait`;
|
|
30915
|
+
const stale = staleAgainstDeclared(`${task.change.content}
|
|
30916
|
+
${task.change.edits.map((e) => e.content ?? "").join("\n")}`, plans);
|
|
30917
|
+
if (stale) {
|
|
30918
|
+
return {
|
|
30919
|
+
...base,
|
|
30920
|
+
file: null,
|
|
30921
|
+
diff: null,
|
|
30922
|
+
applied: [],
|
|
30923
|
+
unmatched: [],
|
|
30924
|
+
next_action: {
|
|
30925
|
+
type: "refresh_plan",
|
|
30926
|
+
description: `Not applied: this task was built from the site's plans in Checkbox, which are older than checkbox.json (${stale}). Applying it would put the old terms back. Leave this file as it is; once this change is deployed, update the site's plans (updateSetupSession plans[], or \`checkbox init --plan \u2026\`), then run \`checkbox plan\` again for a fix built from the new plans.`,
|
|
30927
|
+
command: "checkbox plan"
|
|
30928
|
+
}
|
|
30929
|
+
};
|
|
30930
|
+
}
|
|
29976
30931
|
const candidate = file ?? (task.where.file_hint && existsSync9(resolve8(cwd, task.where.file_hint)) ? task.where.file_hint : void 0);
|
|
29977
30932
|
if (task.change.type === "patch" && task.change.edits.length && candidate) {
|
|
29978
30933
|
const path = resolve8(cwd, candidate);
|
|
@@ -30010,14 +30965,16 @@ function fixForTask(task, cwd, file) {
|
|
|
30010
30965
|
const type = task.change.type === "manual" ? "answer_question" : task.change.type === "needs_facts" ? "provide_facts" : "apply_change";
|
|
30011
30966
|
return { ...base, file: null, diff: null, applied: [], unmatched: [], next_action: { type, description: how[task.change.type] ?? "Apply the change, then verify.", command: task.change.type === "manual" ? `checkbox answer ${task.id}` : task.change.type === "needs_facts" ? `checkbox fix ${task.id}` : verifyCmd } };
|
|
30012
30967
|
}
|
|
30013
|
-
var TASK_RE2, fix_default;
|
|
30968
|
+
var TASK_RE2, major, fix_default;
|
|
30014
30969
|
var init_fix = __esm({
|
|
30015
30970
|
"src/commands/plan/fix.ts"() {
|
|
30016
30971
|
"use strict";
|
|
30017
30972
|
init_args();
|
|
30018
30973
|
init_types2();
|
|
30974
|
+
init_config3();
|
|
30019
30975
|
init_patch();
|
|
30020
30976
|
TASK_RE2 = /^task_[0-9a-f]{32}$/;
|
|
30977
|
+
major = (p) => p.price !== void 0 ? Number(p.price) : p.amount !== void 0 ? p.amount / 100 : void 0;
|
|
30021
30978
|
fix_default = defineCommand({
|
|
30022
30979
|
name: "fix",
|
|
30023
30980
|
summary: "Show the change for a plan task (exact diff of your file for code tasks)",
|
|
@@ -30243,6 +31200,10 @@ function planSnippet(d, s2) {
|
|
|
30243
31200
|
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
31201
|
}
|
|
30245
31202
|
if (existing === null) {
|
|
31203
|
+
if (d.framework === "nextjs-app") {
|
|
31204
|
+
const contents = NEXT_APP_LAYOUT(s2, /\.tsx$/.test(file));
|
|
31205
|
+
return { change: { file, action: "create", summary: "Root layout with the snippet in <head>", contents, added: contents.trim().split("\n") } };
|
|
31206
|
+
}
|
|
30246
31207
|
if (d.framework === "nextjs-pages") {
|
|
30247
31208
|
const contents = NEXT_DOCUMENT(s2);
|
|
30248
31209
|
return { change: { file, action: "create", summary: "Custom _document with the snippet in <Head>", contents, added: contents.trim().split("\n") } };
|
|
@@ -30371,7 +31332,7 @@ function applyChange(root, c) {
|
|
|
30371
31332
|
mkdirSync3(dirname8(abs), { recursive: true });
|
|
30372
31333
|
writeFileSync4(abs, c.contents, c.file.startsWith(".env") ? { mode: 384 } : void 0);
|
|
30373
31334
|
}
|
|
30374
|
-
var MARK_START, MARK_END, NEXT_DOCUMENT, NUXT_PLUGIN;
|
|
31335
|
+
var MARK_START, MARK_END, NEXT_DOCUMENT, NEXT_APP_LAYOUT, NUXT_PLUGIN;
|
|
30375
31336
|
var init_patch2 = __esm({
|
|
30376
31337
|
"src/commands/init/patch.ts"() {
|
|
30377
31338
|
"use strict";
|
|
@@ -30392,6 +31353,17 @@ ${jsxBlock(s2, " ").join("\n")}
|
|
|
30392
31353
|
</Html>
|
|
30393
31354
|
);
|
|
30394
31355
|
}
|
|
31356
|
+
`;
|
|
31357
|
+
NEXT_APP_LAYOUT = (s2, ts) => `${ts ? "import type { ReactNode } from 'react';\n\n" : ""}export default function RootLayout({ children }${ts ? ": { children: ReactNode }" : ""}) {
|
|
31358
|
+
return (
|
|
31359
|
+
<html lang="en">
|
|
31360
|
+
<head>
|
|
31361
|
+
${jsxBlock(s2, " ").join("\n")}
|
|
31362
|
+
</head>
|
|
31363
|
+
<body>{children}</body>
|
|
31364
|
+
</html>
|
|
31365
|
+
);
|
|
31366
|
+
}
|
|
30395
31367
|
`;
|
|
30396
31368
|
NUXT_PLUGIN = (s2) => `// ${MARK_START} \u2014 Checkbox subscription-compliance monitoring (https://checkbox.site/docs/snippet)
|
|
30397
31369
|
export default defineNuxtPlugin(() => {
|
|
@@ -30412,8 +31384,11 @@ __export(init_exports, {
|
|
|
30412
31384
|
configLines: () => configLines,
|
|
30413
31385
|
configNameOf: () => configNameOf,
|
|
30414
31386
|
default: () => init_default,
|
|
31387
|
+
liveKeyNextAction: () => liveKeyNextAction,
|
|
30415
31388
|
parsePlanFlag: () => parsePlanFlag,
|
|
30416
|
-
|
|
31389
|
+
snippetWords: () => snippetWords,
|
|
31390
|
+
syncCheckoutConfigs: () => syncCheckoutConfigs,
|
|
31391
|
+
withEvaluatedAt: () => withEvaluatedAt
|
|
30417
31392
|
});
|
|
30418
31393
|
import { existsSync as existsSync11 } from "node:fs";
|
|
30419
31394
|
import { join as join11 } from "node:path";
|
|
@@ -30433,6 +31408,10 @@ function saveState(ctx, root, patch) {
|
|
|
30433
31408
|
}
|
|
30434
31409
|
ctx.config.save({ init: { ...cfg, projects } });
|
|
30435
31410
|
}
|
|
31411
|
+
function withEvaluatedAt(c) {
|
|
31412
|
+
const t = c.evaluated;
|
|
31413
|
+
return typeof t === "number" && t > 0 ? { evaluated_at: new Date(t * 1e3).toISOString(), ...c } : c;
|
|
31414
|
+
}
|
|
30436
31415
|
async function ask2(question) {
|
|
30437
31416
|
const rl = createInterface2({ input: process.stdin, output: process.stderr, terminal: true });
|
|
30438
31417
|
try {
|
|
@@ -30460,6 +31439,7 @@ function projectFacts(d) {
|
|
|
30460
31439
|
...d.layoutFile ? { layout_file: d.layoutFile } : {},
|
|
30461
31440
|
env_file: d.envFile,
|
|
30462
31441
|
...d.publicDir ? { public_dir: d.publicDir } : {},
|
|
31442
|
+
...d.mailer ? { mailer: d.mailer } : {},
|
|
30463
31443
|
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
31444
|
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
31445
|
};
|
|
@@ -30547,6 +31527,20 @@ function registrable(params, unresolved) {
|
|
|
30547
31527
|
if (unresolved.some((u) => /^(mode|line_items|subscription_data|custom_text|consent_collection)\b/.test(u))) return false;
|
|
30548
31528
|
return params.mode === "subscription" && Array.isArray(params.line_items) && params.line_items.length > 0;
|
|
30549
31529
|
}
|
|
31530
|
+
function snippetWords(homepage, live) {
|
|
31531
|
+
if (homepage) return "detected";
|
|
31532
|
+
if (live) return "live (seen through the page captures it sends; not in the homepage HTML Checkbox loaded)";
|
|
31533
|
+
return "NOT detected yet (deploy, then verify again)";
|
|
31534
|
+
}
|
|
31535
|
+
function liveKeyNextAction(dashboard) {
|
|
31536
|
+
const url = `${dashboard.replace(/\/+$/, "")}/settings/developers?mode=live&create=key`;
|
|
31537
|
+
return {
|
|
31538
|
+
type: "ask_human_for_live_key",
|
|
31539
|
+
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.`,
|
|
31540
|
+
command: "checkbox init --live --relogin --yes --json",
|
|
31541
|
+
url
|
|
31542
|
+
};
|
|
31543
|
+
}
|
|
30550
31544
|
function detectionSummary(d, siteUrl) {
|
|
30551
31545
|
return {
|
|
30552
31546
|
framework: d.framework,
|
|
@@ -30569,9 +31563,14 @@ function detectionSummary(d, siteUrl) {
|
|
|
30569
31563
|
function fileOut(c) {
|
|
30570
31564
|
return { file: c.file, action: c.action, summary: c.summary, added: c.added };
|
|
30571
31565
|
}
|
|
30572
|
-
function
|
|
31566
|
+
function resultOf2(status, d, session, changes, extra, apiBase) {
|
|
30573
31567
|
const names = envVarNames(d);
|
|
30574
|
-
const next = session.next_action.type === "done" ? {
|
|
31568
|
+
const next = session.next_action.type === "done" ? {
|
|
31569
|
+
type: "done",
|
|
31570
|
+
// 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".
|
|
31571
|
+
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.",
|
|
31572
|
+
command: "checkbox plan --json"
|
|
31573
|
+
} : session.next_action;
|
|
30575
31574
|
return {
|
|
30576
31575
|
object: "checkbox_init",
|
|
30577
31576
|
status,
|
|
@@ -30586,7 +31585,10 @@ function resultOf(status, d, session, changes, extra, apiBase) {
|
|
|
30586
31585
|
install: session.install,
|
|
30587
31586
|
stripe: { ...session.stripe, ...extra.stripe ?? {} },
|
|
30588
31587
|
...extra.live && Object.keys(extra.live).length ? { live: extra.live } : {},
|
|
30589
|
-
compliance:
|
|
31588
|
+
// Round 32 (pilot 31, discovery-agent: "compliance.evaluated" read like a count but is a Unix timestamp): the CLI says
|
|
31589
|
+
// when, as evaluated_at (ISO 8601), next to the API's field.
|
|
31590
|
+
compliance: session.compliance ? withEvaluatedAt(session.compliance) : session.compliance,
|
|
31591
|
+
...session.go_live ? { go_live: session.go_live } : {},
|
|
30590
31592
|
steps: session.steps.map((s2) => ({ key: s2.key, status: s2.status, summary: s2.summary })),
|
|
30591
31593
|
warnings: extra.warnings ?? [],
|
|
30592
31594
|
owner_notified: session.owner_notified,
|
|
@@ -30609,6 +31611,10 @@ function renderSession(ctx, s2) {
|
|
|
30609
31611
|
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
31612
|
if (todo.length > 5) lines.push(c.dim(` \u2026 ${todo.length - 5} more: checkbox plan`));
|
|
30611
31613
|
}
|
|
31614
|
+
if (s2.go_live?.steps.length) {
|
|
31615
|
+
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"}`));
|
|
31616
|
+
s2.go_live.steps.forEach((st, i) => lines.push(` ${i + 1}. ${c.dim(st.who === "person" ? "[owner]" : "[agent]")} ${st.text}`));
|
|
31617
|
+
}
|
|
30612
31618
|
lines.push("", `${c.cyan("next")} ${s2.next_action.message}${s2.next_action.command ? `
|
|
30613
31619
|
${s2.next_action.command}` : ""}`, `${c.dim("review")} ${s2.review_url}`);
|
|
30614
31620
|
return lines;
|
|
@@ -30634,6 +31640,7 @@ var init_init = __esm({
|
|
|
30634
31640
|
"use strict";
|
|
30635
31641
|
init_args();
|
|
30636
31642
|
init_config();
|
|
31643
|
+
init_src2();
|
|
30637
31644
|
init_http();
|
|
30638
31645
|
init_types2();
|
|
30639
31646
|
init_detect();
|
|
@@ -30679,7 +31686,7 @@ var init_init = __esm({
|
|
|
30679
31686
|
" --no-stripe Do not offer the Stripe test key / test subscription",
|
|
30680
31687
|
" --live Also request live-mode keys and write the domain-verification file",
|
|
30681
31688
|
" --no-wait Start the login, print the link and exit (run `checkbox init` again to continue)",
|
|
30682
|
-
" --wait-seconds <n> How long to wait for the approval (default 900 on a terminal, 100 otherwise)",
|
|
31689
|
+
" --wait-seconds <n> How long to wait for the approval (default 900 on a terminal, 40 with --json, 100 otherwise)",
|
|
30683
31690
|
" --no-open Do not open the browser",
|
|
30684
31691
|
" --verify Re-check the site now (after deploying) and print the updated checklist",
|
|
30685
31692
|
" --complete Finish the setup and email the human the summary (sent automatically the first time every",
|
|
@@ -30733,7 +31740,7 @@ var init_init = __esm({
|
|
|
30733
31740
|
if (bool(flags, "verify")) {
|
|
30734
31741
|
const v = await client2.sdk.setupSessions.verify(state.setup_session);
|
|
30735
31742
|
session2 = v.setup_session;
|
|
30736
|
-
out.info(v.homepage.ok ? `Site loaded; snippet ${v.homepage.snippet_detected
|
|
31743
|
+
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
31744
|
} else {
|
|
30738
31745
|
session2 = await client2.sdk.setupSessions.retrieve(state.setup_session);
|
|
30739
31746
|
}
|
|
@@ -30743,7 +31750,7 @@ var init_init = __esm({
|
|
|
30743
31750
|
const configs = await syncCheckoutConfigs(client2, session2.site, d.checkoutSessions, root).catch((err) => [{ file: "", line: 0, skipped: err instanceof Error ? err.message : String(err) }]);
|
|
30744
31751
|
if (configs.length) stripeOut.checkout_configs = configs;
|
|
30745
31752
|
}
|
|
30746
|
-
out.result(
|
|
31753
|
+
out.result(resultOf2("verified", d, session2, [], Object.keys(stripeOut).length ? { stripe: stripeOut } : {}, apiBase), () => [...renderSession(ctx, session2), ...configLines(stripeOut.checkout_configs)]);
|
|
30747
31754
|
return EXIT.ok;
|
|
30748
31755
|
}
|
|
30749
31756
|
if (dryRun && (!state.setup_session || !creds)) {
|
|
@@ -30772,10 +31779,11 @@ var init_init = __esm({
|
|
|
30772
31779
|
let token = null;
|
|
30773
31780
|
let apiKey = creds?.apiKey;
|
|
30774
31781
|
let sessionId = state.setup_session;
|
|
30775
|
-
const
|
|
31782
|
+
const pendingLogin = state.pending && Date.parse(state.pending.expires_at) > Date.now() + 1e4 && state.pending.api_base === apiBase ? state.pending : void 0;
|
|
31783
|
+
const reuse = !!(state.setup_session && creds && bool(flags, "relogin") !== true && !pendingLogin);
|
|
30776
31784
|
if (!reuse) {
|
|
30777
31785
|
const anon = ctx.client({ apiKey: "device-login", apiBase });
|
|
30778
|
-
let pending =
|
|
31786
|
+
let pending = pendingLogin;
|
|
30779
31787
|
if (!pending) {
|
|
30780
31788
|
const agent2 = detectAgent(ctx.env, agentName);
|
|
30781
31789
|
const wantStripe = bool(flags, "stripe") !== false && d.billingPlatform === "stripe" && !!d.stripeTestKey;
|
|
@@ -30788,7 +31796,8 @@ var init_init = __esm({
|
|
|
30788
31796
|
...merchantName ? { merchant_name: merchantName.slice(0, 100) } : {},
|
|
30789
31797
|
framework: d.framework,
|
|
30790
31798
|
...d.packageManager ? { package_manager: d.packageManager } : {},
|
|
30791
|
-
|
|
31799
|
+
// "custom" means nothing was detected: never sent as a declared platform (the API then looks for Stripe itself).
|
|
31800
|
+
...d.billingPlatform !== "custom" ? { billing_platform: d.billingPlatform } : {},
|
|
30792
31801
|
...bool(flags, "live") ? { live: true } : {},
|
|
30793
31802
|
...wantStripe ? { stripe_test_key_hint: d.stripeTestKey.hint } : {}
|
|
30794
31803
|
});
|
|
@@ -30806,7 +31815,7 @@ var init_init = __esm({
|
|
|
30806
31815
|
if (bool(flags, "open") !== false) openBrowser(pending.verification_uri_complete, ctx.env);
|
|
30807
31816
|
const tty = !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
30808
31817
|
const waitArg = str(flags, "wait-seconds");
|
|
30809
|
-
const waitSeconds = bool(flags, "wait") === false ? 0 : waitArg !== void 0 ? Number(waitArg) : tty ? 900 : 100;
|
|
31818
|
+
const waitSeconds = bool(flags, "wait") === false ? 0 : waitArg !== void 0 ? Number(waitArg) : tty ? 900 : out.json ? 40 : 100;
|
|
30810
31819
|
if (!Number.isFinite(waitSeconds) || waitSeconds < 0) throw new UsageError("--wait-seconds must be a number of seconds");
|
|
30811
31820
|
const polled = await pollDeviceLogin(anon, pending, waitSeconds * 1e3, ctx);
|
|
30812
31821
|
if (polled.status === "timeout") {
|
|
@@ -30819,7 +31828,7 @@ var init_init = __esm({
|
|
|
30819
31828
|
expires_at: pending.expires_at,
|
|
30820
31829
|
next_action: {
|
|
30821
31830
|
type: "ask_human_to_approve",
|
|
30822
|
-
message: `Ask the human to open ${pending.verification_uri_complete} and click Approve,
|
|
31831
|
+
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
31832
|
command: "checkbox init --yes --json",
|
|
30824
31833
|
url: pending.verification_uri_complete
|
|
30825
31834
|
}
|
|
@@ -30962,9 +31971,12 @@ var init_init = __esm({
|
|
|
30962
31971
|
}
|
|
30963
31972
|
const live = {};
|
|
30964
31973
|
if (!dryRun && bool(flags, "live") && siteUrl) {
|
|
30965
|
-
const liveKey = token?.live_secret_key ?? void 0;
|
|
30966
|
-
if (!liveKey) {
|
|
30967
|
-
|
|
31974
|
+
const liveKey = token?.live_secret_key ?? ctx.env.CHECKBOX_LIVE_SECRET_KEY ?? d.env.CHECKBOX_LIVE_SECRET_KEY ?? void 0;
|
|
31975
|
+
if (!liveKey || !/^ck_live_/.test(liveKey)) {
|
|
31976
|
+
const ask3 = liveKeyNextAction(session.review_url.replace(/\/setup\/[^/]*$/, ""));
|
|
31977
|
+
live.skipped = "no_live_key";
|
|
31978
|
+
live.next_action = ask3;
|
|
31979
|
+
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
31980
|
} else {
|
|
30969
31981
|
try {
|
|
30970
31982
|
const lc = ctx.client({ apiKey: liveKey, apiBase });
|
|
@@ -30978,7 +31990,7 @@ var init_init = __esm({
|
|
|
30978
31990
|
changes.push(wk);
|
|
30979
31991
|
}
|
|
30980
31992
|
}
|
|
30981
|
-
session = await
|
|
31993
|
+
session = await lc.sdk.setupSessions.update(sessionId, { live_site: liveSite.id });
|
|
30982
31994
|
saveState(ctx, root, { live_site: liveSite.id });
|
|
30983
31995
|
live.site = liveSite.id;
|
|
30984
31996
|
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 +32003,8 @@ var init_init = __esm({
|
|
|
30991
32003
|
out.info("Running the first checks\u2026");
|
|
30992
32004
|
session = (await client.sdk.setupSessions.verify(sessionId)).setup_session;
|
|
30993
32005
|
}
|
|
30994
|
-
const doc =
|
|
32006
|
+
const doc = resultOf2(dryRun ? "dry_run" : "configured", d, session, changes, { stripe, live, warnings, manualSnippet, token, envNotWritten: envTracked && !!token }, apiBase);
|
|
32007
|
+
if (live.next_action && (session.next_action.type === "done" || session.next_action.type === "complete_setup")) doc.next_action = live.next_action;
|
|
30995
32008
|
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
32009
|
return EXIT.ok;
|
|
30997
32010
|
}
|
|
@@ -31175,6 +32188,7 @@ var init_plan = __esm({
|
|
|
31175
32188
|
init_args();
|
|
31176
32189
|
init_types2();
|
|
31177
32190
|
init_lib();
|
|
32191
|
+
init_fix();
|
|
31178
32192
|
plan_default = defineCommand({
|
|
31179
32193
|
name: "plan",
|
|
31180
32194
|
summary: "Show the compliance plan: ordered tasks to apply, then verify",
|
|
@@ -31195,7 +32209,15 @@ var init_plan = __esm({
|
|
|
31195
32209
|
const client = ctx.client();
|
|
31196
32210
|
const site = await resolveSite(ctx, str(flags, "site"), client);
|
|
31197
32211
|
const plan2 = await client.sdk.plans.retrieve(site);
|
|
31198
|
-
|
|
32212
|
+
const declared = declaredPlans(ctx.cwd);
|
|
32213
|
+
const staleOf = (t) => staleAgainstDeclared(`${t.change.content}
|
|
32214
|
+
${t.change.edits.map((e) => e.content ?? "").join("\n")}`, declared);
|
|
32215
|
+
const stale = plan2.tasks.filter((t) => (t.status === "open" || t.status === "in_progress") && staleOf(t)).map((t) => t.id);
|
|
32216
|
+
ctx.out.result(stale.length ? { ...plan2, stale_tasks: stale } : plan2, () => {
|
|
32217
|
+
const lines = renderPlan(plan2, ctx.out.c, { all: !!bool(flags, "all"), stale: staleOf });
|
|
32218
|
+
if (stale.length) lines.push("", ctx.out.c.yellow(`${stale.length} task(s) were built from the site's plans in Checkbox, which are older than checkbox.json: do not apply them. Once this change is deployed, update the site's plans (updateSetupSession plans[], or \`checkbox init --plan \u2026\`), then run \`checkbox plan\` again.`));
|
|
32219
|
+
return lines;
|
|
32220
|
+
});
|
|
31199
32221
|
return bool(flags, "exit-code") && plan2.status !== "compliant" ? EXIT.fail : EXIT.ok;
|
|
31200
32222
|
}
|
|
31201
32223
|
});
|