@checkbox-site/cli 0.4.7 → 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 +449 -125
- 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
|
|
|
@@ -4757,12 +4757,16 @@ var init_stripe_ids = __esm({
|
|
|
4757
4757
|
function stripeKeyPermissionLine(filter) {
|
|
4758
4758
|
return STRIPE_KEY_PERMISSIONS.filter(filter ?? (() => true)).map((p) => `${p.resource}: ${p.access}`).join(", ");
|
|
4759
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
|
+
}
|
|
4760
4764
|
function approvalKeyAsk(opts = {}) {
|
|
4761
4765
|
if (opts.stripe === false) return "";
|
|
4762
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)`;
|
|
4763
|
-
return `${opts.stripe ? "In the same message" : "If the site sells through Stripe, in the same message"}, ask them to tick
|
|
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.`;
|
|
4764
4768
|
}
|
|
4765
|
-
var STRIPE_KEY_PERMISSIONS, STRIPE_TEST_API_KEYS_URL;
|
|
4769
|
+
var STRIPE_KEY_PERMISSIONS, STRIPE_TEST_API_KEYS_URL, DEVICE_TEST_KEY_LABEL, OAUTH_TEST_KEY_LABEL, TEST_KEY_CHECKBOX_ID;
|
|
4766
4770
|
var init_stripe_key = __esm({
|
|
4767
4771
|
"../core/src/stripe-key.ts"() {
|
|
4768
4772
|
"use strict";
|
|
@@ -4779,6 +4783,9 @@ var init_stripe_key = __esm({
|
|
|
4779
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." }
|
|
4780
4784
|
];
|
|
4781
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";
|
|
4782
4789
|
}
|
|
4783
4790
|
});
|
|
4784
4791
|
|
|
@@ -4786,7 +4793,7 @@ var init_stripe_key = __esm({
|
|
|
4786
4793
|
function goLiveSteps(input = {}) {
|
|
4787
4794
|
const out = [];
|
|
4788
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." });
|
|
4789
|
-
if (input.termsOfServiceRequired && !input.termsOfServiceUrlSet) out.push({ key: "stripe_terms_url", who: "person", text: GO_LIVE_TERMS_URL_STEP, url: STRIPE_PUBLIC_DETAILS });
|
|
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 });
|
|
4790
4797
|
if (!input.liveKeyConnected)
|
|
4791
4798
|
out.push({
|
|
4792
4799
|
key: "live_stripe_key",
|
|
@@ -4804,12 +4811,13 @@ function goLiveSteps(input = {}) {
|
|
|
4804
4811
|
});
|
|
4805
4812
|
return out;
|
|
4806
4813
|
}
|
|
4807
|
-
var STRIPE_PUBLIC_DETAILS, GO_LIVE_TERMS_URL_STEP, GO_LIVE_STEPS;
|
|
4814
|
+
var STRIPE_PUBLIC_DETAILS, GO_LIVE_TERMS_URL_STEP, GO_LIVE_TERMS_URL_STEP_CONDITIONAL, GO_LIVE_STEPS;
|
|
4808
4815
|
var init_go_live = __esm({
|
|
4809
4816
|
"../core/src/go-live.ts"() {
|
|
4810
4817
|
"use strict";
|
|
4811
4818
|
STRIPE_PUBLIC_DETAILS = "https://dashboard.stripe.com/settings/public";
|
|
4812
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.';
|
|
4813
4821
|
GO_LIVE_STEPS = goLiveSteps().map((s2) => s2.text);
|
|
4814
4822
|
}
|
|
4815
4823
|
});
|
|
@@ -4853,6 +4861,13 @@ var init_secrets = __esm({
|
|
|
4853
4861
|
}
|
|
4854
4862
|
});
|
|
4855
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
|
+
|
|
4856
4871
|
// ../core/src/index.ts
|
|
4857
4872
|
var init_src2 = __esm({
|
|
4858
4873
|
"../core/src/index.ts"() {
|
|
@@ -4864,6 +4879,7 @@ var init_src2 = __esm({
|
|
|
4864
4879
|
init_stripe_key();
|
|
4865
4880
|
init_go_live();
|
|
4866
4881
|
init_secrets();
|
|
4882
|
+
init_site_unreachable();
|
|
4867
4883
|
}
|
|
4868
4884
|
});
|
|
4869
4885
|
|
|
@@ -5902,7 +5918,7 @@ var init_new_york = __esm({
|
|
|
5902
5918
|
NYC_EFFECTIVE = "2026-10-01";
|
|
5903
5919
|
NYC = "6 RCNY \xA7 5-110.1";
|
|
5904
5920
|
NYC_RULE_VERIFIED = "2026-10-31";
|
|
5905
|
-
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.";
|
|
5906
5922
|
PART_W_NOTE = "Current text from L. 2025, ch. 58, Part W, effective Nov. 5, 2025.";
|
|
5907
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.";
|
|
5908
5924
|
NEW_YORK_REQUIREMENTS = [
|
|
@@ -6351,12 +6367,12 @@ var init_new_york = __esm({
|
|
|
6351
6367
|
sourceUrl: NYC_RULE_URL,
|
|
6352
6368
|
effectiveDate: NYC_EFFECTIVE,
|
|
6353
6369
|
lastVerified: NYC_RULE_VERIFIED,
|
|
6354
|
-
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.",
|
|
6355
6371
|
status: "in_force",
|
|
6356
6372
|
confidence: "medium",
|
|
6357
6373
|
verification: "primary",
|
|
6358
6374
|
requirementIdPrefixes: ["US-NY.NYC."],
|
|
6359
|
-
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).",
|
|
6360
6376
|
amendments: [
|
|
6361
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).' }
|
|
6362
6378
|
]
|
|
@@ -10222,11 +10238,11 @@ function findRecurringPrices(text, amounts, freqs) {
|
|
|
10222
10238
|
let best = null;
|
|
10223
10239
|
for (const f of freqs) {
|
|
10224
10240
|
const after = f.index >= money3.index;
|
|
10225
|
-
const
|
|
10226
|
-
if (
|
|
10227
|
-
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;
|
|
10228
10244
|
if (SENTENCE_BREAK_RE.test(between(text, money3, f))) continue;
|
|
10229
|
-
if (!best ||
|
|
10245
|
+
if (!best || gap2 < best.gap) best = { f, gap: gap2 };
|
|
10230
10246
|
}
|
|
10231
10247
|
if (best) {
|
|
10232
10248
|
const start = Math.min(money3.index, best.f.index);
|
|
@@ -11505,10 +11521,10 @@ function withIntroOffers(declared, billing, offers) {
|
|
|
11505
11521
|
}
|
|
11506
11522
|
function shortPlanTerms(p) {
|
|
11507
11523
|
const digits = currencyDigitsOf(p.currency);
|
|
11508
|
-
const
|
|
11524
|
+
const major2 = p.amount / 10 ** digits;
|
|
11509
11525
|
const cur = (p.currency || "usd").toLowerCase();
|
|
11510
11526
|
const sym = { usd: "$", eur: "\u20AC", gbp: "\xA3", jpy: "\xA5" }[cur];
|
|
11511
|
-
const num3 = Number.isInteger(
|
|
11527
|
+
const num3 = Number.isInteger(major2) ? String(major2) : major2.toFixed(digits);
|
|
11512
11528
|
const money3 = sym ? `${sym}${num3}` : `${num3} ${cur.toUpperCase()}`;
|
|
11513
11529
|
const every = (p.intervalCount || 1) > 1 ? ` every ${p.intervalCount} ${p.interval}s` : `/${p.interval}`;
|
|
11514
11530
|
const trial = (p.trialDays ?? 0) > 0 ? ` with a ${p.trialDays}-day free trial` : "";
|
|
@@ -11778,11 +11794,11 @@ function formatMoney(amount, currency) {
|
|
|
11778
11794
|
} catch {
|
|
11779
11795
|
digits = 2;
|
|
11780
11796
|
}
|
|
11781
|
-
const
|
|
11797
|
+
const major2 = amount / 10 ** digits;
|
|
11782
11798
|
try {
|
|
11783
|
-
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);
|
|
11784
11800
|
} catch {
|
|
11785
|
-
return `${
|
|
11801
|
+
return `${major2.toFixed(digits)} ${code2}`;
|
|
11786
11802
|
}
|
|
11787
11803
|
}
|
|
11788
11804
|
function intervalPhrase(interval, count = 1) {
|
|
@@ -13405,7 +13421,7 @@ function checkoutConfigViews(ctx) {
|
|
|
13405
13421
|
return (ctx.checkoutConfigs ?? []).filter((c) => c && c.capture).map((c) => ({
|
|
13406
13422
|
...buildSignupView(c.capture),
|
|
13407
13423
|
label: `${c.kind === "payment_link" ? "Stripe Payment Link" : "Stripe Checkout config"} \u201C${c.name ?? c.id}\u201D`,
|
|
13408
|
-
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 } : {} },
|
|
13409
13425
|
// A Payment Link is read from Stripe on every billing read: it shows the link as of that read.
|
|
13410
13426
|
...c.kind === "payment_link" && readAt ? { observedAt: readAt } : {}
|
|
13411
13427
|
}));
|
|
@@ -13873,7 +13889,7 @@ function platformKey(platform) {
|
|
|
13873
13889
|
}
|
|
13874
13890
|
function portalUnlinkedFix(portalUrl) {
|
|
13875
13891
|
const login = portalUrl && /^https:\/\/billing\.stripe\.com\/p\/login\//.test(portalUrl) ? portalUrl : "billing.stripe.com/p/login/\u2026";
|
|
13876
|
-
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.`;
|
|
13877
13893
|
}
|
|
13878
13894
|
function paymentLinkCancelRemediation(portalUrl, loginPageOn) {
|
|
13879
13895
|
const link = portalUrl && /^https:\/\/billing\.stripe\.com\//.test(portalUrl) ? portalUrl : void 0;
|
|
@@ -14979,6 +14995,18 @@ var init_help_pages = __esm({
|
|
|
14979
14995
|
});
|
|
14980
14996
|
|
|
14981
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
|
+
}
|
|
14982
15010
|
function inAppPath(input) {
|
|
14983
15011
|
const flow = input.ctx.cancellation?.via === "stripe_portal" ? void 0 : input.ctx.cancellation;
|
|
14984
15012
|
if (flow) {
|
|
@@ -15014,7 +15042,7 @@ function inAppPath(input) {
|
|
|
15014
15042
|
return true;
|
|
15015
15043
|
}
|
|
15016
15044
|
});
|
|
15017
|
-
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 } : {} };
|
|
15018
15046
|
}
|
|
15019
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" };
|
|
15020
15048
|
return void 0;
|
|
@@ -15692,6 +15720,16 @@ var init_cancellation = __esm({
|
|
|
15692
15720
|
{ unverified: true, source: "snippet" }
|
|
15693
15721
|
);
|
|
15694
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
|
+
}
|
|
15695
15733
|
if (controls.length) {
|
|
15696
15734
|
return makeResult(
|
|
15697
15735
|
id,
|
|
@@ -16115,6 +16153,16 @@ var init_config2 = __esm({
|
|
|
16115
16153
|
}
|
|
16116
16154
|
if (c.selfServeOnline === false) {
|
|
16117
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
|
+
}
|
|
16118
16166
|
if (model.otherOnlineWorks) {
|
|
16119
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`;
|
|
16120
16168
|
return makeResult(
|
|
@@ -16320,8 +16368,8 @@ function introEmailFinding(rawText, plans, opts) {
|
|
|
16320
16368
|
const usable = plans.filter((p2) => p2.amount > 0 && p2.interval);
|
|
16321
16369
|
if (!usable.length) return void 0;
|
|
16322
16370
|
const text = normalizeText(rawText);
|
|
16323
|
-
const
|
|
16324
|
-
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 ?? []];
|
|
16325
16373
|
const isRegular = (n) => regular.some((r) => Math.abs(r - n) < 5e-3);
|
|
16326
16374
|
const offRegular = (sentence) => findAmounts(sentence).filter((m) => {
|
|
16327
16375
|
if (typeof m.amount !== "number" || !(m.amount > 0) || isRegular(m.amount)) return false;
|
|
@@ -16368,7 +16416,7 @@ function introEmailFinding(rawText, plans, opts) {
|
|
|
16368
16416
|
};
|
|
16369
16417
|
}
|
|
16370
16418
|
const fits = (p2) => {
|
|
16371
|
-
const intro =
|
|
16419
|
+
const intro = major2(p2, p2.introAmount);
|
|
16372
16420
|
const pct = (1 - p2.introAmount / p2.amount) * 100;
|
|
16373
16421
|
return statedAmounts.every((m) => Math.abs(m.amount - intro) < 5e-3) && percents.every((x) => Math.abs(x.value - pct) <= 0.5);
|
|
16374
16422
|
};
|
|
@@ -16397,6 +16445,7 @@ function introEmailFinding(rawText, plans, opts) {
|
|
|
16397
16445
|
fix: `State how long the promo really lasts: ${introTermsPhrase(p2)}.`
|
|
16398
16446
|
};
|
|
16399
16447
|
}
|
|
16448
|
+
if (statesIntroEndDate(text)) return void 0;
|
|
16400
16449
|
const p = fitting[0];
|
|
16401
16450
|
return {
|
|
16402
16451
|
status: "needs_review",
|
|
@@ -16406,29 +16455,32 @@ function introEmailFinding(rawText, plans, opts) {
|
|
|
16406
16455
|
fix: `State how long the promo lasts and the price after it: ${introTermsPhrase(p)}.`
|
|
16407
16456
|
};
|
|
16408
16457
|
}
|
|
16458
|
+
function statesIntroEndDate(text) {
|
|
16459
|
+
return INTRO_END_DATE_RES.some((re) => re.test(text));
|
|
16460
|
+
}
|
|
16409
16461
|
function introFirstChargeFinding(rawText, plans) {
|
|
16410
16462
|
const usable = plans.filter((p2) => p2.amount > 0 && p2.interval);
|
|
16411
16463
|
if (!usable.length) return void 0;
|
|
16412
16464
|
const text = normalizeText(rawText);
|
|
16413
16465
|
const stated = findAmounts(text).filter((m) => typeof m.amount === "number" && m.amount > 0);
|
|
16414
|
-
const
|
|
16466
|
+
const major2 = (p2, minor) => minor / 10 ** currencyDigitsOf(p2.currency);
|
|
16415
16467
|
const states = (n) => stated.some((m) => Math.abs(m.amount - n) < 5e-3);
|
|
16416
|
-
const named = usable.filter((p2) => states(
|
|
16468
|
+
const named = usable.filter((p2) => states(major2(p2, p2.amount)));
|
|
16417
16469
|
if (!named.length || !named.every(hasIntro)) return void 0;
|
|
16418
|
-
if (named.some((p2) => states(
|
|
16470
|
+
if (named.some((p2) => states(major2(p2, p2.introAmount)))) return void 0;
|
|
16419
16471
|
if (new RegExp(PERCENT_OFF_RE.source, "i").test(text)) return void 0;
|
|
16420
16472
|
const p = named[0];
|
|
16421
16473
|
const regular = money(p.amount, p.currency);
|
|
16422
16474
|
const nameOf = p.label?.trim() || (p.name?.trim() ? `plan \u201C${p.name.trim()}\u201D` : `the ${regular} ${planEvery(p)} plan`);
|
|
16423
16475
|
return {
|
|
16424
16476
|
status: "fail",
|
|
16425
|
-
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,
|
|
16426
16478
|
reason: "intro_first_charge",
|
|
16427
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`,
|
|
16428
16480
|
fix: `State the first charge after the trial and the price after it: ${introTermsPhrase(p)}.`
|
|
16429
16481
|
};
|
|
16430
16482
|
}
|
|
16431
|
-
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;
|
|
16432
16484
|
var init_intro_terms = __esm({
|
|
16433
16485
|
"../rules/src/checks/intro-terms.ts"() {
|
|
16434
16486
|
"use strict";
|
|
@@ -16444,6 +16496,15 @@ var init_intro_terms = __esm({
|
|
|
16444
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;
|
|
16445
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;
|
|
16446
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
|
+
];
|
|
16447
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;
|
|
16448
16509
|
}
|
|
16449
16510
|
});
|
|
@@ -16920,7 +16981,7 @@ function withoutRevisedDrafts(emails) {
|
|
|
16920
16981
|
const draft2 = (e) => !e.subscriptionRef && !e.relatedSignupAt && !e.testRun && !isCheckboxSentEmail(e);
|
|
16921
16982
|
const trial = (e) => /\b(?:free\s+)?trial\b/i.test(`${e.subject ?? ""} ${emailBody(e)}`);
|
|
16922
16983
|
const at = (e) => toDate(e.receivedAt)?.getTime() ?? 0;
|
|
16923
|
-
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))));
|
|
16924
16985
|
}
|
|
16925
16986
|
function latestPerTemplateAndPlan(emails, input) {
|
|
16926
16987
|
const offers = contextOffers(input.ctx);
|
|
@@ -17833,29 +17894,29 @@ function contentGap(required, emails, opts = {}) {
|
|
|
17833
17894
|
}
|
|
17834
17895
|
return null;
|
|
17835
17896
|
}
|
|
17836
|
-
function contentFail(spec, required,
|
|
17837
|
-
const intro =
|
|
17838
|
-
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`;
|
|
17839
17900
|
const introData = intro ? { contentIntro: { status: intro.status, reason: intro.reason, quote: intro.quote, fix: intro.fix } } : {};
|
|
17840
|
-
if (!
|
|
17901
|
+
if (!gap2.missing.length && intro) {
|
|
17841
17902
|
return makeResult(
|
|
17842
17903
|
spec.checkId,
|
|
17843
17904
|
intro.status,
|
|
17844
|
-
`The ${spec.noun} (${emailLabel(
|
|
17845
|
-
[...emailRefs(
|
|
17905
|
+
`The ${spec.noun} (${emailLabel(gap2.email)}) ${sentWhat} ${intro.detail}`,
|
|
17906
|
+
[...emailRefs(gap2.email), ...refs2],
|
|
17846
17907
|
intro.status === "fail" ? 0.8 : 0.5,
|
|
17847
|
-
{ ...data, contentMissing: [], contentEmailId:
|
|
17908
|
+
{ ...data, contentMissing: [], contentEmailId: gap2.email.id, ...isCheckboxSentEmail(gap2.email) ? { contentByCheckbox: true } : {}, ...introData }
|
|
17848
17909
|
);
|
|
17849
17910
|
}
|
|
17850
17911
|
return makeResult(
|
|
17851
17912
|
spec.checkId,
|
|
17852
17913
|
"fail",
|
|
17853
|
-
`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
|
|
17854
17915
|
// element says its own reason.
|
|
17855
|
-
|
|
17856
|
-
[...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],
|
|
17857
17918
|
0.8,
|
|
17858
|
-
{ ...data, contentMissing:
|
|
17919
|
+
{ ...data, contentMissing: gap2.missing, contentEmailId: gap2.email.id, ...isCheckboxSentEmail(gap2.email) ? { contentByCheckbox: true } : {}, ...gap2.wrong ? { contentWrong: gap2.wrong } : {}, ...introData }
|
|
17859
17920
|
);
|
|
17860
17921
|
}
|
|
17861
17922
|
async function runReminder(spec, input) {
|
|
@@ -17955,8 +18016,8 @@ async function runReminder(spec, input) {
|
|
|
17955
18016
|
const w = conflicting ? "within each state\u2019s window" : why(window);
|
|
17956
18017
|
if (counted.length) {
|
|
17957
18018
|
if (!outside.length && !missing.length && !partial.length) {
|
|
17958
|
-
const
|
|
17959
|
-
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);
|
|
17960
18021
|
return makeResult(
|
|
17961
18022
|
id,
|
|
17962
18023
|
"pass",
|
|
@@ -18047,8 +18108,8 @@ async function runReminder(spec, input) {
|
|
|
18047
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);
|
|
18048
18109
|
}
|
|
18049
18110
|
if (inWindow.length) {
|
|
18050
|
-
const
|
|
18051
|
-
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);
|
|
18052
18113
|
return makeResult(
|
|
18053
18114
|
id,
|
|
18054
18115
|
"pass",
|
|
@@ -18060,8 +18121,8 @@ async function runReminder(spec, input) {
|
|
|
18060
18121
|
}
|
|
18061
18122
|
if (nearWindow.length) {
|
|
18062
18123
|
const n = nearWindow[0];
|
|
18063
|
-
const
|
|
18064
|
-
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);
|
|
18065
18126
|
data.timingNearWindow = { days: n.days, required: conflicting ? w : why(n.w), roundingRule: DAYS_ROUNDING_RULE, ...n.range ? { range: n.range } : {} };
|
|
18066
18127
|
data.timingOutsideWindow = { days: n.days, required: conflicting ? w : why(n.w) };
|
|
18067
18128
|
return makeResult(
|
|
@@ -19357,8 +19418,8 @@ function gatedByOwnBox(renewalBoxes, path) {
|
|
|
19357
19418
|
return renewalBoxes.some((b) => b.required === true && !b.checkedByDefault && consentGate(b, path) === "gated");
|
|
19358
19419
|
}
|
|
19359
19420
|
function boxNearCta(box, cta) {
|
|
19360
|
-
const
|
|
19361
|
-
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;
|
|
19362
19423
|
}
|
|
19363
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;
|
|
19364
19425
|
var init_signup = __esm({
|
|
@@ -19863,8 +19924,18 @@ var init_signup = __esm({
|
|
|
19863
19924
|
const offers = contextOffers(input.ctx);
|
|
19864
19925
|
const loose = !view.plan || view.plan.how === "only_plan";
|
|
19865
19926
|
const cardOffer = loose && (view.offer || view.checkoutConfig) && offers.length > 1 ? offerForView(view, offers) : void 0;
|
|
19866
|
-
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;
|
|
19867
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
|
+
}
|
|
19868
19939
|
if (!mentioned) {
|
|
19869
19940
|
if (planTrial === 0 && offers.some((o) => o.trialDays > 0)) {
|
|
19870
19941
|
const withTrial = offers.filter((o) => o.trialDays > 0).map(offerName);
|
|
@@ -19889,7 +19960,7 @@ var init_signup = __esm({
|
|
|
19889
19960
|
const meta = {};
|
|
19890
19961
|
if (length && "days" in length && length.days !== null) meta.disclosedTrialDays = length.days;
|
|
19891
19962
|
if (input.facts.trialDays) meta.configuredTrialDays = input.facts.trialDays;
|
|
19892
|
-
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;
|
|
19893
19964
|
const trialPlans = [
|
|
19894
19965
|
...(input.ctx.config?.plans ?? []).map((pl) => ({ trialDays: pl.trialDays, source: configPlanSource(input.ctx) })),
|
|
19895
19966
|
...(input.ctx.referencePlans ?? []).map((pl) => ({ trialDays: pl.trialDays, source: pl.source }))
|
|
@@ -22199,6 +22270,24 @@ var init_brand_scope = __esm({
|
|
|
22199
22270
|
}
|
|
22200
22271
|
});
|
|
22201
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
|
+
|
|
22202
22291
|
// ../rules/src/status.ts
|
|
22203
22292
|
function tierOf(checkId) {
|
|
22204
22293
|
return CHECK_TIERS[checkId] ?? "required";
|
|
@@ -22295,10 +22384,18 @@ function cancelTestProblem(raw) {
|
|
|
22295
22384
|
}
|
|
22296
22385
|
function policyUnreadableFix(text) {
|
|
22297
22386
|
const n = /\((\d+) characters/.exec(text)?.[1];
|
|
22298
|
-
|
|
22299
|
-
|
|
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.`;
|
|
22394
|
+
}
|
|
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.`;
|
|
22300
22397
|
}
|
|
22301
|
-
return
|
|
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.`;
|
|
22302
22399
|
}
|
|
22303
22400
|
function finish(base, reason, checkId, tier = tierOf(checkId)) {
|
|
22304
22401
|
if (reason === "ny_owner_decision") return { ...base, label: NY_EMAIL_ONLY_LABEL, reason, tier };
|
|
@@ -22533,6 +22630,7 @@ var STATUS_LABELS, CHECK_TIERS, LIVE_CHECKS, has, isStripeInput, HOSTED_BEFORE_L
|
|
|
22533
22630
|
var init_status = __esm({
|
|
22534
22631
|
"../rules/src/status.ts"() {
|
|
22535
22632
|
"use strict";
|
|
22633
|
+
init_set_off_fix();
|
|
22536
22634
|
init_notice_content();
|
|
22537
22635
|
init_notices_on();
|
|
22538
22636
|
init_plan_terms();
|
|
@@ -22821,17 +22919,30 @@ var init_status = __esm({
|
|
|
22821
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." }) },
|
|
22822
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." }) },
|
|
22823
22921
|
// Normal round 2 (nextjs, django): "looks like the surrounding text" is a measurement Checkbox cannot finish (borders,
|
|
22824
|
-
// 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.
|
|
22825
22929
|
{
|
|
22826
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",
|
|
22827
22937
|
category: "legal_choice",
|
|
22828
|
-
when:
|
|
22829
|
-
build: () => ({
|
|
22938
|
+
when: (c) => c.kind === "disclosure" && /look like the surrounding text/.test(c.text),
|
|
22939
|
+
build: (c) => ({
|
|
22830
22940
|
status: "your_call",
|
|
22941
|
+
fix: setOffFix(c.text),
|
|
22831
22942
|
question: ask(
|
|
22832
|
-
"Confirm by eye: does the renewal line stand out from the text around it (semibold, a
|
|
22833
|
-
"
|
|
22834
|
-
"
|
|
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."
|
|
22835
22946
|
)
|
|
22836
22947
|
})
|
|
22837
22948
|
},
|
|
@@ -23179,7 +23290,7 @@ var init_status = __esm({
|
|
|
23179
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) }) }
|
|
23180
23291
|
];
|
|
23181
23292
|
STATUS_RULES = RULES.map((r) => ({ code: r.code, category: r.category }));
|
|
23182
|
-
NY_EMAIL_ONLY_LABEL = "Your decision: email only";
|
|
23293
|
+
NY_EMAIL_ONLY_LABEL = "Your decision: email only (not verified)";
|
|
23183
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.';
|
|
23184
23295
|
STRIPE_HOSTED_UNITS = /* @__PURE__ */ new Set(["checkout_config", "payment_link", "stripe_hosted"]);
|
|
23185
23296
|
PLATFORM_NAMES2 = { paddle: "Paddle", shopify: "Shopify", recharge: "Recharge", chargebee: "Chargebee", recurly: "Recurly" };
|
|
@@ -23981,12 +24092,12 @@ function disclosureParts(text) {
|
|
|
23981
24092
|
function disclosureHtml(text) {
|
|
23982
24093
|
return disclosureParts(text).map((p) => "url" in p ? `<a href="${esc(p.url)}">${esc(p.label)}</a>` : esc(p.text)).join("");
|
|
23983
24094
|
}
|
|
23984
|
-
function htmlDisclosureComponent(texts, sha2562) {
|
|
24095
|
+
function htmlDisclosureComponent(texts, sha2562, opts = {}) {
|
|
23985
24096
|
return `<!-- Checkbox renewal terms (generated from your plan's price, interval and cancellation URL; regenerate instead of editing the text).
|
|
23986
24097
|
Paste INSIDE your sign-up <form>, directly ABOVE the subscribe / purchase button.
|
|
23987
|
-
One line in your page's own font, size and colours: semibold sets it apart from normal-weight text (clear and conspicuous).
|
|
23988
|
-
If the text around it is already bold (a bold price line), put it in a bordered or shaded box instead
|
|
23989
|
-
<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 : ""}">
|
|
23990
24101
|
<p style="margin:0 0 6px;font-weight:600;">${disclosureHtml(texts.disclosure)}</p>
|
|
23991
24102
|
<label style="display:flex;gap:8px;align-items:flex-start;">
|
|
23992
24103
|
<!-- Never add "checked": consent must be an affirmative act. -->
|
|
@@ -23996,23 +24107,26 @@ function htmlDisclosureComponent(texts, sha2562) {
|
|
|
23996
24107
|
</div>
|
|
23997
24108
|
<button type="submit">${esc(texts.cta)}</button>`;
|
|
23998
24109
|
}
|
|
23999
|
-
function htmlTermsOnly(disclosure, sha2562, onStripe) {
|
|
24110
|
+
function htmlTermsOnly(disclosure, sha2562, onStripe, opts = {}) {
|
|
24000
24111
|
const where2 = onStripe === "payment_link" ? "Payment Link" : "Stripe Checkout";
|
|
24001
24112
|
return `<!-- Checkbox renewal terms (generated from your plan's price, interval and cancellation URL; regenerate instead of editing the text).
|
|
24002
24113
|
Paste directly ABOVE your own subscribe / Join link or button. Keep that link or button as it is.
|
|
24003
24114
|
No consent checkbox here: the ${where2} collects consent (consent_collection.terms_of_service = "required").
|
|
24004
|
-
One line in your page's own font, size and colours: semibold sets it apart from normal-weight text (clear and conspicuous).
|
|
24005
|
-
If the text around it is already bold (a bold price line), put it in a bordered or shaded box instead
|
|
24006
|
-
<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 : ""}">
|
|
24007
24118
|
<p style="margin:0;font-weight:600;">${disclosureHtml(disclosure)}</p>
|
|
24008
24119
|
</div>`;
|
|
24009
24120
|
}
|
|
24010
|
-
var esc, URL_RE2;
|
|
24121
|
+
var esc, URL_RE2, BOXED_STYLE, BOXED_NOTE;
|
|
24011
24122
|
var init_html = __esm({
|
|
24012
24123
|
"../rules/src/fixes/html.ts"() {
|
|
24013
24124
|
"use strict";
|
|
24014
24125
|
esc = (s2) => s2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
24015
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.`;
|
|
24016
24130
|
}
|
|
24017
24131
|
});
|
|
24018
24132
|
|
|
@@ -24095,7 +24209,7 @@ function modelAfterCapture(before, target, texts, opts) {
|
|
|
24095
24209
|
const ctaY = cta ? cta.rect.y : 700;
|
|
24096
24210
|
const ctaH = cta ? cta.rect.height : 48;
|
|
24097
24211
|
const stripe = target === "stripe_checkout" || target === "stripe_payment_link";
|
|
24098
|
-
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;
|
|
24099
24213
|
const disclosureH = textHeight(texts.disclosure, width, style2.fontSizePx);
|
|
24100
24214
|
const labelH = textHeight(texts.consentLabel, width - 28, style2.fontSizePx);
|
|
24101
24215
|
const disclosure = {
|
|
@@ -24227,7 +24341,7 @@ async function verifyFix(input) {
|
|
|
24227
24341
|
const rest = captures.filter((c) => !allCheckout(c));
|
|
24228
24342
|
const unchangedCheckout = latestPages(captures.filter((c) => allCheckout(c) && !matching.some((m) => m.id === c.id))).filter((c) => (c.ctas?.length ?? 0) > 0);
|
|
24229
24343
|
const otherCheckoutPages = new Set(unchangedCheckout.map((c) => c.url.split(/[?#]/)[0])).size;
|
|
24230
|
-
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 } : {} }));
|
|
24231
24345
|
const changedIds = new Set(objects.map((o) => o.id));
|
|
24232
24346
|
const afterConfigs = input.ctx.checkoutConfigs?.map(
|
|
24233
24347
|
(c) => changedIds.has(c.id) ? { ...c, capture: modelAfterCapture(c.capture, input.target, input.texts, { consentCheckbox: consent }), consentRequired: consent || c.consentRequired } : c
|
|
@@ -24492,13 +24606,18 @@ async function buildReplacement(plan2, jurisdictions, asOf, now, opts = {}) {
|
|
|
24492
24606
|
const verifyTexts = lockedTexts(verifyTerms);
|
|
24493
24607
|
const page = opts.onStripe && opts.page && (opts.page.ctas?.length ?? 0) > 0 ? [{ ...opts.page, pageKind: "checkout", source: "scanner" }] : [];
|
|
24494
24608
|
const ctx = modelContext(jurisdictions, asOf, planFacts(plan2), { config: planConfig(plan2, asOf), ...page.length ? { captures: page } : {}, ...plan2.merchantName ? { merchantNames: [plan2.merchantName] } : {} });
|
|
24495
|
-
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;
|
|
24496
24615
|
const sha = textsSha256(shown);
|
|
24497
24616
|
const placeholders = plan2.cancelUrl ? [] : [CANCEL_URL_PLACEHOLDER];
|
|
24498
24617
|
let short;
|
|
24499
24618
|
if (plan2.cancelUrl) {
|
|
24500
24619
|
const shortText = shortDisclosureText(verifyTerms);
|
|
24501
|
-
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 } : {} });
|
|
24502
24621
|
if (sv.passes && v.passes && shortText !== shown.disclosure) short = shortText;
|
|
24503
24622
|
}
|
|
24504
24623
|
if (opts.onStripe) {
|
|
@@ -24509,9 +24628,9 @@ async function buildReplacement(plan2, jurisdictions, asOf, now, opts = {}) {
|
|
|
24509
24628
|
...short ? { short_disclosure_text: short } : {},
|
|
24510
24629
|
consent_label: shown.consentLabel,
|
|
24511
24630
|
button_text: shown.cta,
|
|
24512
|
-
html: htmlTermsOnly(shown.disclosure, sha, opts.onStripe),
|
|
24631
|
+
html: htmlTermsOnly(shown.disclosure, sha, opts.onStripe, boxed ? { boxed: true } : {}),
|
|
24513
24632
|
consent_on: opts.onStripe,
|
|
24514
|
-
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)." : ""}`,
|
|
24515
24634
|
placeholders,
|
|
24516
24635
|
verified: v.passes,
|
|
24517
24636
|
remaining_checks: [.../* @__PURE__ */ new Set([...v.remaining, "signup.consent.affirmative"])],
|
|
@@ -24523,7 +24642,7 @@ async function buildReplacement(plan2, jurisdictions, asOf, now, opts = {}) {
|
|
|
24523
24642
|
...short ? { short_disclosure_text: short } : {},
|
|
24524
24643
|
consent_label: shown.consentLabel,
|
|
24525
24644
|
button_text: shown.cta,
|
|
24526
|
-
html: htmlDisclosureComponent(shown, sha),
|
|
24645
|
+
html: htmlDisclosureComponent(shown, sha, boxed ? { boxed: true } : {}),
|
|
24527
24646
|
placeholders,
|
|
24528
24647
|
verified: v.passes,
|
|
24529
24648
|
remaining_checks: v.remaining,
|
|
@@ -24814,6 +24933,17 @@ var init_price_hint = __esm({
|
|
|
24814
24933
|
});
|
|
24815
24934
|
|
|
24816
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
|
+
}
|
|
24817
24947
|
function clone(v) {
|
|
24818
24948
|
return JSON.parse(JSON.stringify(v));
|
|
24819
24949
|
}
|
|
@@ -24905,6 +25035,25 @@ function readParams(params, merchant, kind, asOf) {
|
|
|
24905
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)) {
|
|
24906
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" });
|
|
24907
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
|
+
}
|
|
24908
25057
|
const uiMode = typeof params.ui_mode === "string" ? params.ui_mode : void 0;
|
|
24909
25058
|
if (uiMode === "custom" || uiMode === "elements") {
|
|
24910
25059
|
caveats.push({
|
|
@@ -25182,6 +25331,7 @@ function correct(params, shape, checks, forLink, style2 = "full") {
|
|
|
25182
25331
|
out.custom_text = set;
|
|
25183
25332
|
} else {
|
|
25184
25333
|
for (const [k, v] of Object.entries(set)) {
|
|
25334
|
+
if (JSON.stringify(ct[k]) === JSON.stringify(v)) continue;
|
|
25185
25335
|
patch.push({ op: k in ct && ct[k] !== null ? "replace" : "add", path: `/custom_text/${k}`, value: v });
|
|
25186
25336
|
ct[k] = v;
|
|
25187
25337
|
}
|
|
@@ -25449,7 +25599,10 @@ function modelCheckoutConfig(params, opts) {
|
|
|
25449
25599
|
// travels with its plan, so the site's checks and copy know the promo without a separate declaration.
|
|
25450
25600
|
...plan2.introAmount !== void 0 && plan2.introAmount >= 0 && plan2.introAmount < plan2.amount ? { introAmount: plan2.introAmount, ...plan2.introPeriods && plan2.introPeriods > 1 ? { introPeriods: plan2.introPeriods } : {} } : {}
|
|
25451
25601
|
},
|
|
25452
|
-
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
|
|
25453
25606
|
};
|
|
25454
25607
|
}
|
|
25455
25608
|
function noticeChannelField(params) {
|
|
@@ -25476,6 +25629,7 @@ var init_stripe2 = __esm({
|
|
|
25476
25629
|
init_compact();
|
|
25477
25630
|
init_plan_terms();
|
|
25478
25631
|
init_text();
|
|
25632
|
+
init_signup();
|
|
25479
25633
|
init_verify();
|
|
25480
25634
|
init_stripe();
|
|
25481
25635
|
init_common();
|
|
@@ -26183,7 +26337,8 @@ async function checkPageHtml(input, opts = {}) {
|
|
|
26183
26337
|
});
|
|
26184
26338
|
}
|
|
26185
26339
|
const consentOnStripe = onStripe ?? (checkoutType === "payment_link" || checkoutType === "stripe_checkout" ? checkoutType : checkoutType ? void 0 : stripeButtonsKind(capture));
|
|
26186
|
-
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 } : {} };
|
|
26187
26342
|
const built = plan2 ? await buildReplacement(plan2, jurisdictions, asOf, now, { ...repOpts, page: capture }) : void 0;
|
|
26188
26343
|
const differs = plan2 && !multi ? pagePriceDiffers(run2.findings) : [];
|
|
26189
26344
|
if (plan2 && differs.length) {
|
|
@@ -26266,7 +26421,8 @@ function signupStripeObjects(input) {
|
|
|
26266
26421
|
capture: { ...m.capture, siteId: input.siteId },
|
|
26267
26422
|
consentRequired: m.consentRequired,
|
|
26268
26423
|
kind: "checkout_config",
|
|
26269
|
-
...m.plan ? { planId: m.plan.id } : {}
|
|
26424
|
+
...m.plan ? { planId: m.plan.id } : {},
|
|
26425
|
+
...typeof m.trialDays === "number" ? { trialDays: m.trialDays } : {}
|
|
26270
26426
|
});
|
|
26271
26427
|
}
|
|
26272
26428
|
if (m.plan) referencePlans2.push({ ...m.plan, source: "checkout_config", ...row.name ? { label: row.name } : {} });
|
|
@@ -26299,7 +26455,8 @@ function signupStripeObjects(input) {
|
|
|
26299
26455
|
consentRequired: m.consentRequired,
|
|
26300
26456
|
kind: "payment_link",
|
|
26301
26457
|
...link.url ? { url: link.url } : {},
|
|
26302
|
-
...m.plan ? { planId: m.plan.id } : {}
|
|
26458
|
+
...m.plan ? { planId: m.plan.id } : {},
|
|
26459
|
+
...typeof m.trialDays === "number" ? { trialDays: m.trialDays } : {}
|
|
26303
26460
|
});
|
|
26304
26461
|
}
|
|
26305
26462
|
if (m.plan) referencePlans2.push({ ...m.plan, source: "checkout_config", label: `Payment Link \u201C${name}\u201D` });
|
|
@@ -26358,6 +26515,7 @@ __export(preflight_exports, {
|
|
|
26358
26515
|
stripeParamsVerdict: () => stripeParamsVerdict,
|
|
26359
26516
|
supportContactLine: () => supportContactLine,
|
|
26360
26517
|
testRunSessionIds: () => testRunSessionIds,
|
|
26518
|
+
trialPromise: () => trialPromise,
|
|
26361
26519
|
withIntroOffers: () => withIntroOffers,
|
|
26362
26520
|
withTestRunSessions: () => withTestRunSessions
|
|
26363
26521
|
});
|
|
@@ -27005,7 +27163,7 @@ function parseObjectLiteral(source, env2 = {}, consts = {}) {
|
|
|
27005
27163
|
const p = new Parser(source, env2, consts);
|
|
27006
27164
|
if (p.peek() !== "{") return { value: null, unresolved: ["<not an object literal>"] };
|
|
27007
27165
|
const v = p.object("");
|
|
27008
|
-
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 } : {} };
|
|
27009
27167
|
}
|
|
27010
27168
|
var UNKNOWN, Parser;
|
|
27011
27169
|
var init_literal = __esm({
|
|
@@ -27021,6 +27179,7 @@ var init_literal = __esm({
|
|
|
27021
27179
|
i = 0;
|
|
27022
27180
|
unresolved = [];
|
|
27023
27181
|
fromEnv = {};
|
|
27182
|
+
spreads = [];
|
|
27024
27183
|
ws() {
|
|
27025
27184
|
for (; ; ) {
|
|
27026
27185
|
while (this.i < this.s.length && /\s/.test(this.s[this.i])) this.i++;
|
|
@@ -27157,7 +27316,9 @@ var init_literal = __esm({
|
|
|
27157
27316
|
if (!ch) return UNKNOWN;
|
|
27158
27317
|
if (this.s.startsWith("...", this.i)) {
|
|
27159
27318
|
this.i += 3;
|
|
27319
|
+
const from = this.i;
|
|
27160
27320
|
this.skipExpr();
|
|
27321
|
+
if (!path) this.spreads.push(this.s.slice(from, this.i).trim());
|
|
27161
27322
|
this.unresolved.push(`${path ? `${path}.` : ""}...`);
|
|
27162
27323
|
} else {
|
|
27163
27324
|
let key = null;
|
|
@@ -27756,8 +27917,8 @@ function plansFromLiterals(text, file) {
|
|
|
27756
27917
|
if (!interval) continue;
|
|
27757
27918
|
const price3 = /\bprice\s*:\s*(\d+(?:\.\d{1,2})?)\b/.exec(body)?.[1];
|
|
27758
27919
|
const minor = /\b(?:amount|unit_amount|price_cents|cents)\s*:\s*(\d+)\b/.exec(body)?.[1];
|
|
27759
|
-
const
|
|
27760
|
-
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;
|
|
27761
27922
|
const name = /\b(?:name|title|label)\s*:\s*['"]([^'"]{1,80})['"]/.exec(body)?.[1] ?? m[1]?.trim();
|
|
27762
27923
|
if (!name) continue;
|
|
27763
27924
|
const currency = /\bcurrency\s*:\s*['"]([a-z]{3})['"]/i.exec(body)?.[1]?.toLowerCase() ?? "usd";
|
|
@@ -27766,7 +27927,7 @@ function plansFromLiterals(text, file) {
|
|
|
27766
27927
|
if (out.some((p) => p.name === name)) continue;
|
|
27767
27928
|
const key = m[1]?.trim() && /^[A-Za-z_$][\w$]*$/.test(m[1].trim()) ? m[1].trim() : void 0;
|
|
27768
27929
|
const ids = [...new Set([...body.matchAll(/['"`](price_[A-Za-z0-9_]+)['"`]/g)].map((x) => x[1]))];
|
|
27769
|
-
out.push({ name, ...key ? { key } : {}, price:
|
|
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 });
|
|
27770
27931
|
}
|
|
27771
27932
|
return out;
|
|
27772
27933
|
}
|
|
@@ -28304,8 +28465,8 @@ function loaderFlags(file, dir, nodeVersion = process.versions.node, features =
|
|
|
28304
28465
|
if (hasTsx(dir)) return ["--import", "tsx"];
|
|
28305
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`;
|
|
28306
28467
|
if (features.typescript) return [];
|
|
28307
|
-
const [
|
|
28308
|
-
if (
|
|
28468
|
+
const [major2, minor] = nodeVersion.split(".").map(Number);
|
|
28469
|
+
if (major2 > 22 || major2 === 22 && minor >= 6) return ["--experimental-strip-types"];
|
|
28309
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`;
|
|
28310
28471
|
}
|
|
28311
28472
|
function runModule(dir, ref, args = [], env2 = process.env, timeoutMs = 3e4) {
|
|
@@ -28375,7 +28536,23 @@ function paramsFromSource(text, file, call = 1, env2 = {}, consts = {}) {
|
|
|
28375
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`);
|
|
28376
28537
|
const parsed = parseObjectLiteral(lit, env2, consts);
|
|
28377
28538
|
if (!parsed.value) throw new InputError(`${file}:${line2}: could not read the params object`);
|
|
28378
|
-
|
|
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;
|
|
28379
28556
|
}
|
|
28380
28557
|
function lineOfAny(text, needles) {
|
|
28381
28558
|
const lower2 = text.toLowerCase();
|
|
@@ -28486,7 +28663,8 @@ async function stripeJob(loaded, e, o, root) {
|
|
|
28486
28663
|
if (r.unresolved.length) {
|
|
28487
28664
|
job.partial = true;
|
|
28488
28665
|
job.unresolved = r.unresolved;
|
|
28489
|
-
|
|
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.`);
|
|
28490
28668
|
}
|
|
28491
28669
|
}
|
|
28492
28670
|
if (!params || typeof params !== "object" || Array.isArray(params)) throw new InputError("the Checkout Session params must be a JSON object");
|
|
@@ -28622,12 +28800,12 @@ function pageJob(loaded, e, o, root) {
|
|
|
28622
28800
|
function planMoney(plan2) {
|
|
28623
28801
|
if (!plan2) return void 0;
|
|
28624
28802
|
const cur = (plan2.currency ?? "usd").toUpperCase();
|
|
28625
|
-
const
|
|
28626
|
-
if (
|
|
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;
|
|
28627
28805
|
try {
|
|
28628
|
-
return new Intl.NumberFormat("en-US", { style: "currency", currency: cur }).format(
|
|
28806
|
+
return new Intl.NumberFormat("en-US", { style: "currency", currency: cur }).format(major2);
|
|
28629
28807
|
} catch {
|
|
28630
|
-
return `${
|
|
28808
|
+
return `${major2.toFixed(2)} ${cur}`;
|
|
28631
28809
|
}
|
|
28632
28810
|
}
|
|
28633
28811
|
function fillMergeTags(msg2, tags, config, plan2) {
|
|
@@ -28753,7 +28931,7 @@ async function planJobs(loaded, o, root) {
|
|
|
28753
28931
|
}
|
|
28754
28932
|
return jobs;
|
|
28755
28933
|
}
|
|
28756
|
-
var InputError, NOT_INSTALLED, CHILD, jurisdictionsOf;
|
|
28934
|
+
var InputError, NOT_INSTALLED, CHILD, SPREAD_VERDICT_KEYS, jurisdictionsOf;
|
|
28757
28935
|
var init_inputs = __esm({
|
|
28758
28936
|
"src/ci/inputs.ts"() {
|
|
28759
28937
|
"use strict";
|
|
@@ -28781,6 +28959,7 @@ if (typeof v === 'function') v = await v(...JSON.parse(argsJson));
|
|
|
28781
28959
|
process.stdout.write('\\n__CHECKBOX_JSON__' + JSON.stringify(v === undefined ? null : v) + '\\n');
|
|
28782
28960
|
process.exit(0);
|
|
28783
28961
|
`;
|
|
28962
|
+
SPREAD_VERDICT_KEYS = ["mode", "consent_collection", "custom_text", "subscription_data", "discounts"];
|
|
28784
28963
|
jurisdictionsOf = (config) => config.jurisdictions ? { jurisdictions: config.jurisdictions } : {};
|
|
28785
28964
|
}
|
|
28786
28965
|
});
|
|
@@ -29322,7 +29501,31 @@ function resultOf(status, exit, strict) {
|
|
|
29322
29501
|
function ciVocabulary(text) {
|
|
29323
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).');
|
|
29324
29503
|
}
|
|
29325
|
-
function pricingPageFixes(fixes) {
|
|
29504
|
+
function pricingPageFixes(fixes, replacements = []) {
|
|
29505
|
+
return concreteCardFixes(pricingPageFixesRaw(fixes), replacements);
|
|
29506
|
+
}
|
|
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) {
|
|
29326
29529
|
const reworded = fixes.map(
|
|
29327
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")
|
|
29328
29531
|
);
|
|
@@ -29371,8 +29574,10 @@ function fixesOf(r, onlyNeedsFix = false) {
|
|
|
29371
29574
|
const plain = cards.length > 1 ? cardClauses(c.explanation).filter((x) => /look like the surrounding text/.test(x.clause)).map((x) => x.card) : [];
|
|
29372
29575
|
if (plain.length && plain.length < cards.length && /renewal line/i.test(base)) {
|
|
29373
29576
|
cards = cards.filter((x) => !plain.includes(x));
|
|
29374
|
-
const
|
|
29375
|
-
|
|
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
|
+
}
|
|
29376
29581
|
}
|
|
29377
29582
|
const text = cards.length && base ? `Plan card ${cards.map((x) => `\u201C${x}\u201D`).join(", ")}: ${base}` : base;
|
|
29378
29583
|
if (text && !out.includes(text)) out.push(text);
|
|
@@ -29407,17 +29612,22 @@ function correctedOf(type, r, partial = false, placeholders, pricingPage = false
|
|
|
29407
29612
|
if (type === "stripe_checkout" && r.correction?.patch?.length) {
|
|
29408
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) };
|
|
29409
29614
|
}
|
|
29410
|
-
const
|
|
29411
|
-
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;
|
|
29412
29620
|
const all = perPlan.every((p) => p.verified);
|
|
29413
29621
|
return {
|
|
29414
29622
|
format: "text",
|
|
29415
|
-
title: `Suggested replacement per plan, each directly above its own plan's button${all ? " (each passes the checks, verified by the rules engine)" : ""}`,
|
|
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)" : ""}`,
|
|
29416
29624
|
// Round 32 (pilot 31, ci-pr-agent): a pricing page links to the checkout, where consent is asked (Stripe Checkout
|
|
29417
29625
|
// shows its own checkbox), so no consent checkbox label is suggested for it.
|
|
29418
|
-
body: perPlan.map((p) => `${p.plan}:
|
|
29626
|
+
body: `${perPlan.map((p) => `${p.plan}:
|
|
29419
29627
|
${p.disclosure_text}${p.consent_label && !pricingPage ? `
|
|
29420
|
-
Consent checkbox label: ${p.consent_label}` : ""}`).join("\n\n")
|
|
29628
|
+
Consent checkbox label: ${p.consent_label}` : ""}`).join("\n\n")}${perPlan.some((p) => p.html && BOXED_HTML.test(p.html)) ? `
|
|
29629
|
+
|
|
29630
|
+
${BOXED_LINE}` : ""}`
|
|
29421
29631
|
};
|
|
29422
29632
|
}
|
|
29423
29633
|
if ((type === "disclosure" || type === "page") && r.replacement?.disclosure_text && ciStatusOf(r) !== "pass") {
|
|
@@ -29429,7 +29639,9 @@ Consent checkbox label: ${p.consent_label}` : ""}`).join("\n\n")
|
|
|
29429
29639
|
|
|
29430
29640
|
Fuller version: ${r.replacement.disclosure_text}` : r.replacement.disclosure_text}${r.replacement.consent_label ? `
|
|
29431
29641
|
|
|
29432
|
-
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}` : ""}`
|
|
29433
29645
|
};
|
|
29434
29646
|
}
|
|
29435
29647
|
if (type === "email" && r.template && ciStatusOf(r) !== "pass" && r.verdict !== "not_applicable") {
|
|
@@ -29505,6 +29717,11 @@ function unverifiedOffer(r) {
|
|
|
29505
29717
|
if (coupon && open2.length) return coupon.message;
|
|
29506
29718
|
return void 0;
|
|
29507
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
|
+
}
|
|
29508
29725
|
async function runJob(job, engine, config, opts = {}) {
|
|
29509
29726
|
const base = { name: job.name, type: job.type, ...job.plan ? { plan: job.plan } : {}, target: { ...job.target }, ...job.fromSource ? { fromSource: true } : {}, warnings: [...job.warnings] };
|
|
29510
29727
|
if (job.error) return { ...base, status: "error", label: "Could not run", message: job.error, fixes: [] };
|
|
@@ -29525,11 +29742,16 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
29525
29742
|
}
|
|
29526
29743
|
const alerts = job.warnings.filter((w) => /^The params and checkbox\.json disagree/.test(w));
|
|
29527
29744
|
for (const w of report.warnings ?? []) {
|
|
29528
|
-
const text = ciVocabulary(w.message);
|
|
29745
|
+
const text = w.code === "site_plans_differ" ? sitePlansDifferInCi(w.message) : ciVocabulary(w.message);
|
|
29529
29746
|
base.warnings.push(text);
|
|
29530
29747
|
if (ALERT_CODES.has(w.code)) alerts.push(text);
|
|
29531
29748
|
}
|
|
29532
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
|
+
}
|
|
29533
29755
|
const status = ciStatusOf(report);
|
|
29534
29756
|
const params = job.type === "stripe_checkout" ? job.body?.params ?? void 0 : void 0;
|
|
29535
29757
|
let fixes = (status === "pass" || status === "not_applicable" ? [] : fixesOf(report)).map((f) => ciVocabulary(job.type === "stripe_checkout" ? stripeConsentFix(f, params) : f));
|
|
@@ -29540,11 +29762,14 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
29540
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.`);
|
|
29541
29763
|
}
|
|
29542
29764
|
const pricingPage = job.type === "page" && job.body?.page_kind === "pricing";
|
|
29543
|
-
|
|
29544
|
-
|
|
29545
|
-
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));
|
|
29546
29770
|
if (unread) {
|
|
29547
|
-
|
|
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" : ""}.`);
|
|
29548
29773
|
}
|
|
29549
29774
|
const literal = [];
|
|
29550
29775
|
if (unread) {
|
|
@@ -29595,7 +29820,7 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
29595
29820
|
...base,
|
|
29596
29821
|
status: finalStatus,
|
|
29597
29822
|
...blocking ? { blocking: true } : {},
|
|
29598
|
-
label: unread ? `${literal.length ? `${literal.length} fix${literal.length === 1 ? "" : "es"} needed \xB7 ` : ""}Not checked: ${unreadText[0]} is an expression` : declaredCoupon ? "Needs review \xB7 coupon declared, not verified" : unreadCoupon ? "Needs review \xB7 coupon not read" : countedLabel(report.compliance?.label || DEFAULT_LABEL[status], report, pricingPage ? (f) => pricingPageFixes(f.map(ciVocabulary)) : void 0),
|
|
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),
|
|
29599
29824
|
...fixes[0] ? { fix: fixes[0] } : {},
|
|
29600
29825
|
fixes,
|
|
29601
29826
|
...corrected ? { corrected } : {},
|
|
@@ -29667,7 +29892,7 @@ async function runCi(jobs, engine, opts) {
|
|
|
29667
29892
|
result: resultOf(status, exit, opts.strict)
|
|
29668
29893
|
};
|
|
29669
29894
|
}
|
|
29670
|
-
var ENGINE_KIND, STRIPE_OBJECTS_OFFLINE_NOTICE, 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;
|
|
29671
29896
|
var init_runner = __esm({
|
|
29672
29897
|
"src/ci/runner.ts"() {
|
|
29673
29898
|
"use strict";
|
|
@@ -29676,6 +29901,7 @@ var init_runner = __esm({
|
|
|
29676
29901
|
init_src();
|
|
29677
29902
|
init_types2();
|
|
29678
29903
|
init_stripe_verdict();
|
|
29904
|
+
init_set_off_fix();
|
|
29679
29905
|
init_config3();
|
|
29680
29906
|
ENGINE_KIND = {
|
|
29681
29907
|
stripe_checkout: "stripe_checkout_session",
|
|
@@ -29688,7 +29914,8 @@ var init_runner = __esm({
|
|
|
29688
29914
|
CI_DISCLAIMER = "Automated check against Checkbox's catalog of automatic-renewal laws; not legal advice.";
|
|
29689
29915
|
RANK2 = { fail: 0, needs_review: 1 };
|
|
29690
29916
|
OK_STATUSES = /* @__PURE__ */ new Set(["compliant", "pending_live_check", "verified_live", "not_applicable"]);
|
|
29691
|
-
|
|
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.';
|
|
29692
29919
|
NOTICE_TO_EMAIL = {
|
|
29693
29920
|
acknowledgment: "acknowledgment",
|
|
29694
29921
|
trial_ending: "trial_reminder",
|
|
@@ -29740,7 +29967,8 @@ function renderHuman(run2, c, opts = {}) {
|
|
|
29740
29967
|
lines.push(` ${icon(x.status, c)} ${x.name.padEnd(width)} \u2192 ${label}`);
|
|
29741
29968
|
if (x.message) lines.push(c.dim(` ${wrap(x.message, 110, " ")}`));
|
|
29742
29969
|
if (x.fix) lines.push(` fix: ${wrap(x.fix, 110, " ")}${c.dim(` (${where(x)})`)}`);
|
|
29743
|
-
|
|
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)`));
|
|
29744
29972
|
const alerts = x.alerts ?? [];
|
|
29745
29973
|
for (const a of alerts) lines.push(c.yellow(` warning: ${wrap(a, 110, " ")}`));
|
|
29746
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, " ")}`));
|
|
@@ -29766,8 +29994,9 @@ function annotations(run2) {
|
|
|
29766
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;
|
|
29767
29995
|
if (!level) continue;
|
|
29768
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" : ""}` : "";
|
|
29769
29998
|
const body = x.status === "error" ? x.fix ? `${x.message ?? ""}
|
|
29770
|
-
${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");
|
|
29771
30000
|
out.push(`::${level} ${props.join(",")}::${escData(body)}`);
|
|
29772
30001
|
}
|
|
29773
30002
|
if (run2.status === "skipped") out.push(`::error file=${escProp(run2.config)},title=${escProp("Checkbox: nothing was checked")}::${escData(nothingCheckedMessage(run2))}`);
|
|
@@ -29789,7 +30018,7 @@ function stepSummary(run2) {
|
|
|
29789
30018
|
}
|
|
29790
30019
|
const detailed = run2.checks.filter((x) => x.corrected || x.fixes.length > 1);
|
|
29791
30020
|
for (const x of detailed) {
|
|
29792
|
-
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>`, "");
|
|
29793
30022
|
if (x.fixes.length) lines.push(...x.fixes.map((f) => `- ${f}`), "");
|
|
29794
30023
|
if (x.corrected) {
|
|
29795
30024
|
const fence = x.corrected.body.includes("```") ? "````" : "```";
|
|
@@ -29862,6 +30091,7 @@ function renderPlan(p, c, opts = {}) {
|
|
|
29862
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"}>`)}`);
|
|
29863
30092
|
else if ((t.status === "done" || t.status === "wont_fix") && t.closed_reason) lines.push(c.dim(` closed: ${t.closed_reason.replace(/_/g, " ")}`));
|
|
29864
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)}`);
|
|
29865
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}`));
|
|
29866
30096
|
}
|
|
29867
30097
|
const hidden = p.tasks.length - tasks.length;
|
|
@@ -29873,7 +30103,7 @@ function statusColor(s2, c) {
|
|
|
29873
30103
|
return s2 === "compliant" ? c.green("all observed checks passing") : s2 === "action_required" ? c.red(s2) : c.yellow(s2);
|
|
29874
30104
|
}
|
|
29875
30105
|
function renderVerification(v, c) {
|
|
29876
|
-
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");
|
|
29877
30107
|
const firstCheck = !v.task && v.next_action?.type === "first_check_complete";
|
|
29878
30108
|
const lines = [`${c.bold(`Verification ${v.id}`)}${v.task ? ` for ${v.task}` : ""}: ${firstCheck ? c.cyan("first check complete") : mark} (${v.method})`];
|
|
29879
30109
|
if (v.precheck) lines.push(` pre-check (${v.precheck.source}): ${v.precheck.verdict} \u2014 ${v.precheck.note}`);
|
|
@@ -29901,7 +30131,7 @@ function workflowYaml(needs = {}) {
|
|
|
29901
30131
|
const node = !!(needs.install || needs.build);
|
|
29902
30132
|
const steps = [" - uses: actions/checkout@v4"];
|
|
29903
30133
|
if (node) {
|
|
29904
|
-
steps.push(" - uses: actions/setup-node@v4", " with:", " node-version:
|
|
30134
|
+
steps.push(" - uses: actions/setup-node@v4", " with:", " node-version: 22");
|
|
29905
30135
|
if (needs.install) steps.push(` # ${needs.why?.install ?? 'checkbox.json "module" entries import your code and its dependencies.'}`, ` - run: ${needs.install}`);
|
|
29906
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}`);
|
|
29907
30137
|
} else {
|
|
@@ -29997,9 +30227,11 @@ var init_workflow = __esm({
|
|
|
29997
30227
|
// src/commands/ci/index.ts
|
|
29998
30228
|
var ci_exports = {};
|
|
29999
30229
|
__export(ci_exports, {
|
|
30230
|
+
anchorToChanges: () => anchorToChanges,
|
|
30000
30231
|
attachSiteForCoupons: () => attachSiteForCoupons,
|
|
30001
30232
|
couponIdsIn: () => couponIdsIn,
|
|
30002
30233
|
default: () => ci_default,
|
|
30234
|
+
diffHunks: () => diffHunks,
|
|
30003
30235
|
siteFactsFor: () => siteFactsFor,
|
|
30004
30236
|
termsChanged: () => termsChanged,
|
|
30005
30237
|
touchesConfig: () => touchesConfig,
|
|
@@ -30019,6 +30251,38 @@ function termsChanged(cwd, base, jobs) {
|
|
|
30019
30251
|
return false;
|
|
30020
30252
|
}
|
|
30021
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
|
+
}
|
|
30022
30286
|
function gitChanged(cwd, base) {
|
|
30023
30287
|
try {
|
|
30024
30288
|
const root = execFileSync2("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
@@ -30360,6 +30624,7 @@ ${message}
|
|
|
30360
30624
|
const noticesLine = Math.max(1, loaded.raw.split("\n").findIndex((l) => l.includes('"sent_by_checkbox"')) + 1);
|
|
30361
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 } } : {} });
|
|
30362
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);
|
|
30363
30628
|
const run2 = !client && termsChanged(ctx.cwd, baseRef, jobs) ? { ...run0, offline_notice: STRIPE_OBJECTS_OFFLINE_NOTICE } : run0;
|
|
30364
30629
|
ctx.out.result(run2, () => renderHuman(run2, ctx.out.c, { verbose: bool(p.flags, "verbose") === true }));
|
|
30365
30630
|
if (gha) {
|
|
@@ -30558,12 +30823,20 @@ function diffLines(a, b) {
|
|
|
30558
30823
|
ops.push(...a.slice(a.length - suf).map((line2) => ({ t: " ", line: line2 })));
|
|
30559
30824
|
return ops;
|
|
30560
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
|
+
}
|
|
30561
30833
|
function unifiedDiff(before, after, path, context = 3) {
|
|
30562
30834
|
if (before === after) return "";
|
|
30563
|
-
const a = before
|
|
30564
|
-
const b = after
|
|
30835
|
+
const a = gitLines(before);
|
|
30836
|
+
const b = gitLines(after);
|
|
30565
30837
|
const ops = diffLines(a, b);
|
|
30566
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}`];
|
|
30567
30840
|
let i = 0;
|
|
30568
30841
|
while (i < ops.length) {
|
|
30569
30842
|
if (ops[i].t === " ") {
|
|
@@ -30585,26 +30858,50 @@ function unifiedDiff(before, after, path, context = 3) {
|
|
|
30585
30858
|
const bStart = ops.slice(0, start).filter((o) => o.t !== "-").length + 1;
|
|
30586
30859
|
const aLen = slice.filter((o) => o.t !== "+").length;
|
|
30587
30860
|
const bLen = slice.filter((o) => o.t !== "-").length;
|
|
30588
|
-
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));
|
|
30589
30862
|
i = stop;
|
|
30590
30863
|
start = stop;
|
|
30591
30864
|
}
|
|
30592
30865
|
return `${out.join("\n")}
|
|
30593
30866
|
`;
|
|
30594
30867
|
}
|
|
30595
|
-
var escapeRe2, VOID_TAGS;
|
|
30868
|
+
var escapeRe2, VOID_TAGS, NO_EOL, NO_EOL_MARKER;
|
|
30596
30869
|
var init_patch = __esm({
|
|
30597
30870
|
"src/commands/plan/patch.ts"() {
|
|
30598
30871
|
"use strict";
|
|
30599
30872
|
escapeRe2 = (s2) => s2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
30600
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 = "\";
|
|
30601
30876
|
}
|
|
30602
30877
|
});
|
|
30603
30878
|
|
|
30604
30879
|
// src/commands/plan/fix.ts
|
|
30605
30880
|
import { existsSync as existsSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
|
|
30606
30881
|
import { relative as relative6, resolve as resolve8 } from "node:path";
|
|
30607
|
-
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)) {
|
|
30608
30905
|
const base = {
|
|
30609
30906
|
object: "task_fix",
|
|
30610
30907
|
task: task.id,
|
|
@@ -30615,6 +30912,22 @@ function fixForTask(task, cwd, file) {
|
|
|
30615
30912
|
verify: task.verify
|
|
30616
30913
|
};
|
|
30617
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
|
+
}
|
|
30618
30931
|
const candidate = file ?? (task.where.file_hint && existsSync9(resolve8(cwd, task.where.file_hint)) ? task.where.file_hint : void 0);
|
|
30619
30932
|
if (task.change.type === "patch" && task.change.edits.length && candidate) {
|
|
30620
30933
|
const path = resolve8(cwd, candidate);
|
|
@@ -30652,14 +30965,16 @@ function fixForTask(task, cwd, file) {
|
|
|
30652
30965
|
const type = task.change.type === "manual" ? "answer_question" : task.change.type === "needs_facts" ? "provide_facts" : "apply_change";
|
|
30653
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 } };
|
|
30654
30967
|
}
|
|
30655
|
-
var TASK_RE2, fix_default;
|
|
30968
|
+
var TASK_RE2, major, fix_default;
|
|
30656
30969
|
var init_fix = __esm({
|
|
30657
30970
|
"src/commands/plan/fix.ts"() {
|
|
30658
30971
|
"use strict";
|
|
30659
30972
|
init_args();
|
|
30660
30973
|
init_types2();
|
|
30974
|
+
init_config3();
|
|
30661
30975
|
init_patch();
|
|
30662
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;
|
|
30663
30978
|
fix_default = defineCommand({
|
|
30664
30979
|
name: "fix",
|
|
30665
30980
|
summary: "Show the change for a plan task (exact diff of your file for code tasks)",
|
|
@@ -31371,7 +31686,7 @@ var init_init = __esm({
|
|
|
31371
31686
|
" --no-stripe Do not offer the Stripe test key / test subscription",
|
|
31372
31687
|
" --live Also request live-mode keys and write the domain-verification file",
|
|
31373
31688
|
" --no-wait Start the login, print the link and exit (run `checkbox init` again to continue)",
|
|
31374
|
-
" --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)",
|
|
31375
31690
|
" --no-open Do not open the browser",
|
|
31376
31691
|
" --verify Re-check the site now (after deploying) and print the updated checklist",
|
|
31377
31692
|
" --complete Finish the setup and email the human the summary (sent automatically the first time every",
|
|
@@ -31500,7 +31815,7 @@ var init_init = __esm({
|
|
|
31500
31815
|
if (bool(flags, "open") !== false) openBrowser(pending.verification_uri_complete, ctx.env);
|
|
31501
31816
|
const tty = !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
31502
31817
|
const waitArg = str(flags, "wait-seconds");
|
|
31503
|
-
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;
|
|
31504
31819
|
if (!Number.isFinite(waitSeconds) || waitSeconds < 0) throw new UsageError("--wait-seconds must be a number of seconds");
|
|
31505
31820
|
const polled = await pollDeviceLogin(anon, pending, waitSeconds * 1e3, ctx);
|
|
31506
31821
|
if (polled.status === "timeout") {
|
|
@@ -31873,6 +32188,7 @@ var init_plan = __esm({
|
|
|
31873
32188
|
init_args();
|
|
31874
32189
|
init_types2();
|
|
31875
32190
|
init_lib();
|
|
32191
|
+
init_fix();
|
|
31876
32192
|
plan_default = defineCommand({
|
|
31877
32193
|
name: "plan",
|
|
31878
32194
|
summary: "Show the compliance plan: ordered tasks to apply, then verify",
|
|
@@ -31893,7 +32209,15 @@ var init_plan = __esm({
|
|
|
31893
32209
|
const client = ctx.client();
|
|
31894
32210
|
const site = await resolveSite(ctx, str(flags, "site"), client);
|
|
31895
32211
|
const plan2 = await client.sdk.plans.retrieve(site);
|
|
31896
|
-
|
|
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
|
+
});
|
|
31897
32221
|
return bool(flags, "exit-code") && plan2.status !== "compliant" ? EXIT.fail : EXIT.ok;
|
|
31898
32222
|
}
|
|
31899
32223
|
});
|