Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions packages/cli/test/server-account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ describe("genclass plan, team and apps (src/manage.mjs)", () => {
// Pro's, so the account's own checkout, its trial first
expect(f).toContain("P R O");
expect(f).toContain("$15 / developer / month");
expect(f).toContain(`The genclass command comes with Pro ($15 / developer / month): start your 3-day free trial at ${HOST}/account/billing?plan=team&start=1`);
expect(f).toContain(`The genclass command comes with Pro ($15 / developer / month): start your 3-day free trial at ${HOST}/account/billing?plan=pro&start=1`);
expect(f).toContain("Apps and their plans Shop no plan chosen");
expect(JSON.parse((await said(() => plan({ ...me.o, json: true }, me.deps))).text)).toMatchObject({ ok: true, plan: { id: "free" }, limits: { apps: 1 }, offer: { plan: { id: "team" } }, cli: { paid: false, checkout_url: `${HOST}/account/billing?plan=team&start=1`, trial_days: 3 } });
expect(JSON.parse((await said(() => plan({ ...me.o, json: true }, me.deps))).text)).toMatchObject({ ok: true, plan: { id: "free" }, limits: { apps: 1 }, offer: { plan: { id: "team" } }, cli: { paid: false, checkout_url: `${HOST}/account/billing?plan=pro&start=1`, trial_days: 3 } });
});

