@intelligo-dev/cli 1.0.0-beta.14 → 1.0.0-beta.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/args.d.ts +10 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +20 -0
- package/dist/args.js.map +1 -0
- package/dist/bin.js +69 -25
- package/dist/bin.js.map +1 -1
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +19 -2
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/create-flow.d.ts +4 -0
- package/dist/commands/create-flow.d.ts.map +1 -1
- package/dist/commands/create-flow.js +39 -4
- package/dist/commands/create-flow.js.map +1 -1
- package/dist/commands/create.d.ts +16 -0
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +42 -7
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +102 -16
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/sync.d.ts +6 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +34 -3
- package/dist/commands/sync.js.map +1 -1
- package/dist/env-files.d.ts +11 -0
- package/dist/env-files.d.ts.map +1 -1
- package/dist/env-files.js +28 -9
- package/dist/env-files.js.map +1 -1
- package/package.json +1 -1
- package/src/args.ts +25 -0
- package/src/bin.ts +78 -28
- package/src/commands/add.ts +28 -12
- package/src/commands/create-flow.ts +38 -4
- package/src/commands/create.ts +65 -7
- package/src/commands/doctor.ts +129 -20
- package/src/commands/sync.ts +42 -2
- package/src/env-files.ts +28 -7
- package/templates/admin-page/admin-page.tsx.tpl +96 -24
- package/templates/app-scaffold/gitignore.tpl +25 -0
- package/templates/app-scaffold/next.config.mjs.tpl +31 -2
- package/templates/app-scaffold/package.json.tpl +2 -2
- package/templates/manifest.json +21 -3
- package/templates/pnpm-standalone/npmrc.tpl +6 -0
- package/templates/pnpm-standalone/pnpm-workspace.yaml.tpl +10 -0
- package/templates/registry/app-shell.json +2 -2
- package/templates/registry/billing-settings.json +8 -2
- package/templates/registry/chat.json +1 -1
- package/templates/registry/payment-poll.json +11 -5
- package/templates/registry/pricing.json +8 -2
- package/templates/registry/registry.json +18 -3
- package/templates/registry/trial-banner.json +2 -2
- package/templates/registry-items.json +3 -3
- package/templates/registry-requires.json +7 -5
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
3
|
"name": "payment-poll",
|
|
4
4
|
"title": "QR Payment (poll)",
|
|
5
|
-
"description": "The non-card checkout the catalogue was missing: issue an invoice, show a QR the user scans in their banking app, poll until the provider confirms. Provider-agnostic — QPay, SocialPay, PIX, UPI, PromptPay all fit — with the invoice recorded and a paid one granted on the server; lib/local-payment.ts says what each reference costs and grants. Requires the pricing item for lib/billing-config.ts's CURRENCY.",
|
|
5
|
+
"description": "The non-card checkout the catalogue was missing: issue an invoice, show a QR the user scans in their banking app, poll until the provider confirms. Provider-agnostic — QPay, SocialPay, PIX, UPI, PromptPay all fit — with the invoice recorded and a paid one granted on the server; lib/local-payment.ts says what each reference costs and grants. Requires the pricing item for lib/billing-config.ts's CURRENCY. LocalPaymentButton opens the modal for one reference and refreshes the page once paid; bind it in the pricing item's lib/plan-card-config.tsx to offer the QR rail on every plan card, and in the billing-settings item's lib/credit-bundle-config.tsx on every credit bundle.",
|
|
6
6
|
"dependencies": [
|
|
7
7
|
"@intelligo-dev/auth",
|
|
8
8
|
"@intelligo-dev/billing",
|
|
@@ -23,27 +23,33 @@
|
|
|
23
23
|
"type": "registry:component",
|
|
24
24
|
"target": "components/billing/local-payment-modal.tsx"
|
|
25
25
|
},
|
|
26
|
+
{
|
|
27
|
+
"path": "base/payment-poll/components/local-payment-button.tsx",
|
|
28
|
+
"content": "\"use client\";\n\n/**\n * A button that opens `LocalPaymentModal` for one reference and, once\n * the payment is confirmed, closes it and refreshes the page so it shows\n * what was bought.\n *\n * The pricing item's plan card renders it when `lib/plan-card-config.tsx`\n * binds it; it fits anywhere else something is sold the same way.\n */\n\nimport { useState, type ComponentProps } from \"react\";\nimport { QrCode } from \"lucide-react\";\nimport { useTranslations } from \"next-intl\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { useRouter } from \"@/i18n/navigation\";\n\nimport { LocalPaymentModal } from \"./local-payment-modal\";\n\ninterface LocalPaymentButtonProps {\n /** What is being bought — handed to `priceLocalPayment` on the server. */\n reference: string;\n /** Amount to display, in major units of `CURRENCY`. */\n amount: number;\n /** Localized name of what is being bought. */\n label: string;\n /** The button's text; \"Pay with QR\" by default. */\n children?: React.ReactNode;\n variant?: ComponentProps<typeof Button>[\"variant\"];\n className?: string;\n}\n\nexport function LocalPaymentButton({\n reference,\n amount,\n label,\n children,\n variant = \"outline\",\n className = \"w-full\",\n}: LocalPaymentButtonProps) {\n const t = useTranslations(\"payment-poll\");\n const router = useRouter();\n const [open, setOpen] = useState(false);\n\n return (\n <>\n <Button\n variant={variant}\n className={className}\n onClick={() => setOpen(true)}\n >\n <QrCode aria-hidden />\n {children ?? t(\"button\")}\n </Button>\n <LocalPaymentModal\n open={open}\n onClose={() => setOpen(false)}\n reference={reference}\n amount={amount}\n label={label}\n onPaid={() => {\n setOpen(false);\n router.refresh();\n }}\n />\n </>\n );\n}\n",
|
|
29
|
+
"type": "registry:component",
|
|
30
|
+
"target": "components/billing/local-payment-button.tsx"
|
|
31
|
+
},
|
|
26
32
|
{
|
|
27
33
|
"path": "base/payment-poll/actions.ts",
|
|
28
|
-
"content": "\"use server\";\n\n/**\n * Transport for the QR-and-poll payment flow: resolve the caller's\n * workspace, ask `@/lib/local-payment` what the reference costs and\n * grants, and hand the rest to `@intelligo-dev/billing`, which records\n * the invoice and grants a paid one on the server.\n *\n * Neither action takes a user id, a workspace id or an amount from the\n * browser: who pays is a session fact, and what it costs is priced\n * server-side from `reference`.\n */\n\nimport { revalidatePath } from \"next/cache\";\nimport { getTranslations } from \"next-intl/server\";\n\nimport { requireWorkspace } from \"@intelligo-dev/auth\";\nimport type { ActionResult } from \"@intelligo-dev/next\";\nimport {\n isBillingServiceError,\n openLocalInvoice,\n settleLocalInvoice,\n type LocalPaymentStatus,\n} from \"@intelligo-dev/billing\";\nimport type { CreatePaymentResult } from \"@intelligo-dev/billing/payment\";\n\nimport { priceLocalPayment } from \"@/lib/local-payment\";\n\nexport type PaymentActionResult<T> = ActionResult<T>;\n\ntype LocalPaymentInvoice = Pick<\n CreatePaymentResult,\n \"invoiceId\" | \"qrCode\" | \"deeplinks\"\n>;\n\nexport async function startLocalPayment(\n reference: string\n): Promise<PaymentActionResult<LocalPaymentInvoice>> {\n const t = await getTranslations(\"payment-poll\");\n\n if (typeof reference !== \"string\" || reference.length === 0) {\n return { success: false, error: t(\"errors.unavailable\") };\n }\n\n let context;\n try {\n context = await requireWorkspace();\n } catch {\n return { success: false, error: t(\"errors.unauthorized\") };\n }\n\n try {\n const offer = await priceLocalPayment(reference);\n if (!offer) return { success: false, error: t(\"errors.unavailable\") };\n\n const invoice = await openLocalInvoice({\n workspaceId: context.workspace.id,\n userId: context.user.id,\n reference,\n price: offer.price,\n description: offer.description,\n });\n return {\n success: true,\n data: {\n invoiceId: invoice.invoiceId,\n qrCode: invoice.qrCode,\n deeplinks: invoice.deeplinks,\n },\n };\n } catch (error) {\n // Provider errors can carry account identifiers and endpoint\n // details; the caller gets the generic message, the log the cause.\n console.error(\"[payment-poll]\", error);\n return { success: false, error: t(\"errors.createFailed\") };\n }\n}\n\nexport async function pollLocalPayment(\n invoiceId: string\n): Promise<PaymentActionResult<LocalPaymentStatus>> {\n const t = await getTranslations(\"payment-poll\");\n\n if (typeof invoiceId !== \"string\" || invoiceId.length === 0) {\n return { success: false, error: t(\"errors.statusFailed\") };\n }\n\n let context;\n try {\n context = await requireWorkspace();\n } catch {\n return { success: false, error: t(\"errors.unauthorized\") };\n }\n\n try {\n const status = await settleLocalInvoice({\n invoiceId,\n workspaceId: context.workspace.id,\n fulfil: async (payment) => {\n const offer = await priceLocalPayment(payment.reference);\n if (!offer) {\n throw new Error(\n `\"${payment.reference}\" no longer prices through lib/local-payment.ts; ` +\n `invoice ${payment.invoiceId} is paid and left unfulfilled.`\n );\n }\n return offer.grant;\n },\n });\n // What was bought shows everywhere the workspace's plan or balance\n // is rendered, so every page is stale once it lands.\n if (status === \"paid\") revalidatePath(\"/\", \"layout\");\n return { success: true, data: status };\n } catch (error) {\n if (!isBillingServiceError(error) || error.code !== \"payment_not_found\") {\n console.error(\"[payment-poll]\", error);\n }\n return { success: false, error: t(\"errors.statusFailed\") };\n }\n}\n",
|
|
34
|
+
"content": "\"use server\";\n\n/**\n * Transport for the QR-and-poll payment flow: resolve the caller's\n * workspace, ask `@/lib/local-payment` what the reference costs and\n * grants, and hand the rest to `@intelligo-dev/billing`, which records\n * the invoice and grants a paid one on the server.\n *\n * Neither action takes a user id, a workspace id or an amount from the\n * browser: who pays is a session fact, and what it costs is priced\n * server-side from `reference`.\n */\n\nimport { revalidatePath } from \"next/cache\";\nimport { getTranslations } from \"next-intl/server\";\n\nimport { requireRole, requireWorkspace } from \"@intelligo-dev/auth\";\nimport type { ActionResult } from \"@intelligo-dev/next\";\nimport {\n isBillingServiceError,\n openLocalInvoice,\n settleLocalInvoice,\n type LocalPaymentStatus,\n} from \"@intelligo-dev/billing\";\nimport type { CreatePaymentResult } from \"@intelligo-dev/billing/payment\";\n\nimport { priceLocalPayment } from \"@/lib/local-payment\";\nimport { paymentPollConfig } from \"@/lib/payment-poll-config\";\n\n/**\n * Who may open an invoice. Read with `in`: a config written before the\n * field existed still compiles, and keeps the owner-only default.\n */\nconst PAYER_ROLES =\n (\"payerRoles\" in paymentPollConfig ? paymentPollConfig.payerRoles : null) ??\n ([\"owner\"] as const);\n\nexport type PaymentActionResult<T> = ActionResult<T>;\n\ntype LocalPaymentInvoice = Pick<\n CreatePaymentResult,\n \"invoiceId\" | \"qrCode\" | \"deeplinks\"\n>;\n\nexport async function startLocalPayment(\n reference: string\n): Promise<PaymentActionResult<LocalPaymentInvoice>> {\n const t = await getTranslations(\"payment-poll\");\n\n if (typeof reference !== \"string\" || reference.length === 0) {\n return { success: false, error: t(\"errors.unavailable\") };\n }\n\n let context;\n try {\n context = await requireWorkspace();\n } catch {\n return { success: false, error: t(\"errors.unauthorized\") };\n }\n try {\n await requireRole([...PAYER_ROLES]);\n } catch {\n return { success: false, error: t(\"errors.forbidden\") };\n }\n\n try {\n const offer = await priceLocalPayment(reference);\n if (!offer) return { success: false, error: t(\"errors.unavailable\") };\n\n const invoice = await openLocalInvoice({\n workspaceId: context.workspace.id,\n userId: context.user.id,\n reference,\n price: offer.price,\n description: offer.description,\n });\n return {\n success: true,\n data: {\n invoiceId: invoice.invoiceId,\n qrCode: invoice.qrCode,\n deeplinks: invoice.deeplinks,\n },\n };\n } catch (error) {\n // Provider errors can carry account identifiers and endpoint\n // details; the caller gets the generic message, the log the cause.\n console.error(\"[payment-poll]\", error);\n return { success: false, error: t(\"errors.createFailed\") };\n }\n}\n\nexport async function pollLocalPayment(\n invoiceId: string\n): Promise<PaymentActionResult<LocalPaymentStatus>> {\n const t = await getTranslations(\"payment-poll\");\n\n if (typeof invoiceId !== \"string\" || invoiceId.length === 0) {\n return { success: false, error: t(\"errors.statusFailed\") };\n }\n\n let context;\n try {\n context = await requireWorkspace();\n } catch {\n return { success: false, error: t(\"errors.unauthorized\") };\n }\n\n try {\n const status = await settleLocalInvoice({\n invoiceId,\n workspaceId: context.workspace.id,\n fulfil: async (payment) => {\n const offer = await priceLocalPayment(payment.reference);\n if (!offer) {\n throw new Error(\n `\"${payment.reference}\" no longer prices through lib/local-payment.ts; ` +\n `invoice ${payment.invoiceId} is paid and left unfulfilled.`\n );\n }\n return offer.grant;\n },\n });\n // What was bought shows everywhere the workspace's plan or balance\n // is rendered, so every page is stale once it lands.\n if (status === \"paid\") revalidatePath(\"/\", \"layout\");\n return { success: true, data: status };\n } catch (error) {\n if (!isBillingServiceError(error) || error.code !== \"payment_not_found\") {\n console.error(\"[payment-poll]\", error);\n }\n return { success: false, error: t(\"errors.statusFailed\") };\n }\n}\n",
|
|
29
35
|
"type": "registry:file",
|
|
30
36
|
"target": "actions/payment.ts"
|
|
31
37
|
},
|
|
32
38
|
{
|
|
33
39
|
"path": "base/payment-poll/lib/local-payment.ts",
|
|
34
|
-
"content": "import \"server-only\";\n\n/**\n * What a local payment buys — consumer-owned.\n *\n * Card checkout is a redirect: you send the user to the processor and\n * they come back. Most of the world's payment methods are not that.\n * QR-and-poll — QPay and SocialPay in Mongolia, PIX in Brazil, UPI in\n * India, PromptPay in Thailand — issues an invoice, shows a code the\n * user scans in their own banking app, and waits for the provider to\n * say it was paid. `LocalPaymentModal` renders that flow.\n *\n * The framework does the rest: `@intelligo-dev/billing` issues the\n * invoice through the provider your composition root registered\n * (`registerPaymentProvider`, selected by PAYMENT_MODE; outside\n * production an unset PAYMENT_MODE is an in-memory mock), records it\n * against the caller's workspace, and when the provider reports it\n * paid, grants what you return here once, on the server.\n *\n * This file answers one question: what does `reference` cost, and what\n * does paying it grant? It is asked when the invoice is opened and again\n * when it is settled. Price it here, never from anything the browser\n * sent: an amount that arrives as an argument is an amount the buyer\n * chose. Return null for a reference you do not sell this way.\n *\n * The default throws rather than pricing anything — a payment flow that\n * silently no-ops is worse than one that is obviously unbound.\n *\n * An implementation, over the plan catalogue:\n *\n * const plan = getPlanBySlug(reference);\n * if (!plan) return null;\n * return {\n * price: fromMajor(plan.priceOneTime, CURRENCY),\n * grant: { plan: plan.slug },\n * description: plan.name,\n * };\n *\n * or
|
|
40
|
+
"content": "import \"server-only\";\n\n/**\n * What a local payment buys — consumer-owned.\n *\n * Card checkout is a redirect: you send the user to the processor and\n * they come back. Most of the world's payment methods are not that.\n * QR-and-poll — QPay and SocialPay in Mongolia, PIX in Brazil, UPI in\n * India, PromptPay in Thailand — issues an invoice, shows a code the\n * user scans in their own banking app, and waits for the provider to\n * say it was paid. `LocalPaymentModal` renders that flow.\n *\n * The framework does the rest: `@intelligo-dev/billing` issues the\n * invoice through the provider your composition root registered\n * (`registerPaymentProvider`, selected by PAYMENT_MODE; outside\n * production an unset PAYMENT_MODE is an in-memory mock), records it\n * against the caller's workspace, and when the provider reports it\n * paid, grants what you return here once, on the server.\n *\n * This file answers one question: what does `reference` cost, and what\n * does paying it grant? It is asked when the invoice is opened and again\n * when it is settled. Price it here, never from anything the browser\n * sent: an amount that arrives as an argument is an amount the buyer\n * chose. Return null for a reference you do not sell this way.\n *\n * The default throws rather than pricing anything — a payment flow that\n * silently no-ops is worse than one that is obviously unbound.\n *\n * An implementation, over the plan catalogue:\n *\n * const plan = getPlanBySlug(reference);\n * if (!plan) return null;\n * return {\n * price: fromMajor(plan.priceOneTime, CURRENCY),\n * grant: { plan: plan.slug },\n * description: plan.name,\n * };\n *\n * or one of the pricing item's `CREDIT_BUNDLES`, sold from\n * `lib/credit-bundle-config.tsx` under a `credits:` reference so a bundle\n * id never reads as a plan slug. The provider charges in `CURRENCY`: a\n * bundle priced in another currency (the card processor's) is not sold\n * this way — `money(bundle.price…)` would hand the provider a number in\n * the wrong unit.\n *\n * if (reference.startsWith(\"credits:\")) {\n * const bundle = getCreditBundle(reference.slice(\"credits:\".length));\n * if (!bundle || !(\"grant\" in bundle)) return null;\n * if (bundle.price.currency !== CURRENCY) return null;\n * return {\n * price: money(bundle.price.amount, bundle.price.currency),\n * grant: { credits: money(bundle.grant.amount, bundle.grant.currency) },\n * description: bundle.name,\n * };\n * }\n *\n * A grant of credits is in the deployment's billing currency. A provider\n * registered with its `currency` refuses a price in any other.\n */\n\nimport type { LocalPaymentOffer } from \"@intelligo-dev/billing\";\n\nexport async function priceLocalPayment(\n _reference: string\n): Promise<LocalPaymentOffer | null> {\n throw new Error(\n \"priceLocalPayment is not bound. Implement it in lib/local-payment.ts \" +\n \"with what each reference costs and grants, or remove the payment-poll item.\"\n );\n}\n",
|
|
35
41
|
"type": "registry:file",
|
|
36
42
|
"target": "lib/local-payment.ts"
|
|
37
43
|
},
|
|
38
44
|
{
|
|
39
45
|
"path": "base/payment-poll/lib/payment-poll-config.ts",
|
|
40
|
-
"content": "/**\n * Polling behaviour for the QR payment modal — consumer-owned.\n *\n * `pollIntervalMs`: how often to ask the provider whether the invoice\n * settled. Three seconds is a compromise: fast enough that the success\n * screen feels immediate, slow enough not to hammer a provider that\n * rate-limits status reads.\n *\n * `timeoutMs`: when to stop asking and offer a retry instead. Five\n * minutes is long enough for someone to switch apps, log into their\n * bank, and confirm — the common case that a shorter timeout would\n * cut off mid-payment.\n */\n\nexport interface PaymentPollConfig {\n pollIntervalMs: number;\n timeoutMs: number;\n}\n\nexport const paymentPollConfig: PaymentPollConfig = {\n pollIntervalMs: 3_000,\n timeoutMs: 5 * 60_000,\n};\n",
|
|
46
|
+
"content": "/**\n * Polling behaviour for the QR payment modal — consumer-owned.\n *\n * `pollIntervalMs`: how often to ask the provider whether the invoice\n * settled. Three seconds is a compromise: fast enough that the success\n * screen feels immediate, slow enough not to hammer a provider that\n * rate-limits status reads.\n *\n * `timeoutMs`: when to stop asking and offer a retry instead. Five\n * minutes is long enough for someone to switch apps, log into their\n * bank, and confirm — the common case that a shorter timeout would\n * cut off mid-payment.\n *\n * `payerRoles`: who in a workspace may open an invoice. A payment buys\n * the workspace a plan or credit, so by default only an owner may, as\n * with card checkout.\n */\n\nimport type { WorkspaceRole } from \"@intelligo-dev/auth\";\n\nexport interface PaymentPollConfig {\n pollIntervalMs: number;\n timeoutMs: number;\n payerRoles?: WorkspaceRole[];\n}\n\nexport const paymentPollConfig: PaymentPollConfig = {\n pollIntervalMs: 3_000,\n timeoutMs: 5 * 60_000,\n payerRoles: [\"owner\"],\n};\n",
|
|
41
47
|
"type": "registry:file",
|
|
42
48
|
"target": "lib/payment-poll-config.ts"
|
|
43
49
|
},
|
|
44
50
|
{
|
|
45
51
|
"path": "base/payment-poll/messages/en.json",
|
|
46
|
-
"content": "{\n \"title\": \"{label} — {amount}\",\n \"qrAlt\": \"Payment QR code\",\n \"orChooseApp\": \"Or open your banking app\",\n \"timedOutHelp\": \"We stopped checking after a few minutes. If you've already paid, it may still land — otherwise start a new payment.\",\n \"retry\": \"Try again\",\n \"close\": \"Close\",\n \"paidTitle\": \"Payment received\",\n \"paidBody\": \"{label} is active.\",\n \"failedTitle\": \"Payment failed\",\n \"state\": {\n \"creating\": \"Preparing your payment…\",\n \"waiting\": \"Scan the code or choose your bank\",\n \"polling\": \"Waiting for payment…\",\n \"timedOut\": \"Still waiting?\",\n \"paid\": \"All set\",\n \"failed\": \"Something went wrong\"\n },\n \"errors\": {\n \"unauthorized\": \"Please sign in and try again.\",\n \"unavailable\": \"This can't be paid for this way.\",\n \"createFailed\": \"We couldn't start the payment. Try again in a moment.\",\n \"statusFailed\": \"We couldn't check the payment status.\",\n \"declined\": \"The payment was declined.\"\n }\n}\n",
|
|
52
|
+
"content": "{\n \"button\": \"Pay with QR\",\n \"title\": \"{label} — {amount}\",\n \"qrAlt\": \"Payment QR code\",\n \"orChooseApp\": \"Or open your banking app\",\n \"timedOutHelp\": \"We stopped checking after a few minutes. If you've already paid, it may still land — otherwise start a new payment.\",\n \"retry\": \"Try again\",\n \"close\": \"Close\",\n \"paidTitle\": \"Payment received\",\n \"paidBody\": \"{label} is active.\",\n \"failedTitle\": \"Payment failed\",\n \"state\": {\n \"creating\": \"Preparing your payment…\",\n \"waiting\": \"Scan the code or choose your bank\",\n \"polling\": \"Waiting for payment…\",\n \"timedOut\": \"Still waiting?\",\n \"paid\": \"All set\",\n \"failed\": \"Something went wrong\"\n },\n \"errors\": {\n \"unauthorized\": \"Please sign in and try again.\",\n \"unavailable\": \"This can't be paid for this way.\",\n \"createFailed\": \"We couldn't start the payment. Try again in a moment.\",\n \"statusFailed\": \"We couldn't check the payment status.\",\n \"declined\": \"The payment was declined.\",\n \"forbidden\": \"Only a workspace owner can pay for this.\"\n }\n}\n",
|
|
47
53
|
"type": "registry:file",
|
|
48
54
|
"target": "messages/en/payment-poll.json"
|
|
49
55
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
3
|
"name": "pricing",
|
|
4
4
|
"title": "Pricing",
|
|
5
|
-
"description": "Plan comparison and upgrade page with a monthly/yearly toggle, backed by the framework's checkout service and the product's registered plan catalogue. Ships the lib/billing.ts product binding and actions/billing.ts checkout actions that the checkout and billing-settings items also depend on — install this item first.",
|
|
5
|
+
"description": "Plan comparison and upgrade page with a monthly/yearly toggle, backed by the framework's checkout service and the product's registered plan catalogue. Ships the lib/billing.ts product binding and actions/billing.ts checkout actions that the checkout and billing-settings items also depend on — install this item first. Other ways to buy a plan render under its checkout button from the lib/plan-card-config.tsx seam, empty by default (the payment-poll item's LocalPaymentButton binds there).",
|
|
6
6
|
"dependencies": [
|
|
7
7
|
"@intelligo-dev/core",
|
|
8
8
|
"@intelligo-dev/auth",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"path": "base/pricing/components/plan-card.tsx",
|
|
51
|
-
"content": "\"use client\";\n\n/**\n * One plan's pricing card: name, price, feature list, and a checkout\n * CTA whose state depends on whether it's the workspace's current\n * plan, the free plan, or something the caller can afford to buy.\n *\n * The catalogue has no tier order, so every non-current, non-free plan\n * gets the same \"switch to this plan\" button. The plan's name,\n * description and features come from the deployment's `plans` messages\n * when it translates them, and from the catalogue otherwise\n * (`lib/plan-copy.ts`)
|
|
51
|
+
"content": "\"use client\";\n\n/**\n * One plan's pricing card: name, price, feature list, and a checkout\n * CTA whose state depends on whether it's the workspace's current\n * plan, the free plan, or something the caller can afford to buy.\n *\n * The catalogue has no tier order, so every non-current, non-free plan\n * gets the same \"switch to this plan\" button. The plan's name,\n * description and features come from the deployment's `plans` messages\n * when it translates them, and from the catalogue otherwise\n * (`lib/plan-copy.ts`). Other ways to buy the plan, such as a QR\n * payment, render under the checkout button from `lib/plan-card-config.tsx`.\n */\n\nimport { Check } from \"lucide-react\";\nimport { useFormatter, useTranslations } from \"next-intl\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card } from \"@/components/ui/card\";\n\nimport { CheckoutButton } from \"./checkout-button\";\nimport { CURRENCY } from \"@/lib/billing-config\";\nimport { planCardConfig } from \"@/lib/plan-card-config\";\nimport { planCopy } from \"@/lib/plan-copy\";\nimport type { PlanConfig } from \"@intelligo-dev/billing/plans\";\n\ninterface PlanCardProps {\n plan: PlanConfig;\n currentPlanSlug: string;\n interval: \"monthly\" | \"yearly\";\n /** Whether the signed-in caller is allowed to start checkout (owner-only). */\n canCheckout: boolean;\n isRecommended?: boolean;\n}\n\nexport function PlanCard({\n plan,\n currentPlanSlug,\n interval,\n canCheckout,\n isRecommended = false,\n}: PlanCardProps) {\n const t = useTranslations(\"pricing\");\n const tPlans = useTranslations(\"plans\");\n const format = useFormatter();\n const copy = planCopy(tPlans, plan);\n const isCurrent = plan.slug === currentPlanSlug;\n const Actions = planCardConfig.actions;\n\n // A plan with interval prices is quoted per period; one without is a\n // single purchase, and the toggle above is hidden for it entirely\n // (see pricing-content.tsx) so the label can't contradict the price.\n const intervalPrice =\n interval === \"monthly\" ? plan.priceMonthly : plan.priceYearly;\n const price = intervalPrice ?? plan.priceOneTime;\n const isFree = price === 0;\n const periodLabel =\n intervalPrice === undefined\n ? t(\"planCard.oneTime\")\n : interval === \"monthly\"\n ? t(\"planCard.perMonth\")\n : t(\"planCard.perYear\");\n\n // Money is never rendered by a package helper: currency is a\n // deployment decision (`CURRENCY` in lib/billing-config.ts), and\n // \"free\" is copy, not a formatted zero.\n const priceLabel = isFree\n ? t(\"planCard.free\")\n : format.number(price, {\n style: \"currency\",\n currency: CURRENCY,\n maximumFractionDigits: 0,\n });\n\n return (\n <Card\n className={`relative flex h-full flex-col p-6 ${\n isRecommended\n ? \"border-2 border-foreground/30 shadow-lg dark:border-foreground/40\"\n : \"\"\n }`}\n >\n {isRecommended && (\n <Badge className=\"absolute -top-3 left-1/2 -translate-x-1/2 bg-foreground text-background\">\n {t(\"planCard.recommended\")}\n </Badge>\n )}\n\n <div className=\"flex flex-1 flex-col space-y-6\">\n <div className=\"space-y-2\">\n <div className=\"flex items-center justify-between\">\n <h3 className=\"text-xl font-bold text-foreground\">{copy.name}</h3>\n {isCurrent && (\n <Badge className=\"border border-border bg-accent text-muted-foreground\">\n {t(\"planCard.current\")}\n </Badge>\n )}\n </div>\n <p className=\"text-sm text-muted-foreground\">{copy.description}</p>\n </div>\n\n <div className=\"space-y-1\">\n <div className=\"flex items-baseline gap-1\">\n <span className=\"text-4xl font-bold text-foreground\">\n {priceLabel}\n </span>\n {!isFree && (\n <span className=\"text-sm text-muted-foreground\">\n {periodLabel}\n </span>\n )}\n </div>\n </div>\n\n <ul className=\"space-y-3\">\n {copy.features.map((feature) => (\n <li key={feature} className=\"flex items-start gap-2\">\n <Check className=\"mt-0.5 size-4 flex-shrink-0 text-muted-foreground\" />\n <span className=\"text-sm text-foreground\">{feature}</span>\n </li>\n ))}\n </ul>\n\n <div className=\"mt-auto pt-2\">\n {isCurrent ? (\n // Outline, not the default fill: the plan you are already\n // on is a statement of fact, and a solid disabled button\n // reads as the page's primary action greyed out.\n <Button disabled variant=\"outline\" className=\"w-full\">\n {t(\"planCard.currentPlanButton\")}\n </Button>\n ) : isFree ? (\n <div className=\"py-2 text-center\">\n <p className=\"text-sm text-muted-foreground\">\n {t(\"planCard.freeForever\")}\n </p>\n </div>\n ) : !canCheckout ? (\n <Button variant=\"outline\" disabled className=\"w-full\">\n {t(\"planCard.askOwner\")}\n </Button>\n ) : (\n <div className=\"space-y-2\">\n <CheckoutButton\n planSlug={plan.slug}\n interval={interval}\n className=\"w-full\"\n >\n {t(\"planCard.switchTo\", { planName: copy.name })}\n </CheckoutButton>\n {Actions ? (\n <Actions\n plan={plan}\n interval={interval}\n price={price}\n planName={copy.name}\n />\n ) : null}\n </div>\n )}\n </div>\n </div>\n </Card>\n );\n}\n",
|
|
52
52
|
"type": "registry:component",
|
|
53
53
|
"target": "components/billing/plan-card.tsx"
|
|
54
54
|
},
|
|
@@ -94,6 +94,12 @@
|
|
|
94
94
|
"type": "registry:file",
|
|
95
95
|
"target": "lib/plan-copy.ts"
|
|
96
96
|
},
|
|
97
|
+
{
|
|
98
|
+
"path": "base/pricing/lib/plan-card-config.tsx",
|
|
99
|
+
"content": "/**\n * What your product adds to a plan card, without editing\n * `components/billing/plan-card.tsx`.\n *\n * - `actions`: rendered under the card's checkout button, as another\n * way to buy the same plan (a QR payment rail, a bank transfer, a\n * \"talk to sales\" link). It appears only where the checkout button\n * does: on a priced plan that is not the workspace's current one,\n * for a caller allowed to change plans. It receives the plan, the\n * selected interval, the price the card shows (major units of\n * `CURRENCY`) and the plan's localized name.\n *\n * Empty by default: the card offers card checkout alone. To offer the\n * QR-and-poll rail, install the `payment-poll` item, price each plan in\n * its `lib/local-payment.ts`, and bind its button here:\n *\n * import { LocalPaymentButton } from \"@/components/billing/local-payment-button\";\n *\n * export const planCardConfig: PlanCardConfig = {\n * actions: ({ plan, price, planName }) => (\n * <LocalPaymentButton reference={plan.slug} amount={price} label={planName} />\n * ),\n * };\n *\n * The amount is only displayed; `priceLocalPayment(reference)` prices\n * the invoice on the server. Encode the interval in the reference\n * (`${plan.slug}:${interval}`) when the two prices differ.\n */\n\nimport type { ComponentType } from \"react\";\n\nimport type { PlanConfig } from \"@intelligo-dev/billing/plans\";\n\nexport interface PlanCardActionProps {\n plan: PlanConfig;\n interval: \"monthly\" | \"yearly\";\n /** The price the card shows, in major units of `CURRENCY`. */\n price: number;\n /** The plan's name as the card renders it. */\n planName: string;\n}\n\nexport interface PlanCardConfig {\n /** Rendered under the checkout button of a plan the caller can buy. */\n actions?: ComponentType<PlanCardActionProps>;\n}\n\nexport const planCardConfig: PlanCardConfig = {};\n",
|
|
100
|
+
"type": "registry:file",
|
|
101
|
+
"target": "lib/plan-card-config.tsx"
|
|
102
|
+
},
|
|
97
103
|
{
|
|
98
104
|
"path": "base/pricing/messages/en.json",
|
|
99
105
|
"content": "{\n \"meta\": {\n \"title\": \"Pricing\"\n },\n \"page\": {\n \"title\": \"Pricing\",\n \"description\": \"Start free, upgrade when you need more.\"\n },\n \"planCard\": {\n \"recommended\": \"Recommended\",\n \"current\": \"Current\",\n \"oneTime\": \"one-time\",\n \"currentPlanButton\": \"Current plan\",\n \"freeForever\": \"Free forever\",\n \"askOwner\": \"Ask an owner to change plans\",\n \"switchTo\": \"Switch to {planName}\",\n \"free\": \"Free\",\n \"perMonth\": \"per month\",\n \"perYear\": \"per year\"\n },\n \"intervalToggle\": {\n \"monthly\": \"Monthly\",\n \"yearly\": \"Yearly\"\n },\n \"checkoutButton\": {\n \"redirecting\": \"Redirecting…\"\n },\n \"content\": {\n \"canceled\": \"Checkout was canceled. You were not charged.\"\n },\n \"error\": {\n \"title\": \"Something went wrong while loading pricing. Please try again.\",\n \"tryAgain\": \"Try again\",\n \"goHome\": \"Go home\"\n },\n \"errors\": {\n \"invalidPlan\": \"That plan isn't available.\",\n \"checkoutUnavailable\": \"This plan isn't set up for checkout yet. Contact support.\",\n \"invalidBundle\": \"That credit bundle isn't available.\",\n \"noBillingAccount\": \"No billing account found. Subscribe to a plan or purchase credits first.\",\n \"providerError\": \"Something went wrong. Please try again.\",\n \"invalidInput\": \"Invalid input\",\n \"genericFailure\": \"Something went wrong.\"\n }\n}\n",
|
|
@@ -1954,7 +1954,7 @@
|
|
|
1954
1954
|
"name": "pricing",
|
|
1955
1955
|
"type": "registry:block",
|
|
1956
1956
|
"title": "Pricing",
|
|
1957
|
-
"description": "Plan comparison and upgrade page with a monthly/yearly toggle, backed by the framework's checkout service and the product's registered plan catalogue. Ships the lib/billing.ts product binding and actions/billing.ts checkout actions that the checkout and billing-settings items also depend on — install this item first.",
|
|
1957
|
+
"description": "Plan comparison and upgrade page with a monthly/yearly toggle, backed by the framework's checkout service and the product's registered plan catalogue. Ships the lib/billing.ts product binding and actions/billing.ts checkout actions that the checkout and billing-settings items also depend on — install this item first. Other ways to buy a plan render under its checkout button from the lib/plan-card-config.tsx seam, empty by default (the payment-poll item's LocalPaymentButton binds there).",
|
|
1958
1958
|
"registryDependencies": [
|
|
1959
1959
|
"alert",
|
|
1960
1960
|
"badge",
|
|
@@ -2034,6 +2034,11 @@
|
|
|
2034
2034
|
"type": "registry:file",
|
|
2035
2035
|
"target": "lib/plan-copy.ts"
|
|
2036
2036
|
},
|
|
2037
|
+
{
|
|
2038
|
+
"path": "base/pricing/lib/plan-card-config.tsx",
|
|
2039
|
+
"type": "registry:file",
|
|
2040
|
+
"target": "lib/plan-card-config.tsx"
|
|
2041
|
+
},
|
|
2037
2042
|
{
|
|
2038
2043
|
"path": "base/pricing/messages/en.json",
|
|
2039
2044
|
"type": "registry:file",
|
|
@@ -2086,7 +2091,7 @@
|
|
|
2086
2091
|
"name": "billing-settings",
|
|
2087
2092
|
"type": "registry:block",
|
|
2088
2093
|
"title": "Billing Settings",
|
|
2089
|
-
"description": "Workspace billing management: current plan, credit balance, credit bundle purchase, and the Stripe billing portal, shaped server-side by role (member/admin/owner). Requires the pricing item (provides lib/billing.ts and actions/billing.ts); install it first.",
|
|
2094
|
+
"description": "Workspace billing management: current plan, credit balance, credit bundle purchase, and the Stripe billing portal, shaped server-side by role (member/admin/owner). Requires the pricing item (provides lib/billing.ts and actions/billing.ts); install it first. Other ways to buy a credit bundle render under its button from the lib/credit-bundle-config.tsx seam, which can also hide the card button (the payment-poll item's LocalPaymentButton binds there).",
|
|
2090
2095
|
"registryDependencies": [
|
|
2091
2096
|
"alert",
|
|
2092
2097
|
"@intelligo/button",
|
|
@@ -2132,6 +2137,11 @@
|
|
|
2132
2137
|
"path": "base/billing-settings/error.tsx",
|
|
2133
2138
|
"type": "registry:page",
|
|
2134
2139
|
"target": "app/[locale]/(app)/settings/billing/error.tsx"
|
|
2140
|
+
},
|
|
2141
|
+
{
|
|
2142
|
+
"path": "base/billing-settings/lib/credit-bundle-config.tsx",
|
|
2143
|
+
"type": "registry:file",
|
|
2144
|
+
"target": "lib/credit-bundle-config.tsx"
|
|
2135
2145
|
}
|
|
2136
2146
|
]
|
|
2137
2147
|
},
|
|
@@ -2198,7 +2208,7 @@
|
|
|
2198
2208
|
"name": "payment-poll",
|
|
2199
2209
|
"type": "registry:block",
|
|
2200
2210
|
"title": "QR Payment (poll)",
|
|
2201
|
-
"description": "The non-card checkout the catalogue was missing: issue an invoice, show a QR the user scans in their banking app, poll until the provider confirms. Provider-agnostic — QPay, SocialPay, PIX, UPI, PromptPay all fit — with the invoice recorded and a paid one granted on the server; lib/local-payment.ts says what each reference costs and grants. Requires the pricing item for lib/billing-config.ts's CURRENCY.",
|
|
2211
|
+
"description": "The non-card checkout the catalogue was missing: issue an invoice, show a QR the user scans in their banking app, poll until the provider confirms. Provider-agnostic — QPay, SocialPay, PIX, UPI, PromptPay all fit — with the invoice recorded and a paid one granted on the server; lib/local-payment.ts says what each reference costs and grants. Requires the pricing item for lib/billing-config.ts's CURRENCY. LocalPaymentButton opens the modal for one reference and refreshes the page once paid; bind it in the pricing item's lib/plan-card-config.tsx to offer the QR rail on every plan card, and in the billing-settings item's lib/credit-bundle-config.tsx on every credit bundle.",
|
|
2202
2212
|
"registryDependencies": [
|
|
2203
2213
|
"@intelligo/button",
|
|
2204
2214
|
"@intelligo/dialog",
|
|
@@ -2218,6 +2228,11 @@
|
|
|
2218
2228
|
"type": "registry:component",
|
|
2219
2229
|
"target": "components/billing/local-payment-modal.tsx"
|
|
2220
2230
|
},
|
|
2231
|
+
{
|
|
2232
|
+
"path": "base/payment-poll/components/local-payment-button.tsx",
|
|
2233
|
+
"type": "registry:component",
|
|
2234
|
+
"target": "components/billing/local-payment-button.tsx"
|
|
2235
|
+
},
|
|
2221
2236
|
{
|
|
2222
2237
|
"path": "base/payment-poll/actions.ts",
|
|
2223
2238
|
"type": "registry:file",
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
{
|
|
18
18
|
"path": "base/trial-banner/components/trial-banner-container.tsx",
|
|
19
|
-
"content": "/**\n * Trial banner container — the intended `bannerTop` binding for the\n * app-shell's `ShellConfig` seam (`@/lib/shell-config`).\n *\n * `shellConfig.bannerTop` is rendered with no props (inside\n * `SidebarInset`, above `main`), so this wrapper resolves the active\n * workspace, fetches its trial status, and decides whether the client\n * `TrialBanner` renders at all. An async Server Component is a valid\n * `bannerTop` binding — React 19's `FunctionComponent` allows\n * `Promise<ReactNode>` — so the fetch happens server-side rather than\n * through a client round trip.\n *\n * A trial that isn't active (none, converted, depleted, expired)\n * renders nothing; so does a fetch failure — a broken banner lookup\n * must never take the shell down with it.\n *\n * Route suppression (e.g. keeping the banner out of the chat surface)\n * is client-side in `TrialBanner` via `trialBannerConfig.hideOnPaths` —\n * a Server Component outside the `[locale]` segment has no reliable\n * pathname to branch on.\n */\n\nimport { getWorkspaceContext } from \"@intelligo-dev/auth\";\nimport { getTrialStatus } from \"@intelligo-dev/billing\";\nimport { createLogger } from \"@intelligo-dev/core/logger\";\n\nimport { TrialBanner } from \"./trial-banner\";\n\nconst log = createLogger(\"TrialBannerContainer\");\n\nexport async function TrialBannerContainer() {\n const workspace = await getWorkspaceContext();\n if (!workspace?.workspace?.id) return null;\n\n try {\n const trial = await getTrialStatus(workspace.workspace.id);\n if (\n trial.status !== \"active\" ||\n trial.isExpired ||\n trial.daysRemaining <= 0\n ) {\n return null;\n }\n\n return (\n <TrialBanner\n daysRemaining={trial.daysRemaining}\n creditsRemaining={trial.creditsRemaining}\n initialCredits={trial.initialCredits}\n />\n );\n } catch (error) {\n log.error(\"Failed to fetch trial status\", {\n error: error instanceof Error ? error.message : String(error),\n });\n return null;\n }\n}\n",
|
|
19
|
+
"content": "/**\n * Trial banner container — the intended `bannerTop` binding for the\n * app-shell's `ShellConfig` seam (`@/lib/shell-config`).\n *\n * `shellConfig.bannerTop` is rendered with no props (inside\n * `SidebarInset`, above `main`), so this wrapper resolves the active\n * workspace, fetches its trial status, and decides whether the client\n * `TrialBanner` renders at all. An async Server Component is a valid\n * `bannerTop` binding — React 19's `FunctionComponent` allows\n * `Promise<ReactNode>` — so the fetch happens server-side rather than\n * through a client round trip.\n *\n * A trial that isn't active (none, converted, depleted, expired)\n * renders nothing; so does a fetch failure — a broken banner lookup\n * must never take the shell down with it.\n *\n * The credit counts are formatted here, on the server, and reach the\n * client as strings: compact notation (\"1.2K\") is spelled by the ICU\n * data of whichever runtime formats it, and Node's and a browser's\n * differ for many locales — formatting on both sides would render one\n * string on the server and another at hydration.\n *\n * Route suppression (e.g. keeping the banner out of the chat surface)\n * is client-side in `TrialBanner` via `trialBannerConfig.hideOnPaths` —\n * a Server Component outside the `[locale]` segment has no reliable\n * pathname to branch on.\n */\n\nimport { getWorkspaceContext } from \"@intelligo-dev/auth\";\nimport { getTrialStatus } from \"@intelligo-dev/billing\";\nimport { createLogger } from \"@intelligo-dev/core/logger\";\nimport { getFormatter } from \"next-intl/server\";\n\nimport { TrialBanner } from \"./trial-banner\";\n\nconst log = createLogger(\"TrialBannerContainer\");\n\nexport async function TrialBannerContainer() {\n const workspace = await getWorkspaceContext();\n if (!workspace?.workspace?.id) return null;\n\n try {\n const trial = await getTrialStatus(workspace.workspace.id);\n if (\n trial.status !== \"active\" ||\n trial.isExpired ||\n trial.daysRemaining <= 0\n ) {\n return null;\n }\n\n const format = await getFormatter();\n const compact = (value: number) =>\n format.number(value, { notation: \"compact\", maximumFractionDigits: 1 });\n\n return (\n <TrialBanner\n daysRemaining={trial.daysRemaining}\n creditsRemaining={compact(trial.creditsRemaining)}\n initialCredits={compact(trial.initialCredits)}\n />\n );\n } catch (error) {\n log.error(\"Failed to fetch trial status\", {\n error: error instanceof Error ? error.message : String(error),\n });\n return null;\n }\n}\n",
|
|
20
20
|
"type": "registry:component",
|
|
21
21
|
"target": "components/trial/trial-banner-container.tsx"
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"path": "base/trial-banner/components/trial-banner.tsx",
|
|
25
|
-
"content": "\"use client\";\n\n/**\n * Trial banner — a dismissable strip above the app shell's content\n * showing days and credits remaining, with an upgrade CTA.\n *\n * Urgency ramps the visual weight with semantic tokens: normal (>3 days) sits on `primary`, warning (≤3 days) on\n * `foreground`/`muted`, urgent (≤1 day) on `destructive`. Dismissing\n * hides the banner for the rest of the day (localStorage; per-browser,\n * deliberately not server state — a nudge, not a notification).\n *\n * `trialBannerConfig` (consumer-owned `lib/trial-banner-config.ts`)\n * decides where the upgrade CTA points and which route prefixes\n * suppress the banner entirely — the default keeps it out of `/chat`,\n * where a persistent strip over a conversation costs the most.\n */\n\nimport { useEffect, useState } from \"react\";\nimport { Sparkles, X } from \"lucide-react\";\nimport {
|
|
25
|
+
"content": "\"use client\";\n\n/**\n * Trial banner — a dismissable strip above the app shell's content\n * showing days and credits remaining, with an upgrade CTA.\n *\n * Urgency ramps the visual weight with semantic tokens: normal (>3 days) sits on `primary`, warning (≤3 days) on\n * `foreground`/`muted`, urgent (≤1 day) on `destructive`. Dismissing\n * hides the banner for the rest of the day (localStorage; per-browser,\n * deliberately not server state — a nudge, not a notification).\n *\n * `trialBannerConfig` (consumer-owned `lib/trial-banner-config.ts`)\n * decides where the upgrade CTA points and which route prefixes\n * suppress the banner entirely — the default keeps it out of `/chat`,\n * where a persistent strip over a conversation costs the most.\n */\n\nimport { useEffect, useState } from \"react\";\nimport { Sparkles, X } from \"lucide-react\";\nimport { useTranslations } from \"next-intl\";\n\nimport { Link, usePathname } from \"@/i18n/navigation\";\nimport { Button } from \"@/components/ui/button\";\nimport { trialBannerConfig } from \"@/lib/trial-banner-config\";\n\nconst DISMISSED_KEY = \"trial-banner-dismissed\";\n\n/** Local calendar day, as a stable `YYYY-MM-DD` storage key. */\nfunction today(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\ntype TrialBannerProps = {\n daysRemaining: number;\n /**\n * Credit counts already formatted for display, by the server\n * (`TrialBannerContainer`). Formatting them here would run Intl twice —\n * Node's ICU during SSR, the browser's at hydration — and the two\n * disagree on compact notation for many locales.\n */\n creditsRemaining: string;\n initialCredits: string;\n};\n\nexport function TrialBanner({\n daysRemaining,\n creditsRemaining,\n initialCredits,\n}: TrialBannerProps) {\n const t = useTranslations(\"trial-banner\");\n const pathname = usePathname();\n const [isVisible, setIsVisible] = useState(true);\n\n // Dismissed-today check runs in an effect: localStorage doesn't exist\n // during SSR, and reading it during render would desync hydration.\n useEffect(() => {\n if (localStorage.getItem(DISMISSED_KEY) === today()) {\n setIsVisible(false);\n }\n }, []);\n\n const hidden = trialBannerConfig.hideOnPaths.some(\n (prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`)\n );\n if (hidden || !isVisible) return null;\n\n function handleDismiss() {\n localStorage.setItem(DISMISSED_KEY, today());\n setIsVisible(false);\n }\n\n const isUrgent = daysRemaining <= 1;\n const isWarning = daysRemaining > 1 && daysRemaining <= 3;\n\n const strip = isUrgent\n ? \"bg-destructive/10 border-destructive/20\"\n : isWarning\n ? \"bg-muted border-border\"\n : \"bg-primary/5 border-primary/20\";\n const accent = isUrgent\n ? \"text-destructive\"\n : isWarning\n ? \"text-foreground\"\n : \"text-primary\";\n const chip = isUrgent\n ? \"bg-destructive/10 text-destructive\"\n : isWarning\n ? \"bg-foreground/10 text-foreground\"\n : \"bg-primary/10 text-primary\";\n\n return (\n <div className={`relative w-full border-b transition-colors ${strip}`}>\n <div className=\"container mx-auto px-4 py-3\">\n <div className=\"flex items-center justify-between gap-4\">\n <div className=\"flex items-center gap-3\">\n <Sparkles className={`size-5 ${accent}`} aria-hidden />\n <div className=\"flex flex-col sm:flex-row sm:items-center sm:gap-2\">\n <div className=\"flex items-center gap-2\">\n <span\n className={`inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${chip}`}\n >\n {t(\"label\")}\n </span>\n <span className={`text-sm font-semibold ${accent}`}>\n {daysRemaining === 0\n ? t(\"expiresToday\")\n : t(\"daysRemaining\", { days: daysRemaining })}\n </span>\n </div>\n <span className={`text-xs ${accent}`}>\n {t(\"creditsLeft\", {\n remaining: creditsRemaining,\n initial: initialCredits,\n })}\n </span>\n </div>\n </div>\n\n <div className=\"flex items-center gap-2\">\n <Button\n size=\"sm\"\n variant={isUrgent ? \"destructive\" : \"default\"}\n render={<Link href={trialBannerConfig.upgradeHref} />}\n nativeButton={false}\n >\n {t(\"upgrade\")}\n </Button>\n <button\n type=\"button\"\n onClick={handleDismiss}\n className=\"rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted\"\n aria-label={t(\"dismiss\")}\n >\n <X className=\"size-4\" />\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n}\n",
|
|
26
26
|
"type": "registry:component",
|
|
27
27
|
"target": "components/trial/trial-banner.tsx"
|
|
28
28
|
},
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
},
|
|
59
59
|
"pricing": {
|
|
60
60
|
"title": "Pricing",
|
|
61
|
-
"description": "Plan comparison and upgrade page with a monthly/yearly toggle, backed by the framework's checkout service and the product's registered plan catalogue. Ships the lib/billing.ts product binding and actions/billing.ts checkout actions that the checkout and billing-settings items also depend on — install this item first."
|
|
61
|
+
"description": "Plan comparison and upgrade page with a monthly/yearly toggle, backed by the framework's checkout service and the product's registered plan catalogue. Ships the lib/billing.ts product binding and actions/billing.ts checkout actions that the checkout and billing-settings items also depend on — install this item first. Other ways to buy a plan render under its checkout button from the lib/plan-card-config.tsx seam, empty by default (the payment-poll item's LocalPaymentButton binds there)."
|
|
62
62
|
},
|
|
63
63
|
"checkout": {
|
|
64
64
|
"title": "Checkout",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
},
|
|
67
67
|
"billing-settings": {
|
|
68
68
|
"title": "Billing Settings",
|
|
69
|
-
"description": "Workspace billing management: current plan, credit balance, credit bundle purchase, and the Stripe billing portal, shaped server-side by role (member/admin/owner). Requires the pricing item (provides lib/billing.ts and actions/billing.ts); install it first."
|
|
69
|
+
"description": "Workspace billing management: current plan, credit balance, credit bundle purchase, and the Stripe billing portal, shaped server-side by role (member/admin/owner). Requires the pricing item (provides lib/billing.ts and actions/billing.ts); install it first. Other ways to buy a credit bundle render under its button from the lib/credit-bundle-config.tsx seam, which can also hide the card button (the payment-poll item's LocalPaymentButton binds there)."
|
|
70
70
|
},
|
|
71
71
|
"feature-gating": {
|
|
72
72
|
"title": "Feature Gating",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
},
|
|
75
75
|
"payment-poll": {
|
|
76
76
|
"title": "QR Payment (poll)",
|
|
77
|
-
"description": "The non-card checkout the catalogue was missing: issue an invoice, show a QR the user scans in their banking app, poll until the provider confirms. Provider-agnostic — QPay, SocialPay, PIX, UPI, PromptPay all fit — with the invoice recorded and a paid one granted on the server; lib/local-payment.ts says what each reference costs and grants. Requires the pricing item for lib/billing-config.ts's CURRENCY."
|
|
77
|
+
"description": "The non-card checkout the catalogue was missing: issue an invoice, show a QR the user scans in their banking app, poll until the provider confirms. Provider-agnostic — QPay, SocialPay, PIX, UPI, PromptPay all fit — with the invoice recorded and a paid one granted on the server; lib/local-payment.ts says what each reference costs and grants. Requires the pricing item for lib/billing-config.ts's CURRENCY. LocalPaymentButton opens the modal for one reference and refreshes the page once paid; bind it in the pricing item's lib/plan-card-config.tsx to offer the QR rail on every plan card, and in the billing-settings item's lib/credit-bundle-config.tsx on every credit bundle."
|
|
78
78
|
},
|
|
79
79
|
"profile-settings": {
|
|
80
80
|
"title": "Profile Settings",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "Machine-readable requirements of every registry item \u2014 what shadcn's schema cannot express. `scaffold`: consumer files `intelligo create` provides that items may import. Per item: `marker` (the file whose presence means the item is installed), `items` (sibling items that ship files this one imports \u2014 install them first), `files` (scaffold files it imports), `exports` (names a scaffold file must export), `features` (feature keys the item
|
|
2
|
+
"$comment": "Machine-readable requirements of every registry item \u2014 what shadcn's schema cannot express. `scaffold`: consumer files `intelligo create` provides that items may import. Per item: `marker` (the file whose presence means the item is installed), `items` (sibling items that ship files this one imports \u2014 install them first), `files` (scaffold files it imports), `exports` (names a scaffold file must export), `features` (feature keys the item's non-seam files gate on; register them in lib/plans.ts \u2014 a key a seam names is the deployment's to change). `seams`: files an item ships once and the deployment then owns, each with what it configures \u2014 `intelligo sync` never overwrites one, and message files (`messages/`) are merged key by key, the deployment's copy winning. tests/architecture/registry.test.ts asserts this file matches the code; CI derives the install order from it; `intelligo doctor` checks an app against it (packages/cli/templates/registry-requires.json is a copy the CLI build regenerates; the suite fails if it is stale).",
|
|
3
3
|
"scaffold": [
|
|
4
4
|
"lib/intelligo",
|
|
5
5
|
"lib/plans",
|
|
@@ -9,15 +9,17 @@
|
|
|
9
9
|
"i18n/navigation"
|
|
10
10
|
],
|
|
11
11
|
"seams": {
|
|
12
|
-
"lib/shell-config.tsx": "shell banner and header slots",
|
|
12
|
+
"lib/shell-config.tsx": "shell banner and header slots, and where unfinished onboarding goes",
|
|
13
13
|
"lib/nav-config.ts": "navigation",
|
|
14
14
|
"lib/settings-nav.ts": "settings tabs",
|
|
15
15
|
"lib/error-reporting.ts": "the error reporter",
|
|
16
16
|
"lib/billing-config.ts": "product slug, currency, credit bundles",
|
|
17
|
+
"lib/credit-bundle-config.tsx": "other ways to buy a credit bundle, and whether card checkout shows",
|
|
18
|
+
"lib/plan-card-config.tsx": "other ways to buy a plan on its pricing card",
|
|
17
19
|
"lib/feature-catalog.ts": "feature names and plans",
|
|
18
20
|
"lib/feature-gating-config.ts": "upgrade targets for gated features",
|
|
19
21
|
"lib/trial-banner-config.ts": "trial banner target and suppressed routes",
|
|
20
|
-
"lib/payment-poll-config.ts": "payment polling behaviour",
|
|
22
|
+
"lib/payment-poll-config.ts": "payment polling behaviour, and who may pay",
|
|
21
23
|
"lib/local-payment.ts": "the local payment provider",
|
|
22
24
|
"lib/onboarding-steps.ts": "onboarding steps",
|
|
23
25
|
"lib/dashboard-config.tsx": "dashboard copy",
|
|
@@ -121,7 +123,8 @@
|
|
|
121
123
|
},
|
|
122
124
|
"payment-poll": {
|
|
123
125
|
"marker": "components/billing/local-payment-modal.tsx",
|
|
124
|
-
"items": ["pricing"]
|
|
126
|
+
"items": ["pricing"],
|
|
127
|
+
"files": ["i18n/navigation"]
|
|
125
128
|
},
|
|
126
129
|
"profile-settings": {
|
|
127
130
|
"marker": "app/[locale]/(app)/settings/profile/page.tsx",
|
|
@@ -156,7 +159,6 @@
|
|
|
156
159
|
"lib/intelligo",
|
|
157
160
|
"lib/utils"
|
|
158
161
|
],
|
|
159
|
-
"features": ["chat"],
|
|
160
162
|
"exports": {
|
|
161
163
|
"lib/intelligo": ["composeIntelligo", "executions"]
|
|
162
164
|
}
|