it("Free says so after a command, with the way up; profile and upgrade name the app on Free and its plan page", async () => {
Expand All @@ -127,13 +127,13 @@ describe("genclass plan, team and apps (src/manage.mjs)", () => {
expect(f).toContain("Apps and their plans Shop no plan chosen");
expect(f).toContain("Start the 3-day free trial $15 / developer / month npx genclass upgrade");
expect(f).toContain("Sign out npx genclass logout");
expect(JSON.parse((await said(() => profile({ ...me.o, json: true }, me.deps))).text)).toMatchObject({ ok: true, plan: { id: "free" }, apps: [{ name: "Shop", plan: { id: "free" } }], upgrade_url: `${HOST}/account/billing?plan=team&start=1`, cli: { paid: false } });
expect(JSON.parse((await said(() => profile({ ...me.o, json: true }, me.deps))).text)).toMatchObject({ ok: true, plan: { id: "free" }, apps: [{ name: "Shop", plan: { id: "free" } }], upgrade_url: `${HOST}/account/billing?plan=pro&start=1`, cli: { paid: false } });
// upgrade: Pro's card and its checkout, for the account (the genclass command is Pro's)
const u = await said(() => upgrade(me.o, me.deps));
expect(u.code).toBe(0);
expect(tooWide(u.text)).toEqual([]);
expect(flat(u.text)).toContain("Start your 3-day free trial");
expect(flat(u.text)).toContain(`start your 3-day free trial at ${HOST}/account/billing?plan=team&start=1`);
expect(flat(u.text)).toContain(`start your 3-day free trial at ${HOST}/account/billing?plan=pro&start=1`);
// on Team: no line, nothing to upgrade
sql.db.prepare("UPDATE projects SET plan = 'team', stripe_mode = 'live', subscription_id = 'sub_s', subscription_status = 'active' WHERE id = ?").run(me.app.id);
expect(await freeLine(me.o, me.deps)).toBe(false);
Expand Down Expand Up @@ -175,7 +175,7 @@ describe("genclass plan, team and apps (src/manage.mjs)", () => {
const no = await said(() => team({ ...me.o, args: ["invite"] }, me.deps));
expect(no.code).toBe(1);
// the genclass command is Pro's: the invite, like every command, says where Pro starts
expect(flat(no.text)).toContain(`The genclass command comes with Pro ($15 / developer / month): start your 3-day free trial at ${HOST}/account/billing?plan=team&start=1`);
expect(flat(no.text)).toContain(`The genclass command comes with Pro ($15 / developer / month): start your 3-day free trial at ${HOST}/account/billing?plan=pro&start=1`);
expect(flat(no.text)).toContain("npx genclass upgrade");
team3(sql, me.account.id);
const made = await said(() => team({ ...me.o, args: ["invite"] }, me.deps));
Expand Down Expand Up @@ -221,10 +221,10 @@ describe("genclass is Pro's: a view the server refuses with 402 says where Pro s
const v = await said(() => analytics(me.o, me.deps));
expect(v.code).toBe(1);
expect(tooWide(v.text)).toEqual([]);
expect(flat(v.text)).toContain(`The genclass command comes with Pro ($15 / developer / month): start your 3-day free trial at ${HOST}/account/billing?plan=team&start=1`);
expect(flat(v.text)).toContain(`The genclass command comes with Pro ($15 / developer / month): start your 3-day free trial at ${HOST}/account/billing?plan=pro&start=1`);
expect(flat(v.text)).toContain("npx genclass upgrade");
expect(v.text).not.toContain("Could not read the dashboard");
expect(JSON.parse((await said(() => analytics({ ...me.o, json: true }, me.deps))).text)).toEqual({ ok: false, error: "the genclass command comes with Pro", needs_pro: true, checkout_url: `${HOST}/account/billing?plan=team&start=1`, trial_days: 3, run: "npx genclass upgrade" });
expect(JSON.parse((await said(() => analytics({ ...me.o, json: true }, me.deps))).text)).toEqual({ ok: false, error: "the genclass command comes with Pro", needs_pro: true, checkout_url: `${HOST}/account/billing?plan=pro&start=1`, trial_days: 3, run: "npx genclass upgrade" });
});
});

Expand Down Expand Up @@ -253,7 +253,7 @@ describe("genclass connections (src/manage.mjs connections)", () => {
expect(code).toBe(1);
expect(tooWide(text)).toEqual([]);
const f = flat(text);
expect(f).toContain(`The genclass command comes with Pro ($15 / developer / month): start your 3-day free trial at ${HOST}/account/billing?plan=team&start=1`);
expect(f).toContain(`The genclass command comes with Pro ($15 / developer / month): start your 3-day free trial at ${HOST}/account/billing?plan=pro&start=1`);
expect(f).toContain("npx genclass upgrade");
expect(f).not.toContain("not set up");
// setting one up on Free: the same, said before anything is asked for
Expand All @@ -262,7 +262,7 @@ describe("genclass connections (src/manage.mjs connections)", () => {
expect(flat(no.text)).toContain("The genclass command comes with Pro");
expect(flat(no.text)).toContain("npx genclass upgrade");
expect(no.text).not.toContain("Slack incoming webhook URL");
expect(JSON.parse((await said(() => connections({ ...me.o, json: true }, me.deps))).text)).toMatchObject({ ok: false, needs_pro: true, checkout_url: `${HOST}/account/billing?plan=team&start=1`, run: "npx genclass upgrade" });
expect(JSON.parse((await said(() => connections({ ...me.o, json: true }, me.deps))).text)).toMatchObject({ ok: false, needs_pro: true, checkout_url: `${HOST}/account/billing?plan=pro&start=1`, run: "npx genclass upgrade" });
});

it("Team: add one (what is typed is never shown), test it, turn it off, remove it", async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ describe("genclass against the collector (src/api.mjs, telemetry-worker/src/cli.
designs: [],
ideasLocked: 2,
designsLocked: 1,
upgradeUrl: `${HOST}/account/billing?plan=team&from=cli`,
upgradeUrl: `${HOST}/account/billing?plan=pro&from=cli`,
upgradePlan: { plan: "Pro", price: "$15 / developer / month", trialDays: 3 },
limited: false,
};
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/test/state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,11 @@ describe("genclass: Pro right after the sign-in, and each app's plan page (src/s
price: "$15 / developer / month",
summary: "Everything for up to 3 apps, per developer.",
features: ["Every suggestion, with a prompt for your agent", "GitHub App on private repositories", "Visitor analytics", "Alerts to Sentry, PostHog, Datadog, Slack or a webhook"],
checkout_url: `${HOST}/account/billing?plan=team&start=1`,
checkout_url: `${HOST}/account/billing?plan=pro&start=1`,
trial_days: 3,
free: { name: "Free", apps: 1, history_days: 7, suggestions_shown: 1 },
};
const CHECKOUT = `${HOST}/account/billing?plan=team&start=1`;
const CHECKOUT = `${HOST}/account/billing?plan=pro&start=1`;
const LOCKED = { paid: false, plan: "Pro", price: "$15 / developer / month", trial_days: 3, checkout_url: CHECKOUT };
const PRO = { plan: { id: "team", name: "Pro" }, cli: { paid: true } };
const plain = (t: string) => t.replace(/\u001b\[[0-9;]*m/g, "");
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/suggest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ describe("genclass suggest: end to end (src/suggest/index.mjs header)", () => {
const text = await printed(async () => (code = await suggest({ cwd: dir, url: "https://shop.example", yes: true, json: true }, deps(w))));
expect(code).toBe(0);
const j = JSON.parse(text);
expect(j).toMatchObject({ site: "https://shop.example", source: "checks", plan: { id: "free", name: "Free" }, upgrade_url: `${HOST}/account/billing?plan=team&from=cli`, ideas: [], designs: [] });
expect(j).toMatchObject({ site: "https://shop.example", source: "checks", plan: { id: "free", name: "Free" }, upgrade_url: `${HOST}/account/billing?plan=pro&from=cli`, ideas: [], designs: [] });
// Free: the first one in full, the rest counted and not listed
expect(j.failed).toHaveLength(1);
expect(j.failed[0]).toMatchObject({ n: 1, why: expect.any(String), do: expect.any(String), prompt: expect.any(String) });
Expand Down
5 changes: 3 additions & 2 deletions telemetry-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,8 @@ their link opens the dashboard (24 hours, 7 and 30 days).
written yet); with one seat the owner is told to add seats in Manage billing; a plan GenClass grants (a paid plan
under a grant of the same plan too), and every account on a server with `PLAN_GATES = "open"`, no limit. A member
works under its team's plan in the genclass command (`src/cli.ts` workingAccess: suggestions, `ask`, the plan it
names; `cli.via` is `"team"`). The owner is the first seat, then members in
names; `cli.via` is `"team"`). An app such a member adds goes into the team's account (`src/team.ts` appHome), so
the team's plan covers it and the owner sees it. The owner is the first seat, then members in
the order they joined. A member beyond the seats (fewer seats, or the plan ended) keeps the row and loses the
access until there is a seat again: no billing change removes anyone.
- **What a member can do.** Open the dashboards and read the stats of the owner's apps, shown with the owner's plan
Expand Down Expand Up @@ -855,7 +856,7 @@ does about it.
| `GET /api/billing` | browser session: `{ plan, subscription_status, current_period_end, cancel_at_period_end, on_sale, access, seats }` (`on_sale`: a plan is sold and this account can buy it); `access` is `paid`, `comp` (GenClass's grant), `open` (this server's `PLAN_GATES`) or null (Free); `seats`: the people the account holds, null for no limit |
| `POST /api/billing/trial` | 410: GenClass has no trial any more ([Grants and no trial](#grants-enterprise-and-no-trial)) |
| `GET /account/trial` | `/account`, through `/signup` when signed out (genclass.dev's pricing page linked here until 2026-10-19) |
| `GET /account/billing?project=<id>` | one app's plan page (`billing/plans.json` version 5): "Choose a plan for <app>", Pro's card (its name, price, first 5 lines and "N more"), Start your 3-day free trial (the account's Checkout, coming back here) or Continue with Pro for a paying account, and a very small "Skip" under it (Free, after the reason popup; it read "Try for free (limited)" until 2026-11-13), none for an app past what the account's plan covers (Free: since 13 November 2026 one app, the oldest; Pro: Enterprise by email); the app's live fixes this month, once there are some. `&from=cli` (the genclass command's plan step, which waits for the choice; the choice goes on to the app's dashboard), `&new=1` (an app just added on the website, which goes on to its setup), `&start=1` (opens the Checkout at once). `?plan=team` without a project opens the plan page of the account's first app on Free. |
| `GET /account/billing?project=<id>` | one app's plan page (`billing/plans.json` version 5): "Choose a plan for <app>", Pro's card (its name, price, first 5 lines and "N more"), Start your 3-day free trial (the account's Checkout, coming back here) or Continue with Pro for a paying account, and a very small "Skip" under it (Free, after the reason popup; it read "Try for free (limited)" until 2026-11-13), none for an app past what the account's plan covers (Free: since 13 November 2026 one app, the oldest; Pro: Enterprise by email); the app's live fixes this month, once there are some. `&from=cli` (the genclass command's plan step, which waits for the choice; the choice goes on to the app's dashboard), `&new=1` (an app just added on the website, which goes on to its setup), `&start=1` (opens the Checkout at once). `?plan=pro` (links name a plan in lower case, `plans.ts` planParam; its id, `team`, still works) without a project opens the plan page of the account's first app on Free. |
| `POST /api/billing/projects/<id>/free` | `{ reason, note? }`, browser session and our `Origin`: Free chosen for one app on its plan page, after the popup "What's stopping you from paying?" (`reason` one of `FREE_REASONS` in `src/billing.ts`: price, trying, approval, feature, unsure, other; `note` at most 500 characters). Keeps the answer (`plan_feedback`, migration 0017), writes `projects.plan_chosen` (which `GET /api/cli/overview` reports as `plan_chosen`) and tells the owner (`NOTIFY_WEBHOOK`, kind `free_chosen`). 400 without a reason, 409 for an app past the ones Free covers, 404 for another account's app. Until an own app on Free has a choice (where Team is sold), `/account/apps/<id>` redirects to its plan page. |
| `GET /account/billing` | the plan, its renewal or end date, the plans side by side (Free, Team and Enterprise), the seats box and Upgrade (and Switch, with two paid plans) buttons, the Team section (members, invite links, the teams the account is in) and Manage billing (only to an account that can buy), Talk to us (a mailto link) for Enterprise, and which plan is sold through Link (Managed Payments; none today). With no plan sold it shows no Upgrade button, no "billed monthly" and no Stripe footer. `?plan=<id>` (a pricing page button) survives the sign-in redirect and highlights and focuses that plan's button. Back from Checkout (`?done=1`) while the plan still reads Free, or back from a switch (`?done=1&to=<plan>`) while the plan is still the old one, it shows no buttons and waits for the webhook. |
| `GET /api/account/projects/<id>/integrations` | browser session: the app's forwarding destinations, masked |
Expand Down
10 changes: 6 additions & 4 deletions telemetry-worker/src/account-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ dl.req{display:grid;grid-template-columns:minmax(110px,auto) minmax(0,1fr);borde
dl.req dt,dl.req dd{padding:11px 0;border-bottom:1px solid var(--rule);font-size:14.5px;min-width:0;overflow-wrap:anywhere}
dl.req dt{font:500 12.5px var(--mono);color:var(--ink3);padding-top:13px;padding-right:16px}
ul.scope{margin:10px 0 0;padding-left:20px;font-size:14.5px;line-height:1.5;color:var(--ink2)}
details.scope-d{margin-top:16px;font-size:14.5px;color:var(--ink2)}
details.scope-d summary{cursor:pointer;color:var(--ink)}
details.scope-d .lead{margin-top:10px}
ul.scope li{margin-top:5px}
.acts{display:flex;gap:10px;margin-top:22px}
.acts .btn{flex:1}
Expand Down Expand Up @@ -573,19 +576,18 @@ ${signInWays({ ...v.providers, next: v.next })}
<div class="ucode" aria-label="Code">${esc(r.userCode)}</div>
<dl class="req">
<dt>Requested by</dt><dd>${r.client ? `${esc(r.client)} <span class="muted">(as the command line reported itself)</span>` : 'Unknown client <span class="muted">(it did not name itself as the GenClass command line)</span>'}</dd>
<dt>Requested</dt><dd>${time(r.created, "time")} · ${time(r.created, "ago")}</dd>
<dt>Code expires</dt><dd id="exp">${time(r.expires, "time")}</dd>
<dt>Account</dt><dd>${who}</dd>
</dl>
<p class="lead" id="scope">If you approve, the terminal that shows this code can do this, for 90 days or until you revoke it on your account page:</p>
<details class="scope-d" id="scope"><summary>What this terminal can do for 90 days</summary>
<ul class="scope" id="scopelist">
<li>list and create apps, add an app from its private dashboard link, and write a project key into your project</li>
<li>show your apps' dashboards, visitor analytics and stored events</li>
<li>ask GenClass's model about an app (your question and the app's numbers go to the model)</li>
<li>show and change where an app's alerts are sent, its funnels, and its private dashboard link</li>
<li>show your plan and your team, and make a team invite link</li>
</ul>
<p class="lead" id="never">It cannot change how you sign in, pay for anything, remove people, or delete an app or your account.</p>
<p class="lead" id="never">It cannot change how you sign in, pay for anything, remove people, or delete an app or your account. You can revoke it any time on your account page.</p>
</details>
<p class="caution" id="caution"><b>Approve only a sign-in you started.</b> If you did not run ${runOf(r.client)} yourself just now, or someone sent you this link or code, choose Deny.</p>
<div class="acts" id="acts"><button class="btn green" id="yes" type="button"><span>Approve</span></button><button class="btn ghost" id="no" type="button"><span>Deny</span></button></div>
<p class="err" id="err" role="alert"></p>
Expand Down
Loading
Loading