@eifi1/ui-kit 0.42.0 → 0.43.0
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 +12 -6
- package/dist/admin/admin-area-labels.d.ts +4 -0
- package/dist/admin/admin-area-labels.js +3 -1
- package/dist/admin/admin-area-labels.js.map +1 -1
- package/dist/admin/admin-overview.d.ts +25 -8
- package/dist/admin/admin-overview.js +26 -3
- package/dist/admin/admin-overview.js.map +1 -1
- package/dist/admin/admin-sections.d.ts +36 -5
- package/dist/admin/admin-sections.js +35 -18
- package/dist/admin/admin-sections.js.map +1 -1
- package/dist/billing/billing-banner.d.ts +8 -0
- package/dist/billing/billing-banner.js +25 -2
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/billing-labels.d.ts +4 -0
- package/dist/billing/billing-labels.js +1 -0
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/cancel-request.d.ts +53 -0
- package/dist/billing/cancel-request.js +21 -0
- package/dist/billing/cancel-request.js.map +1 -0
- package/dist/billing/provider-page.d.ts +40 -0
- package/dist/billing/provider-page.js +33 -0
- package/dist/billing/provider-page.js.map +1 -0
- package/dist/billing/subscription-actions.d.ts +32 -1
- package/dist/billing/subscription-actions.js +57 -4
- package/dist/billing/subscription-actions.js.map +1 -1
- package/dist/components/account-chips.d.ts +8 -1
- package/dist/components/account-chips.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-menu.d.ts +7 -4
- package/dist/feedback/feedback-menu.js +4 -2
- package/dist/feedback/feedback-menu.js.map +1 -1
- package/dist/hooks/use-search-param-state.js +4 -1
- package/dist/hooks/use-search-param-state.js.map +1 -1
- package/dist/i18n/defaults.js +2 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +36 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +3 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +51 -2
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +36 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +36 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +36 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +36 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +36 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +15 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/search/global-search.d.ts +3 -0
- package/dist/search/global-search.js +3 -1
- package/dist/search/global-search.js.map +1 -1
- package/dist/settings/settings-catalogue.d.ts +9 -1
- package/dist/settings/settings-catalogue.js +1 -1
- package/dist/settings/settings-catalogue.js.map +1 -1
- package/dist/shell/admin-menu.d.ts +26 -3
- package/dist/shell/admin-menu.js +8 -4
- package/dist/shell/admin-menu.js.map +1 -1
- package/dist/shell/dev-switchers.js +5 -0
- package/dist/shell/dev-switchers.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.d.ts +6 -0
- package/dist/shell/top-bar-parts-labels.js +4 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell/tour-menu.d.ts +19 -10
- package/dist/shell/tour-menu.js +20 -6
- package/dist/shell/tour-menu.js.map +1 -1
- package/dist/shell.d.ts +2 -0
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/tour/tour-catalogue-labels.d.ts +44 -0
- package/dist/tour/tour-catalogue-labels.js +32 -0
- package/dist/tour/tour-catalogue-labels.js.map +1 -0
- package/dist/tour/tour-catalogue.d.ts +68 -0
- package/dist/tour/tour-catalogue.js +189 -0
- package/dist/tour/tour-catalogue.js.map +1 -0
- package/dist/tour/tour-info.d.ts +112 -0
- package/dist/tour/tour-info.js +36 -0
- package/dist/tour/tour-info.js.map +1 -0
- package/dist/tour/use-tour-progress.d.ts +61 -0
- package/dist/tour/use-tour-progress.js +116 -0
- package/dist/tour/use-tour-progress.js.map +1 -0
- package/dist/tour/use-tour-url-sync.d.ts +58 -0
- package/dist/tour/use-tour-url-sync.js +110 -0
- package/dist/tour/use-tour-url-sync.js.map +1 -0
- package/dist/tour.d.ts +8 -0
- package/dist/tour.js +18 -0
- package/dist/tour.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/admin-area-labels.ts +7 -1
- package/src/admin/admin-overview.tsx +61 -10
- package/src/admin/admin-sections.tsx +109 -48
- package/src/billing/billing-banner.tsx +25 -2
- package/src/billing/billing-labels.ts +6 -0
- package/src/billing/cancel-request.ts +75 -0
- package/src/billing/provider-page.ts +75 -0
- package/src/billing/subscription-actions.tsx +97 -11
- package/src/components/account-chips.tsx +8 -1
- package/src/feedback/feedback-menu.tsx +8 -3
- package/src/hooks/use-search-param-state.ts +6 -1
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/german.ts +36 -1
- package/src/i18n/kit-labels.tsx +3 -0
- package/src/i18n/locales/en.ts +13 -0
- package/src/i18n/locales/es.ts +36 -1
- package/src/i18n/locales/fr.ts +36 -1
- package/src/i18n/locales/hu.ts +36 -1
- package/src/i18n/locales/it.ts +36 -1
- package/src/i18n/locales/zh.ts +36 -1
- package/src/i18n/review.ts +14 -0
- package/src/index.ts +8 -0
- package/src/search/global-search.tsx +5 -0
- package/src/settings/settings-catalogue.ts +10 -2
- package/src/shell/admin-menu.tsx +34 -5
- package/src/shell/dev-switchers.tsx +8 -0
- package/src/shell/top-bar-parts-labels.ts +9 -0
- package/src/shell/tour-menu.tsx +50 -15
- package/src/shell.ts +3 -0
- package/src/tour/tour-catalogue-labels.ts +75 -0
- package/src/tour/tour-catalogue.tsx +316 -0
- package/src/tour/tour-info.ts +144 -0
- package/src/tour/use-tour-progress.ts +198 -0
- package/src/tour/use-tour-url-sync.ts +221 -0
- package/src/tour.ts +11 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/billing/billing-banner.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { CreditCard, Hourglass, PencilOff } from \"lucide-react\";\n\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport type { AlertSize, AlertTone } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { Spinner } from \"../components/spinner\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport type { BillingPortalAccess } from \"./billing-overview\";\n\n/**\n * The billing banners (docs/billing-harmonization.md §3.3, §7, §12.6, §12.21):\n *\n * - `trial-ending` — from 7 days before `trial_ends_at` (§3.3);\n * - `grant-ending` — from 7 days before `comped_until`: a beta user's 12 months, an\n * operator's grant;\n * - `payment-failed` — from the first failed payment (`past_due`), while the provider\n * retries; since 0.42, for an admin who isn't the buyer (`portal=\"buyer_only\"`), it\n * names who can update the payment method, with no button;\n * - `plan-ended` — out of good standing: read-only, never locked (§2.3);\n * - `guest` — a guest in someone else's read-only item (§12.6);\n * - `processing` — back from checkout before the webhook landed (§12.21).\n */\nexport type BillingBannerKind =\n | \"trial-ending\"\n | \"grant-ending\"\n | \"payment-failed\"\n | \"plan-ended\"\n | \"guest\"\n | \"processing\";\n\n/** A `div`'s props, `ref` included since 0.35 (round 0.35 R4, the follow-up to API-2): they\n * go on to the {@link AlertBanner}, so the ref is the banner's `<div>` — with an × too,\n * since the banner has no `href` or `onClick` that would make its row a link or a button. */\ninterface BillingBannerBaseProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"onClick\"> {\n /**\n * Over the kind's tone: `trial-ending` and `grant-ending` are `info` until the last\n * three days and `warning` in them; `payment-failed` is `danger` (money did not\n * arrive — kastlan's past-due strip was red); `plan-ended` is `warning`, a state with\n * everything still there rather than an error; `guest` and `processing` are `info`.\n */\n tone?: AlertTone;\n /** `strip` (default): the band under the top bar, on every page. `box`: the same\n * message on the subscription page itself. */\n variant?: \"strip\" | \"box\";\n /** Default `sm` for a strip (the demo's banner), `md` for a box. */\n size?: AlertSize;\n /**\n * Where the action goes — the subscription page, say. The app's action, as an `href`\n * or as {@link onAction}; without either there is no button. Never a write-locked\n * commit: billing itself always works (§3.3).\n */\n actionHref?: string;\n /** What the action does — the app's portal call (`POST /billing/portal`, then off to\n * its `url`): for `payment-failed` with `{target: \"payment_method\"}`, the portal's\n * page for the card (§14.5); for `processing`, `useCheckoutProcessing`'s\n * `checkAgain`. The kit sends no request. */\n onAction?: () => void;\n /**\n * Over the kind's action words: \"Choose a plan\" (trial, grant, plan ended), \"Update\n * payment method\" (payment failed), \"Check again\" (processing). The guest banner has\n * none of its own: an action there needs this.\n */\n actionLabel?: string;\n /** The action is under way: the button's spinner, no second press. */\n actionPending?: boolean;\n /** An × that calls this; the app removes the banner and remembers that it did. */\n onDismiss?: () => void;\n dismissLabel?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\nexport type BillingBannerProps = BillingBannerBaseProps &\n (\n | {\n kind: \"trial-ending\" | \"grant-ending\";\n /** Calendar days left — `billingDaysLeft(trial_ends_at)`. 0 is today. */\n daysLeft: number;\n item?: never;\n portal?: never;\n buyerName?: never;\n }\n | {\n kind: \"guest\";\n /** The item's own name (\"Household 2026\"), or nothing. NEVER the owner's status:\n * the banner says \"for now\" and nothing more (§12.6). */\n item?: string;\n daysLeft?: never;\n portal?: never;\n buyerName?: never;\n }\n | {\n kind: \"payment-failed\";\n /**\n * 0.42 (§16.7 A, §16.8): `GET /billing/status`'s `portal` (`BillingStanding`).\n * `\"buyer_only\"` — the viewer isn't the buyer, so the portal's page for the card\n * would answer 403: the banner says who can update the payment method\n * (`paymentFailedAskBuyer`) and shows NO button, whatever `actionHref` or\n * `onAction` say. `\"open\"` (default): as before.\n */\n portal?: BillingPortalAccess;\n /** 0.42: the status's `buyer_name`, under `\"buyer_only\"`. Empty: \"…the admin who\n * bought the subscription\". */\n buyerName?: string | null;\n daysLeft?: never;\n item?: never;\n }\n | {\n kind: \"plan-ended\" | \"processing\";\n daysLeft?: never;\n item?: never;\n portal?: never;\n buyerName?: never;\n }\n );\n\n/** The last days, in which an ending trial or grant turns from news into a warning. */\nconst LAST_DAYS = 3;\n\nfunction defaultTone(kind: BillingBannerKind, daysLeft: number | undefined): AlertTone {\n switch (kind) {\n case \"trial-ending\":\n case \"grant-ending\":\n return (daysLeft ?? 0) > LAST_DAYS ? \"info\" : \"warning\";\n case \"payment-failed\":\n return \"danger\";\n case \"plan-ended\":\n return \"warning\";\n default:\n return \"info\";\n }\n}\n\n/**\n * One of the billing banners, as a kit `AlertBanner` strip (§7): the kind's sentence, its\n * glyph and tone, and the app's action. Carries `data-billing-banner` with the kind.\n *\n * The app decides WHICH banner shows, from `GET /billing/overview` (and hides all of them\n * while billing is off, §4); the kit only says it. A guest's banner takes no status at\n * all — the type does not let one in — so the owner's payment state cannot leak into it\n * by a wrong prop (§12.6).\n *\n * `processing` is a live region (`role=\"status\"`): it appears when the person comes back\n * from checkout and goes when the overview catches up (§12.21), and a screen reader\n * should hear both. The others are page furniture, role-less as the strip is.\n */\nexport function BillingBanner(props: BillingBannerProps) {\n const {\n kind,\n daysLeft,\n item,\n portal,\n buyerName,\n tone,\n variant = \"strip\",\n size,\n actionHref,\n onAction,\n actionLabel,\n actionPending,\n onDismiss,\n dismissLabel,\n labels: labelsProp,\n role,\n ...rest\n } = props;\n const labels = useBillingLabels(labelsProp);\n const days = Math.max(0, Math.floor(daysLeft ?? 0));\n\n let text: string;\n let icon: ReactNode;\n let ownAction: string | undefined;\n // 0.42 (§16.7 A): only the buyer can reach the portal's card page.\n const askBuyer = kind === \"payment-failed\" && portal === \"buyer_only\";\n switch (kind) {\n case \"trial-ending\":\n text = labels.trialEnding(days);\n icon = <Hourglass />;\n ownAction = labels.choosePlan;\n break;\n case \"grant-ending\":\n text = labels.grantEnding(days);\n icon = <Hourglass />;\n ownAction = labels.choosePlan;\n break;\n case \"payment-failed\":\n text = askBuyer ? labels.paymentFailedAskBuyer(buyerName || undefined) : labels.paymentFailed;\n icon = <CreditCard />;\n ownAction = labels.updatePayment;\n break;\n case \"plan-ended\":\n text = labels.planEnded;\n icon = <PencilOff />;\n ownAction = labels.choosePlan;\n break;\n case \"guest\":\n text = labels.guestReadOnly(item);\n icon = <PencilOff />;\n ownAction = undefined;\n break;\n case \"processing\":\n text = labels.processing;\n icon = <Spinner label={null} />;\n ownAction = labels.checkAgain;\n break;\n }\n\n const words = actionLabel ?? ownAction;\n const action =\n askBuyer || words === undefined || (actionHref === undefined && onAction === undefined) ? undefined : actionHref !== undefined ? (\n <Button href={actionHref} variant=\"secondary\" size=\"sm\">\n {words}\n </Button>\n ) : (\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={onAction} pending={actionPending}>\n {words}\n </Button>\n );\n\n return (\n <AlertBanner\n {...rest}\n role={role ?? (kind === \"processing\" ? \"status\" : undefined)}\n variant={variant}\n size={size ?? (variant === \"strip\" ? \"sm\" : \"md\")}\n tone={tone ?? defaultTone(kind, days)}\n icon={icon}\n action={action}\n onDismiss={onDismiss}\n dismissLabel={dismissLabel}\n data-billing-banner={kind}\n >\n {text}\n </AlertBanner>\n );\n}\n"],"mappings":";AAoLa;AAnLb,SAAS,YAAY,WAAW,iBAAiB;AAGjD,SAAS,mBAAmB;AAE5B,SAAS,cAAc;AACvB,SAAS,eAAe;AACxB,SAAS,wBAAwB;AAgHjC,MAAM,YAAY;AAElB,SAAS,YAAY,MAAyB,UAAyC;AACrF,UAAQ,MAAM;AAAA,IACZ,KAAK;AAAA,IACL,KAAK;AACH,cAAQ,YAAY,KAAK,YAAY,SAAS;AAAA,IAChD,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAeO,SAAS,cAAc,OAA2B;AACvD,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AACJ,QAAM,SAAS,iBAAiB,UAAU;AAC1C,QAAM,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,YAAY,CAAC,CAAC;AAElD,MAAI;AACJ,MAAI;AACJ,MAAI;AAEJ,QAAM,WAAW,SAAS,oBAAoB,WAAW;AACzD,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,OAAO,YAAY,IAAI;AAC9B,aAAO,oBAAC,aAAU;AAClB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO,YAAY,IAAI;AAC9B,aAAO,oBAAC,aAAU;AAClB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,WAAW,OAAO,sBAAsB,aAAa,MAAS,IAAI,OAAO;AAChF,aAAO,oBAAC,cAAW;AACnB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO;AACd,aAAO,oBAAC,aAAU;AAClB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO,cAAc,IAAI;AAChC,aAAO,oBAAC,aAAU;AAClB,kBAAY;AACZ;AAAA,IACF,KAAK;AACH,aAAO,OAAO;AACd,aAAO,oBAAC,WAAQ,OAAO,MAAM;AAC7B,kBAAY,OAAO;AACnB;AAAA,EACJ;AAEA,QAAM,QAAQ,eAAe;AAC7B,QAAM,SACJ,YAAY,UAAU,UAAc,eAAe,UAAa,aAAa,SAAa,SAAY,eAAe,SACnH,oBAAC,UAAO,MAAM,YAAY,SAAQ,aAAY,MAAK,MAChD,iBACH,IAEA,oBAAC,UAAO,MAAK,UAAS,SAAQ,aAAY,MAAK,MAAK,SAAS,UAAU,SAAS,eAC7E,iBACH;AAGJ,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAM,SAAS,SAAS,eAAe,WAAW;AAAA,MAClD;AAAA,MACA,MAAM,SAAS,YAAY,UAAU,OAAO;AAAA,MAC5C,MAAM,QAAQ,YAAY,MAAM,IAAI;AAAA,MACpC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,uBAAqB;AAAA,MAEpB;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/billing/billing-banner.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { CreditCard, Hourglass, PencilOff } from \"lucide-react\";\n\nimport { DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport type { AlertSize, AlertTone } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { Spinner } from \"../components/spinner\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport type { BillingPortalAccess } from \"./billing-overview\";\nimport { ExternalMark } from \"./subscription-actions\";\n\n/**\n * The billing banners (docs/billing-harmonization.md §3.3, §7, §12.6, §12.21):\n *\n * - `trial-ending` — from 7 days before `trial_ends_at` (§3.3);\n * - `grant-ending` — from 7 days before `comped_until`: a beta user's 12 months, an\n * operator's grant;\n * - `payment-failed` — from the first failed payment (`past_due`), while the provider\n * retries; since 0.42, for an admin who isn't the buyer (`portal=\"buyer_only\"`), it\n * names who can update the payment method, with no button;\n * - `plan-ended` — out of good standing: read-only, never locked (§2.3);\n * - `guest` — a guest in someone else's read-only item (§12.6);\n * - `processing` — back from checkout before the webhook landed (§12.21).\n */\nexport type BillingBannerKind =\n | \"trial-ending\"\n | \"grant-ending\"\n | \"payment-failed\"\n | \"plan-ended\"\n | \"guest\"\n | \"processing\";\n\n/** A `div`'s props, `ref` included since 0.35 (round 0.35 R4, the follow-up to API-2): they\n * go on to the {@link AlertBanner}, so the ref is the banner's `<div>` — with an × too,\n * since the banner has no `href` or `onClick` that would make its row a link or a button. */\ninterface BillingBannerBaseProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"onClick\"> {\n /**\n * Over the kind's tone: `trial-ending` and `grant-ending` are `info` until the last\n * three days and `warning` in them; `payment-failed` is `danger` (money did not\n * arrive — kastlan's past-due strip was red); `plan-ended` is `warning`, a state with\n * everything still there rather than an error; `guest` and `processing` are `info`.\n */\n tone?: AlertTone;\n /** `strip` (default): the band under the top bar, on every page. `box`: the same\n * message on the subscription page itself. */\n variant?: \"strip\" | \"box\";\n /** Default `sm` for a strip (the demo's banner), `md` for a box. */\n size?: AlertSize;\n /**\n * Where the action goes — the subscription page, say. The app's action, as an `href`\n * or as {@link onAction}; without either there is no button. Never a write-locked\n * commit: billing itself always works (§3.3).\n */\n actionHref?: string;\n /** What the action does — the app's portal call (`POST /billing/portal`, then off to\n * its `url`): for `payment-failed` with `{target: \"payment_method\"}`, the portal's\n * page for the card (§14.5); for `processing`, `useCheckoutProcessing`'s\n * `checkAgain`. The kit sends no request. */\n onAction?: () => void;\n /**\n * Over the kind's action words: \"Choose a plan\" (trial, grant, plan ended), \"Update\n * payment method\" (payment failed), \"Check again\" (processing). The guest banner has\n * none of its own: an action there needs this.\n */\n actionLabel?: string;\n /** The action is under way: the button's spinner, no second press. */\n actionPending?: boolean;\n /**\n * 0.43 (§17): the action opens the provider's portal in a NEW TAB (`openProviderPage`\n * in {@link onAction}): the external-link mark, \"(opens in a new tab)\" for a reader,\n * and `target=\"_blank\"` on an {@link actionHref}. Default: true for `payment-failed`\n * with `onAction` (the portal's card page), false otherwise — an `actionHref` is\n * usually the app's own subscription page.\n */\n actionNewTab?: boolean;\n /** An × that calls this; the app removes the banner and remembers that it did. */\n onDismiss?: () => void;\n dismissLabel?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\nexport type BillingBannerProps = BillingBannerBaseProps &\n (\n | {\n kind: \"trial-ending\" | \"grant-ending\";\n /** Calendar days left — `billingDaysLeft(trial_ends_at)`. 0 is today. */\n daysLeft: number;\n item?: never;\n portal?: never;\n buyerName?: never;\n }\n | {\n kind: \"guest\";\n /** The item's own name (\"Household 2026\"), or nothing. NEVER the owner's status:\n * the banner says \"for now\" and nothing more (§12.6). */\n item?: string;\n daysLeft?: never;\n portal?: never;\n buyerName?: never;\n }\n | {\n kind: \"payment-failed\";\n /**\n * 0.42 (§16.7 A, §16.8): `GET /billing/status`'s `portal` (`BillingStanding`).\n * `\"buyer_only\"` — the viewer isn't the buyer, so the portal's page for the card\n * would answer 403: the banner says who can update the payment method\n * (`paymentFailedAskBuyer`) and shows NO button, whatever `actionHref` or\n * `onAction` say. `\"open\"` (default): as before.\n */\n portal?: BillingPortalAccess;\n /** 0.42: the status's `buyer_name`, under `\"buyer_only\"`. Empty: \"…the admin who\n * bought the subscription\". */\n buyerName?: string | null;\n daysLeft?: never;\n item?: never;\n }\n | {\n kind: \"plan-ended\" | \"processing\";\n daysLeft?: never;\n item?: never;\n portal?: never;\n buyerName?: never;\n }\n );\n\n/** The last days, in which an ending trial or grant turns from news into a warning. */\nconst LAST_DAYS = 3;\n\nfunction defaultTone(kind: BillingBannerKind, daysLeft: number | undefined): AlertTone {\n switch (kind) {\n case \"trial-ending\":\n case \"grant-ending\":\n return (daysLeft ?? 0) > LAST_DAYS ? \"info\" : \"warning\";\n case \"payment-failed\":\n return \"danger\";\n case \"plan-ended\":\n return \"warning\";\n default:\n return \"info\";\n }\n}\n\n/**\n * One of the billing banners, as a kit `AlertBanner` strip (§7): the kind's sentence, its\n * glyph and tone, and the app's action. Carries `data-billing-banner` with the kind.\n *\n * The app decides WHICH banner shows, from `GET /billing/overview` (and hides all of them\n * while billing is off, §4); the kit only says it. A guest's banner takes no status at\n * all — the type does not let one in — so the owner's payment state cannot leak into it\n * by a wrong prop (§12.6).\n *\n * `processing` is a live region (`role=\"status\"`): it appears when the person comes back\n * from checkout and goes when the overview catches up (§12.21), and a screen reader\n * should hear both. The others are page furniture, role-less as the strip is.\n */\nexport function BillingBanner(props: BillingBannerProps) {\n const {\n kind,\n daysLeft,\n item,\n portal,\n buyerName,\n tone,\n variant = \"strip\",\n size,\n actionHref,\n onAction,\n actionLabel,\n actionPending,\n actionNewTab,\n onDismiss,\n dismissLabel,\n labels: labelsProp,\n role,\n ...rest\n } = props;\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const days = Math.max(0, Math.floor(daysLeft ?? 0));\n\n let text: string;\n let icon: ReactNode;\n let ownAction: string | undefined;\n // 0.42 (§16.7 A): only the buyer can reach the portal's card page.\n const askBuyer = kind === \"payment-failed\" && portal === \"buyer_only\";\n switch (kind) {\n case \"trial-ending\":\n text = labels.trialEnding(days);\n icon = <Hourglass />;\n ownAction = labels.choosePlan;\n break;\n case \"grant-ending\":\n text = labels.grantEnding(days);\n icon = <Hourglass />;\n ownAction = labels.choosePlan;\n break;\n case \"payment-failed\":\n text = askBuyer ? labels.paymentFailedAskBuyer(buyerName || undefined) : labels.paymentFailed;\n icon = <CreditCard />;\n ownAction = labels.updatePayment;\n break;\n case \"plan-ended\":\n text = labels.planEnded;\n icon = <PencilOff />;\n ownAction = labels.choosePlan;\n break;\n case \"guest\":\n text = labels.guestReadOnly(item);\n icon = <PencilOff />;\n ownAction = undefined;\n break;\n case \"processing\":\n text = labels.processing;\n icon = <Spinner label={null} />;\n ownAction = labels.checkAgain;\n break;\n }\n\n const words = actionLabel ?? ownAction;\n const newTab = actionNewTab ?? (kind === \"payment-failed\" && actionHref === undefined && onAction !== undefined);\n const action =\n askBuyer || words === undefined || (actionHref === undefined && onAction === undefined) ? undefined : actionHref !== undefined ? (\n <Button href={actionHref} variant=\"secondary\" size=\"sm\" external={newTab || undefined}>\n {words}\n {newTab && <ExternalMark />}\n </Button>\n ) : (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={onAction}\n pending={actionPending}\n className={newTab ? \"relative\" : undefined}\n data-portal-action={newTab ? \"payment_method\" : undefined}\n >\n {words}\n {newTab && <ExternalMark reader={common.opensInNewTab} />}\n </Button>\n );\n\n return (\n <AlertBanner\n {...rest}\n role={role ?? (kind === \"processing\" ? \"status\" : undefined)}\n variant={variant}\n size={size ?? (variant === \"strip\" ? \"sm\" : \"md\")}\n tone={tone ?? defaultTone(kind, days)}\n icon={icon}\n action={action}\n onDismiss={onDismiss}\n dismissLabel={dismissLabel}\n data-billing-banner={kind}\n >\n {text}\n </AlertBanner>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -154,6 +154,10 @@ export interface BillingLabels {
|
|
|
154
154
|
/** In place of "Cancel subscription" while a cancel is scheduled (§16.7 C, `POST
|
|
155
155
|
* /billing/resume`): "Keep subscription". */
|
|
156
156
|
keepSubscription: string;
|
|
157
|
+
/** 0.43 (§17.3): the muted note under the actions while the portal opens in a new tab —
|
|
158
|
+
* `provider` the provider's name ("Paddle"), `app` the app's; without the app, "this
|
|
159
|
+
* page". "Payment and invoices open at Paddle in a new tab; Kastlan stays open here." */
|
|
160
|
+
portalOpensNewTab: (provider: string, app?: string) => string;
|
|
157
161
|
/** While the checkout opens. */
|
|
158
162
|
payOpening: string;
|
|
159
163
|
/** A link without a transaction, or billing switched off: nothing to pay. */
|
|
@@ -68,6 +68,7 @@ const DEFAULT_BILLING_LABELS = {
|
|
|
68
68
|
cancelConfirmBody: (date) => `Your subscription ends on ${date}. Until then everything works as before.`,
|
|
69
69
|
cancelConfirmAction: "Cancel subscription now",
|
|
70
70
|
keepSubscription: "Keep subscription",
|
|
71
|
+
portalOpensNewTab: (provider, app) => `Payment and invoices open at ${provider} in a new tab; ${app ? `${app} stays open here` : "this page stays open"}.`,
|
|
71
72
|
payOpening: "Opening the secure checkout\u2026",
|
|
72
73
|
payNothing: "There\u2019s no payment to make here. To choose a plan, go back to the app.",
|
|
73
74
|
payFailed: "The checkout couldn\u2019t be opened. Please go back and try again in a moment.",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/billing/billing-labels.ts"],"sourcesContent":["import { useKitLabels } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\n\n/**\n * The subscription's status vocabulary (docs/billing-harmonization.md §3.2) — server-kit's\n * `SubscriptionStatus`, value for value:\n *\n * - `trialing` — the 30-day trial without a card (§2.7), or a pure guest's row whose trial\n * has not started (§12.9, `trial_ends_at` empty);\n * - `active` — paid;\n * - `past_due` — a payment failed; in good standing during the provider's retries (§3.3);\n * - `canceled` — the payer cancelled; the American spelling is the wire's, the word shown\n * is \"Cancelled\";\n * - `expired` — a trial, a grant or a failed subscription ran out: read-only (§2.3);\n * - `comped` — free by the operator's grant or as a beta user (§3.2, `comped_until`).\n */\nexport type SubscriptionStatus = \"trialing\" | \"active\" | \"past_due\" | \"canceled\" | \"expired\" | \"comped\";\n\n/** Every status, in the order a lifecycle reads: trial, paid, trouble, ended, granted. */\nexport const SUBSCRIPTION_STATUSES: readonly SubscriptionStatus[] = Object.freeze([\n \"trialing\",\n \"active\",\n \"past_due\",\n \"canceled\",\n \"expired\",\n \"comped\",\n]);\n\n/**\n * The words of the subscription parts — the `billing` namespace of `<UiKitProvider labels>`\n * (docs/billing-harmonization.md §7, §10; ui-kit 0.32.0).\n *\n * WORDING (§12.24): the English group is \"Subscription\", never \"Plan\" — keksdose's \"plan\"\n * is the month's budget plan (\"One plan per month\"), and two meanings of one word on one\n * settings page is the clash the review found. \"Plan\" stays the word for WHAT is bought\n * (Free, Pro), \"subscription\" for the standing arrangement. German says \"Tarif\" for the\n * plan and \"Abonnement\" for the subscription (§10).\n *\n * Prices are GROSS (§12.17): Swiss consumer law (PBV) wants a consumer price with the\n * MWST in it, so the cards say so (`vatIncluded`).\n */\nexport interface BillingLabels {\n /** The settings group or admin section the parts live on (§7, §12.24): \"Subscription\". */\n group: string;\n\n /* ── PlanPicker / PlanCard (§7) ── */\n /** The picker's name when it has no visible `legend`: \"Plans\". */\n plans: string;\n /** The name of the billing-period switch: \"Billing period\". */\n interval: string;\n /** The switch's two options — yearly first (§12.17). */\n yearly: string;\n monthly: string;\n /** The name of the currency switch: \"Currency\". */\n currency: string;\n /** After a price: \"per year\", \"per month\". */\n perYear: string;\n perMonth: string;\n /** Under a price (§12.17): \"VAT included\". German: \"inkl. MWST\". */\n vatIncluded: string;\n /** A plan with no price, or a price of 0: \"Free\". */\n free: string;\n /** A plan with prices, but none for this currency and period. */\n notOffered: string;\n /** A limit of `null` (§3.1): \"Unlimited\". */\n unlimited: string;\n /** The accessible names of a card's two lists. */\n limits: string;\n features: string;\n /** The chip on the payer's current plan. */\n current: string;\n /** The picker's button for `plan` (the plan's display name) with no current plan —\n * during a trial, or after a plan ended: \"Choose Pro\". */\n choose: (plan: string) => string;\n /** …for a plan after the current one in the catalogue's order: \"Upgrade to Pro\". */\n upgrade: (plan: string) => string;\n /** …for a plan before it: \"Switch to Free\". */\n downgrade: (plan: string) => string;\n /** Why the button is off while the current plan is the one picked. */\n isCurrent: string;\n /** Why it is off while nothing is picked. */\n pickFirst: string;\n\n /* ── SubscriptionStatusChip (§6) ── */\n /** One word per status. */\n status: Record<SubscriptionStatus, string>;\n\n /* ── BillingBanner (§3.3, §12.6, §12.21) ── */\n /** A trial ends in `days` calendar days — 0 is today, 1 tomorrow. */\n trialEnding: (days: number) => string;\n /** A free grant (`comped_until`: a beta user's 12 months, an operator's grant) ends in\n * `days` calendar days. */\n grantEnding: (days: number) => string;\n /** The first failed payment (`past_due`), while the provider retries. */\n paymentFailed: string;\n /** Out of good standing: read-only, never locked (§2.3). */\n planEnded: string;\n /**\n * A guest in someone else's item that is read-only for billing (§12.6). It never says\n * WHY beyond \"for now\": the owner's payment status is the owner's personal data.\n * `item` is the item's own name (\"Household 2026\"), shown in quotes — or `undefined`.\n */\n guestReadOnly: (item?: string) => string;\n /** Back from checkout before the provider's webhook landed (§12.21). */\n processing: string;\n /** The action of the trial, grant and plan-ended banners, and of `PlanLimitNotice`'s\n * `upgrade` mode: \"Choose a plan\". */\n choosePlan: string;\n /** The payment-failed banner's action, to the provider's portal: \"Update payment method\". */\n updatePayment: string;\n /**\n * 0.42 (§16.7 A, §16.8): the payment-failed banner for an admin who isn't the buyer\n * (`portal: \"buyer_only\"`), with no button — only the buyer can change the card.\n * `buyer` is their display name, or `undefined` when they are gone (\"the admin who\n * bought the subscription\").\n */\n paymentFailedAskBuyer: (buyer?: string) => string;\n /** The processing banner's action, when the app gives one: \"Check again\". */\n checkAgain: string;\n\n /* ── The read-only lock (§3.3, §12.6) ── */\n /** The `WriteLockProvider` reason for the payer (§3.3, verbatim). A guest's reason is\n * {@link guestReadOnly}. */\n lockReason: string;\n\n /* ── PlanLimitNotice (§3.4) ── */\n /** The notice's first line: \"Plan limit reached\". */\n limitReached: string;\n /** Mode `upgrade`: what the action does. */\n limitUpgrade: string;\n /** Mode `contact` (the operator grants plans by hand, keksdose's today). */\n limitContact: string;\n /** Mode `contact`'s action, a mail to support: \"Ask for more\". */\n askForMore: string;\n /** The meter's value: \"3 of 3\". Both figures are formatted already. */\n usage: (used: string, limit: string) => string;\n /** The subject of the `contact` mail; `dimension` is the app's name for it (\"Budgets\"). */\n contactSubject: (dimension: string) => string;\n /**\n * The figure line of the limit's toast (`usePlanLimitToast`, §14.11) and of an\n * operator's plan change over the new limit (§14.12): \"Budgets: 3 of 3\". A key of its\n * own rather than `${dimension}: ${usage}`, because French puts a space before the\n * colon. Both figures are formatted already.\n */\n limitUsageLine: (dimension: string, used: string, limit: string) => string;\n\n /* ── The provider's portal (§7, §12.26) ── */\n /** The portal: payment method and invoices (§2.8, the provider hosts both). */\n manage: string;\n /**\n * The visible cancel link on the app's subscription page (§12.26). German: §312k BGB\n * wants the button that leads to the cancellation labelled \"Verträge hier kündigen\" or\n * as unambiguously — a translator should use exactly that.\n */\n cancel: string;\n /**\n * In place of both while the payer hasn't reached the provider (the overview's\n * `at_provider` is false, §14.5): there is no customer to open a portal for yet. Also\n * the words for `409 billing_not_at_provider`, a stale page that asked anyway.\n */\n notAtProvider: string;\n\n /* ── The portal for the buyer only, the in-app cancel (0.42, §16) ── */\n /**\n * In place of \"Payment and invoices\" for an admin who isn't the buyer (`portal:\n * \"buyer_only\"`, §16.2 decision 3): the provider's portal opens only for the buyer.\n * `buyer` is their display name — or `undefined` when they are gone or have left the\n * company, and the words then say \"the admin who bought the subscription\".\n */\n buyerHolds: (buyer?: string) => string;\n /** `CancelSubscriptionConfirm`'s title: \"Cancel subscription\". */\n cancelConfirmTitle: string;\n /** Its body; `date` is the period's end, formatted: \"Your subscription ends on\n * 9 Nov 2026. Until then everything works as before.\" */\n cancelConfirmBody: (date: string) => string;\n /** Its confirm button (§16.4): \"Cancel subscription now\". German: \"Jetzt kündigen\",\n * the confirmation button's words in §312k BGB. */\n cancelConfirmAction: string;\n /** In place of \"Cancel subscription\" while a cancel is scheduled (§16.7 C, `POST\n * /billing/resume`): \"Keep subscription\". */\n keepSubscription: string;\n\n /* ── The pay page (§14.4) ── */\n /* Built into the static page `dist/pay/` at the kit's build: it has no React and no\n provider, so an app's override never reaches it. */\n /** While the checkout opens. */\n payOpening: string;\n /** A link without a transaction, or billing switched off: nothing to pay. */\n payNothing: string;\n /** Paddle's script didn't load, or the page's configuration is wrong. */\n payFailed: string;\n /** The buyer closed the checkout without paying. */\n payBack: string;\n /** 0.39 (§15.2.5): paid, for a transaction that names no app the page knows (a\n * payment-method update from Paddle's e-mail) — no way back to offer. */\n payDone: string;\n\n /* ── PlanSwitchConfirm (0.39, §15.2.10) ── */\n /** The dialog's title: \"Change plan\". */\n switchTitle: string;\n /** The line naming both plans, each with its billing period: \"From Pro (Monthly) to\n * Business (Yearly)\". */\n switchFromTo: (from: string, to: string) => string;\n /** What an upgrade charges at once (Paddle's preview, prorated): \"Charged now\". */\n switchChargedNow: string;\n /** What a downgrade credits — the customer's balance, which Paddle takes off the next\n * charges: \"Credited towards your next charges\". */\n switchCredited: string;\n /** The next regular charge; `date` is formatted: \"Next charge on 9 Nov 2026\". */\n switchNextCharge: (date: string) => string;\n /** The confirm button: \"Change plan\". */\n switchConfirm: string;\n\n /* ── The permanent-grant warning (0.42, §16.2 decision 5, §16.8 B) ── */\n /** The warning's title, before a checkout (`PlanPicker`) or a switch\n * (`PlanSwitchConfirm`) under a grant without an end: \"Your free access continues\". */\n grantWarningTitle: string;\n /** What the purchase does under it: nothing to the plan while the grant runs. */\n grantWarningBody: string;\n /** `PlanPicker`'s button that goes on to the checkout: \"Buy anyway\". */\n grantWarningAction: string;\n\n /* ── Offline (§12.5) ── */\n /** Changes refused as `billing_read_only` inside a sync reply, kept queued and sent\n * after payment: \"3 changes waiting for a plan\". */\n waitingChanges: (count: number) => string;\n /** The sign-out guard's way out for someone who will not pay. */\n discardWaiting: string;\n\n /* ── Refusals with no part of their own (0.32.1, kastlan's 0.32 report) ── */\n /**\n * `billing_not_configured`: billing is on but the provider's settings are missing — the\n * operator's mistake, so the words ask the payer to wait, not to act. For the toast or\n * alert after \"Choose a plan\" or \"Payment and invoices\" (`isBillingError(err,\n * \"billing_not_configured\")`). `billing_read_only` and `plan_limit` need none: the\n * write lock, the banner and `PlanLimitNotice` say them.\n */\n notConfigured: string;\n /** `billing_disabled`: billing is switched off on this server (§2.9), for a page or a\n * link that reached it anyway. */\n disabled: string;\n /* 0.33.0 — server-kit 0.7's refusals (§14.2). `billing_not_at_provider` is\n {@link notAtProvider}'s: a stale page that asked for the portal anyway. */\n /** `billing_provider_unavailable` (502): the provider didn't answer. Try again. */\n providerUnavailable: string;\n /** `billing_already_subscribed` (409): a second checkout while a subscription runs\n * (§14.6). Since 0.39 plan changes go through the app's own switch (§15.2.10), not the\n * portal, and the words say so. */\n alreadySubscribed: string;\n /** `billing_plan_not_sold` (422): the catalogue doesn't sell this plan with that\n * period or currency — at a checkout, or (0.39) a switch to a plan no longer sold. */\n planNotSold: string;\n /** 0.39 (§15.2.10): `billing_not_subscribed` (409), a plan switch with no running\n * provider subscription — the page offers the checkout instead. */\n notSubscribed: string;\n /** 0.39: `billing_plan_unchanged` (409), a switch to the price the payer pays now. */\n planUnchanged: string;\n}\n\n/** \"today\", \"tomorrow\", \"in 5 days\". */\nfunction inDays(days: number): string {\n if (days <= 0) return \"today\";\n if (days === 1) return \"tomorrow\";\n return `in ${days} days`;\n}\n\nexport const DEFAULT_BILLING_LABELS: BillingLabels = {\n group: \"Subscription\",\n\n plans: \"Plans\",\n interval: \"Billing period\",\n yearly: \"Yearly\",\n monthly: \"Monthly\",\n currency: \"Currency\",\n perYear: \"per year\",\n perMonth: \"per month\",\n vatIncluded: \"VAT included\",\n free: \"Free\",\n notOffered: \"Not offered for this billing period\",\n unlimited: \"Unlimited\",\n limits: \"Limits\",\n features: \"Included\",\n current: \"Current plan\",\n choose: (plan) => `Choose ${plan}`,\n upgrade: (plan) => `Upgrade to ${plan}`,\n downgrade: (plan) => `Switch to ${plan}`,\n isCurrent: \"This is your current plan.\",\n pickFirst: \"Pick a plan first.\",\n\n status: {\n trialing: \"Trial\",\n active: \"Active\",\n past_due: \"Payment overdue\",\n canceled: \"Cancelled\",\n expired: \"Expired\",\n comped: \"Complimentary\",\n },\n\n trialEnding: (days) =>\n `Your trial ends ${inDays(days)}. After that you can still view everything, but changes need a plan.`,\n grantEnding: (days) =>\n `Your free access ends ${inDays(days)}. After that you can still view everything, but changes need a plan.`,\n paymentFailed: \"Your last payment didn’t go through. Update your payment method to keep your plan.\",\n planEnded: \"Your plan has ended. You can still view and export everything; choose a plan to make changes again.\",\n guestReadOnly: (item) =>\n item ? `“${item}” is read-only for now; its owner can lift that.` : \"This is read-only for now; its owner can lift that.\",\n processing: \"Your payment is being processed. Your plan starts as soon as it’s confirmed; this page updates by itself.\",\n choosePlan: \"Choose a plan\",\n updatePayment: \"Update payment method\",\n paymentFailedAskBuyer: (buyer) =>\n buyer\n ? `The payment failed. ${buyer} can update the payment method.`\n : \"The payment failed. The admin who bought the subscription can update the payment method.\",\n checkAgain: \"Check again\",\n\n lockReason: \"Your plan has ended. Choose a plan to make changes again.\",\n\n limitReached: \"Plan limit reached\",\n limitUpgrade: \"To add more, choose a plan with a higher limit.\",\n limitContact: \"To add more, ask us for a higher limit.\",\n askForMore: \"Ask for more\",\n usage: (used, limit) => `${used} of ${limit}`,\n contactSubject: (dimension) => `Plan limit: ${dimension}`,\n limitUsageLine: (dimension, used, limit) => `${dimension}: ${used} of ${limit}`,\n\n manage: \"Payment and invoices\",\n cancel: \"Cancel subscription\",\n notAtProvider: \"Nothing to manage yet. Payments and invoices appear here once you have a paid plan.\",\n buyerHolds: (buyer) =>\n buyer\n ? `The payment method and the invoices are with ${buyer}, who bought the subscription.`\n : \"The payment method and the invoices are with the admin who bought the subscription.\",\n cancelConfirmTitle: \"Cancel subscription\",\n cancelConfirmBody: (date) => `Your subscription ends on ${date}. Until then everything works as before.`,\n cancelConfirmAction: \"Cancel subscription now\",\n keepSubscription: \"Keep subscription\",\n\n payOpening: \"Opening the secure checkout…\",\n payNothing: \"There’s no payment to make here. To choose a plan, go back to the app.\",\n payFailed: \"The checkout couldn’t be opened. Please go back and try again in a moment.\",\n payBack: \"The checkout was closed. You can go back, or reload this page to open it again.\",\n payDone: \"Payment complete. You can close this page.\",\n\n switchTitle: \"Change plan\",\n switchFromTo: (from, to) => `From ${from} to ${to}`,\n switchChargedNow: \"Charged now\",\n switchCredited: \"Credited towards your next charges\",\n switchNextCharge: (date) => `Next charge on ${date}`,\n switchConfirm: \"Change plan\",\n\n grantWarningTitle: \"Your free access continues\",\n grantWarningBody:\n \"You have free access with no end date. A purchase won’t change your plan while the free access runs: you would pay for a plan that doesn’t apply.\",\n grantWarningAction: \"Buy anyway\",\n\n waitingChanges: (count) =>\n count === 1 ? \"1 change waiting for a plan\" : `${count} changes waiting for a plan`,\n discardWaiting: \"Discard waiting changes\",\n\n notConfigured: \"Payments aren't set up yet. Please try again later.\",\n disabled: \"Subscriptions aren't available here.\",\n providerUnavailable: \"The payment provider can’t be reached right now. Please try again in a moment.\",\n alreadySubscribed: \"You already have a subscription. Change your plan above.\",\n planNotSold: \"This plan isn’t offered for that billing period or currency. Please choose another.\",\n notSubscribed: \"There’s no running subscription to change. Choose a plan to subscribe.\",\n planUnchanged: \"That’s already your plan. Pick another plan or billing period.\",\n};\n\n/** The `billing` namespace, resolved: English, then the provider, then `labels` — which\n * may be partial at every depth (`{ status: { comped: \"Beta\" } }`). */\nexport function useBillingLabels(labels?: LabelOverride<BillingLabels>): BillingLabels {\n return useKitLabels(\"billing\", DEFAULT_BILLING_LABELS, labels);\n}\n"],"mappings":";AAAA,SAAS,oBAAoB;AAmBtB,MAAM,wBAAuD,OAAO,OAAO;AAAA,EAChF;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AA0OD,SAAS,OAAO,MAAsB;AACpC,MAAI,QAAQ,EAAG,QAAO;AACtB,MAAI,SAAS,EAAG,QAAO;AACvB,SAAO,MAAM,IAAI;AACnB;AAEO,MAAM,yBAAwC;AAAA,EACnD,OAAO;AAAA,EAEP,OAAO;AAAA,EACP,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU;AAAA,EACV,SAAS;AAAA,EACT,UAAU;AAAA,EACV,aAAa;AAAA,EACb,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA,EACT,QAAQ,CAAC,SAAS,UAAU,IAAI;AAAA,EAChC,SAAS,CAAC,SAAS,cAAc,IAAI;AAAA,EACrC,WAAW,CAAC,SAAS,aAAa,IAAI;AAAA,EACtC,WAAW;AAAA,EACX,WAAW;AAAA,EAEX,QAAQ;AAAA,IACN,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,UAAU;AAAA,IACV,SAAS;AAAA,IACT,QAAQ;AAAA,EACV;AAAA,EAEA,aAAa,CAAC,SACZ,mBAAmB,OAAO,IAAI,CAAC;AAAA,EACjC,aAAa,CAAC,SACZ,yBAAyB,OAAO,IAAI,CAAC;AAAA,EACvC,eAAe;AAAA,EACf,WAAW;AAAA,EACX,eAAe,CAAC,SACd,OAAO,SAAI,IAAI,0DAAqD;AAAA,EACtE,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,uBAAuB,CAAC,UACtB,QACI,uBAAuB,KAAK,oCAC5B;AAAA,EACN,YAAY;AAAA,EAEZ,YAAY;AAAA,EAEZ,cAAc;AAAA,EACd,cAAc;AAAA,EACd,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,OAAO,CAAC,MAAM,UAAU,GAAG,IAAI,OAAO,KAAK;AAAA,EAC3C,gBAAgB,CAAC,cAAc,eAAe,SAAS;AAAA,EACvD,gBAAgB,CAAC,WAAW,MAAM,UAAU,GAAG,SAAS,KAAK,IAAI,OAAO,KAAK;AAAA,EAE7E,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,eAAe;AAAA,EACf,YAAY,CAAC,UACX,QACI,gDAAgD,KAAK,mCACrD;AAAA,EACN,oBAAoB;AAAA,EACpB,mBAAmB,CAAC,SAAS,6BAA6B,IAAI;AAAA,EAC9D,qBAAqB;AAAA,EACrB,kBAAkB;AAAA,EAElB,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,SAAS;AAAA,EACT,SAAS;AAAA,EAET,aAAa;AAAA,EACb,cAAc,CAAC,MAAM,OAAO,QAAQ,IAAI,OAAO,EAAE;AAAA,EACjD,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,kBAAkB,CAAC,SAAS,kBAAkB,IAAI;AAAA,EAClD,eAAe;AAAA,EAEf,mBAAmB;AAAA,EACnB,kBACE;AAAA,EACF,oBAAoB;AAAA,EAEpB,gBAAgB,CAAC,UACf,UAAU,IAAI,gCAAgC,GAAG,KAAK;AAAA,EACxD,gBAAgB;AAAA,EAEhB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,qBAAqB;AAAA,EACrB,mBAAmB;AAAA,EACnB,aAAa;AAAA,EACb,eAAe;AAAA,EACf,eAAe;AACjB;AAIO,SAAS,iBAAiB,QAAsD;AACrF,SAAO,aAAa,WAAW,wBAAwB,MAAM;AAC/D;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/billing/billing-labels.ts"],"sourcesContent":["import { useKitLabels } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\n\n/**\n * The subscription's status vocabulary (docs/billing-harmonization.md §3.2) — server-kit's\n * `SubscriptionStatus`, value for value:\n *\n * - `trialing` — the 30-day trial without a card (§2.7), or a pure guest's row whose trial\n * has not started (§12.9, `trial_ends_at` empty);\n * - `active` — paid;\n * - `past_due` — a payment failed; in good standing during the provider's retries (§3.3);\n * - `canceled` — the payer cancelled; the American spelling is the wire's, the word shown\n * is \"Cancelled\";\n * - `expired` — a trial, a grant or a failed subscription ran out: read-only (§2.3);\n * - `comped` — free by the operator's grant or as a beta user (§3.2, `comped_until`).\n */\nexport type SubscriptionStatus = \"trialing\" | \"active\" | \"past_due\" | \"canceled\" | \"expired\" | \"comped\";\n\n/** Every status, in the order a lifecycle reads: trial, paid, trouble, ended, granted. */\nexport const SUBSCRIPTION_STATUSES: readonly SubscriptionStatus[] = Object.freeze([\n \"trialing\",\n \"active\",\n \"past_due\",\n \"canceled\",\n \"expired\",\n \"comped\",\n]);\n\n/**\n * The words of the subscription parts — the `billing` namespace of `<UiKitProvider labels>`\n * (docs/billing-harmonization.md §7, §10; ui-kit 0.32.0).\n *\n * WORDING (§12.24): the English group is \"Subscription\", never \"Plan\" — keksdose's \"plan\"\n * is the month's budget plan (\"One plan per month\"), and two meanings of one word on one\n * settings page is the clash the review found. \"Plan\" stays the word for WHAT is bought\n * (Free, Pro), \"subscription\" for the standing arrangement. German says \"Tarif\" for the\n * plan and \"Abonnement\" for the subscription (§10).\n *\n * Prices are GROSS (§12.17): Swiss consumer law (PBV) wants a consumer price with the\n * MWST in it, so the cards say so (`vatIncluded`).\n */\nexport interface BillingLabels {\n /** The settings group or admin section the parts live on (§7, §12.24): \"Subscription\". */\n group: string;\n\n /* ── PlanPicker / PlanCard (§7) ── */\n /** The picker's name when it has no visible `legend`: \"Plans\". */\n plans: string;\n /** The name of the billing-period switch: \"Billing period\". */\n interval: string;\n /** The switch's two options — yearly first (§12.17). */\n yearly: string;\n monthly: string;\n /** The name of the currency switch: \"Currency\". */\n currency: string;\n /** After a price: \"per year\", \"per month\". */\n perYear: string;\n perMonth: string;\n /** Under a price (§12.17): \"VAT included\". German: \"inkl. MWST\". */\n vatIncluded: string;\n /** A plan with no price, or a price of 0: \"Free\". */\n free: string;\n /** A plan with prices, but none for this currency and period. */\n notOffered: string;\n /** A limit of `null` (§3.1): \"Unlimited\". */\n unlimited: string;\n /** The accessible names of a card's two lists. */\n limits: string;\n features: string;\n /** The chip on the payer's current plan. */\n current: string;\n /** The picker's button for `plan` (the plan's display name) with no current plan —\n * during a trial, or after a plan ended: \"Choose Pro\". */\n choose: (plan: string) => string;\n /** …for a plan after the current one in the catalogue's order: \"Upgrade to Pro\". */\n upgrade: (plan: string) => string;\n /** …for a plan before it: \"Switch to Free\". */\n downgrade: (plan: string) => string;\n /** Why the button is off while the current plan is the one picked. */\n isCurrent: string;\n /** Why it is off while nothing is picked. */\n pickFirst: string;\n\n /* ── SubscriptionStatusChip (§6) ── */\n /** One word per status. */\n status: Record<SubscriptionStatus, string>;\n\n /* ── BillingBanner (§3.3, §12.6, §12.21) ── */\n /** A trial ends in `days` calendar days — 0 is today, 1 tomorrow. */\n trialEnding: (days: number) => string;\n /** A free grant (`comped_until`: a beta user's 12 months, an operator's grant) ends in\n * `days` calendar days. */\n grantEnding: (days: number) => string;\n /** The first failed payment (`past_due`), while the provider retries. */\n paymentFailed: string;\n /** Out of good standing: read-only, never locked (§2.3). */\n planEnded: string;\n /**\n * A guest in someone else's item that is read-only for billing (§12.6). It never says\n * WHY beyond \"for now\": the owner's payment status is the owner's personal data.\n * `item` is the item's own name (\"Household 2026\"), shown in quotes — or `undefined`.\n */\n guestReadOnly: (item?: string) => string;\n /** Back from checkout before the provider's webhook landed (§12.21). */\n processing: string;\n /** The action of the trial, grant and plan-ended banners, and of `PlanLimitNotice`'s\n * `upgrade` mode: \"Choose a plan\". */\n choosePlan: string;\n /** The payment-failed banner's action, to the provider's portal: \"Update payment method\". */\n updatePayment: string;\n /**\n * 0.42 (§16.7 A, §16.8): the payment-failed banner for an admin who isn't the buyer\n * (`portal: \"buyer_only\"`), with no button — only the buyer can change the card.\n * `buyer` is their display name, or `undefined` when they are gone (\"the admin who\n * bought the subscription\").\n */\n paymentFailedAskBuyer: (buyer?: string) => string;\n /** The processing banner's action, when the app gives one: \"Check again\". */\n checkAgain: string;\n\n /* ── The read-only lock (§3.3, §12.6) ── */\n /** The `WriteLockProvider` reason for the payer (§3.3, verbatim). A guest's reason is\n * {@link guestReadOnly}. */\n lockReason: string;\n\n /* ── PlanLimitNotice (§3.4) ── */\n /** The notice's first line: \"Plan limit reached\". */\n limitReached: string;\n /** Mode `upgrade`: what the action does. */\n limitUpgrade: string;\n /** Mode `contact` (the operator grants plans by hand, keksdose's today). */\n limitContact: string;\n /** Mode `contact`'s action, a mail to support: \"Ask for more\". */\n askForMore: string;\n /** The meter's value: \"3 of 3\". Both figures are formatted already. */\n usage: (used: string, limit: string) => string;\n /** The subject of the `contact` mail; `dimension` is the app's name for it (\"Budgets\"). */\n contactSubject: (dimension: string) => string;\n /**\n * The figure line of the limit's toast (`usePlanLimitToast`, §14.11) and of an\n * operator's plan change over the new limit (§14.12): \"Budgets: 3 of 3\". A key of its\n * own rather than `${dimension}: ${usage}`, because French puts a space before the\n * colon. Both figures are formatted already.\n */\n limitUsageLine: (dimension: string, used: string, limit: string) => string;\n\n /* ── The provider's portal (§7, §12.26) ── */\n /** The portal: payment method and invoices (§2.8, the provider hosts both). */\n manage: string;\n /**\n * The visible cancel link on the app's subscription page (§12.26). German: §312k BGB\n * wants the button that leads to the cancellation labelled \"Verträge hier kündigen\" or\n * as unambiguously — a translator should use exactly that.\n */\n cancel: string;\n /**\n * In place of both while the payer hasn't reached the provider (the overview's\n * `at_provider` is false, §14.5): there is no customer to open a portal for yet. Also\n * the words for `409 billing_not_at_provider`, a stale page that asked anyway.\n */\n notAtProvider: string;\n\n /* ── The portal for the buyer only, the in-app cancel (0.42, §16) ── */\n /**\n * In place of \"Payment and invoices\" for an admin who isn't the buyer (`portal:\n * \"buyer_only\"`, §16.2 decision 3): the provider's portal opens only for the buyer.\n * `buyer` is their display name — or `undefined` when they are gone or have left the\n * company, and the words then say \"the admin who bought the subscription\".\n */\n buyerHolds: (buyer?: string) => string;\n /** `CancelSubscriptionConfirm`'s title: \"Cancel subscription\". */\n cancelConfirmTitle: string;\n /** Its body; `date` is the period's end, formatted: \"Your subscription ends on\n * 9 Nov 2026. Until then everything works as before.\" */\n cancelConfirmBody: (date: string) => string;\n /** Its confirm button (§16.4): \"Cancel subscription now\". German: \"Jetzt kündigen\",\n * the confirmation button's words in §312k BGB. */\n cancelConfirmAction: string;\n /** In place of \"Cancel subscription\" while a cancel is scheduled (§16.7 C, `POST\n * /billing/resume`): \"Keep subscription\". */\n keepSubscription: string;\n /** 0.43 (§17.3): the muted note under the actions while the portal opens in a new tab —\n * `provider` the provider's name (\"Paddle\"), `app` the app's; without the app, \"this\n * page\". \"Payment and invoices open at Paddle in a new tab; Kastlan stays open here.\" */\n portalOpensNewTab: (provider: string, app?: string) => string;\n\n /* ── The pay page (§14.4) ── */\n /* Built into the static page `dist/pay/` at the kit's build: it has no React and no\n provider, so an app's override never reaches it. */\n /** While the checkout opens. */\n payOpening: string;\n /** A link without a transaction, or billing switched off: nothing to pay. */\n payNothing: string;\n /** Paddle's script didn't load, or the page's configuration is wrong. */\n payFailed: string;\n /** The buyer closed the checkout without paying. */\n payBack: string;\n /** 0.39 (§15.2.5): paid, for a transaction that names no app the page knows (a\n * payment-method update from Paddle's e-mail) — no way back to offer. */\n payDone: string;\n\n /* ── PlanSwitchConfirm (0.39, §15.2.10) ── */\n /** The dialog's title: \"Change plan\". */\n switchTitle: string;\n /** The line naming both plans, each with its billing period: \"From Pro (Monthly) to\n * Business (Yearly)\". */\n switchFromTo: (from: string, to: string) => string;\n /** What an upgrade charges at once (Paddle's preview, prorated): \"Charged now\". */\n switchChargedNow: string;\n /** What a downgrade credits — the customer's balance, which Paddle takes off the next\n * charges: \"Credited towards your next charges\". */\n switchCredited: string;\n /** The next regular charge; `date` is formatted: \"Next charge on 9 Nov 2026\". */\n switchNextCharge: (date: string) => string;\n /** The confirm button: \"Change plan\". */\n switchConfirm: string;\n\n /* ── The permanent-grant warning (0.42, §16.2 decision 5, §16.8 B) ── */\n /** The warning's title, before a checkout (`PlanPicker`) or a switch\n * (`PlanSwitchConfirm`) under a grant without an end: \"Your free access continues\". */\n grantWarningTitle: string;\n /** What the purchase does under it: nothing to the plan while the grant runs. */\n grantWarningBody: string;\n /** `PlanPicker`'s button that goes on to the checkout: \"Buy anyway\". */\n grantWarningAction: string;\n\n /* ── Offline (§12.5) ── */\n /** Changes refused as `billing_read_only` inside a sync reply, kept queued and sent\n * after payment: \"3 changes waiting for a plan\". */\n waitingChanges: (count: number) => string;\n /** The sign-out guard's way out for someone who will not pay. */\n discardWaiting: string;\n\n /* ── Refusals with no part of their own (0.32.1, kastlan's 0.32 report) ── */\n /**\n * `billing_not_configured`: billing is on but the provider's settings are missing — the\n * operator's mistake, so the words ask the payer to wait, not to act. For the toast or\n * alert after \"Choose a plan\" or \"Payment and invoices\" (`isBillingError(err,\n * \"billing_not_configured\")`). `billing_read_only` and `plan_limit` need none: the\n * write lock, the banner and `PlanLimitNotice` say them.\n */\n notConfigured: string;\n /** `billing_disabled`: billing is switched off on this server (§2.9), for a page or a\n * link that reached it anyway. */\n disabled: string;\n /* 0.33.0 — server-kit 0.7's refusals (§14.2). `billing_not_at_provider` is\n {@link notAtProvider}'s: a stale page that asked for the portal anyway. */\n /** `billing_provider_unavailable` (502): the provider didn't answer. Try again. */\n providerUnavailable: string;\n /** `billing_already_subscribed` (409): a second checkout while a subscription runs\n * (§14.6). Since 0.39 plan changes go through the app's own switch (§15.2.10), not the\n * portal, and the words say so. */\n alreadySubscribed: string;\n /** `billing_plan_not_sold` (422): the catalogue doesn't sell this plan with that\n * period or currency — at a checkout, or (0.39) a switch to a plan no longer sold. */\n planNotSold: string;\n /** 0.39 (§15.2.10): `billing_not_subscribed` (409), a plan switch with no running\n * provider subscription — the page offers the checkout instead. */\n notSubscribed: string;\n /** 0.39: `billing_plan_unchanged` (409), a switch to the price the payer pays now. */\n planUnchanged: string;\n}\n\n/** \"today\", \"tomorrow\", \"in 5 days\". */\nfunction inDays(days: number): string {\n if (days <= 0) return \"today\";\n if (days === 1) return \"tomorrow\";\n return `in ${days} days`;\n}\n\nexport const DEFAULT_BILLING_LABELS: BillingLabels = {\n group: \"Subscription\",\n\n plans: \"Plans\",\n interval: \"Billing period\",\n yearly: \"Yearly\",\n monthly: \"Monthly\",\n currency: \"Currency\",\n perYear: \"per year\",\n perMonth: \"per month\",\n vatIncluded: \"VAT included\",\n free: \"Free\",\n notOffered: \"Not offered for this billing period\",\n unlimited: \"Unlimited\",\n limits: \"Limits\",\n features: \"Included\",\n current: \"Current plan\",\n choose: (plan) => `Choose ${plan}`,\n upgrade: (plan) => `Upgrade to ${plan}`,\n downgrade: (plan) => `Switch to ${plan}`,\n isCurrent: \"This is your current plan.\",\n pickFirst: \"Pick a plan first.\",\n\n status: {\n trialing: \"Trial\",\n active: \"Active\",\n past_due: \"Payment overdue\",\n canceled: \"Cancelled\",\n expired: \"Expired\",\n comped: \"Complimentary\",\n },\n\n trialEnding: (days) =>\n `Your trial ends ${inDays(days)}. After that you can still view everything, but changes need a plan.`,\n grantEnding: (days) =>\n `Your free access ends ${inDays(days)}. After that you can still view everything, but changes need a plan.`,\n paymentFailed: \"Your last payment didn’t go through. Update your payment method to keep your plan.\",\n planEnded: \"Your plan has ended. You can still view and export everything; choose a plan to make changes again.\",\n guestReadOnly: (item) =>\n item ? `“${item}” is read-only for now; its owner can lift that.` : \"This is read-only for now; its owner can lift that.\",\n processing: \"Your payment is being processed. Your plan starts as soon as it’s confirmed; this page updates by itself.\",\n choosePlan: \"Choose a plan\",\n updatePayment: \"Update payment method\",\n paymentFailedAskBuyer: (buyer) =>\n buyer\n ? `The payment failed. ${buyer} can update the payment method.`\n : \"The payment failed. The admin who bought the subscription can update the payment method.\",\n checkAgain: \"Check again\",\n\n lockReason: \"Your plan has ended. Choose a plan to make changes again.\",\n\n limitReached: \"Plan limit reached\",\n limitUpgrade: \"To add more, choose a plan with a higher limit.\",\n limitContact: \"To add more, ask us for a higher limit.\",\n askForMore: \"Ask for more\",\n usage: (used, limit) => `${used} of ${limit}`,\n contactSubject: (dimension) => `Plan limit: ${dimension}`,\n limitUsageLine: (dimension, used, limit) => `${dimension}: ${used} of ${limit}`,\n\n manage: \"Payment and invoices\",\n cancel: \"Cancel subscription\",\n notAtProvider: \"Nothing to manage yet. Payments and invoices appear here once you have a paid plan.\",\n buyerHolds: (buyer) =>\n buyer\n ? `The payment method and the invoices are with ${buyer}, who bought the subscription.`\n : \"The payment method and the invoices are with the admin who bought the subscription.\",\n cancelConfirmTitle: \"Cancel subscription\",\n cancelConfirmBody: (date) => `Your subscription ends on ${date}. Until then everything works as before.`,\n cancelConfirmAction: \"Cancel subscription now\",\n keepSubscription: \"Keep subscription\",\n portalOpensNewTab: (provider, app) =>\n `Payment and invoices open at ${provider} in a new tab; ${app ? `${app} stays open here` : \"this page stays open\"}.`,\n\n payOpening: \"Opening the secure checkout…\",\n payNothing: \"There’s no payment to make here. To choose a plan, go back to the app.\",\n payFailed: \"The checkout couldn’t be opened. Please go back and try again in a moment.\",\n payBack: \"The checkout was closed. You can go back, or reload this page to open it again.\",\n payDone: \"Payment complete. You can close this page.\",\n\n switchTitle: \"Change plan\",\n switchFromTo: (from, to) => `From ${from} to ${to}`,\n switchChargedNow: \"Charged now\",\n switchCredited: \"Credited towards your next charges\",\n switchNextCharge: (date) => `Next charge on ${date}`,\n switchConfirm: \"Change plan\",\n\n grantWarningTitle: \"Your free access continues\",\n grantWarningBody:\n \"You have free access with no end date. A purchase won’t change your plan while the free access runs: you would pay for a plan that doesn’t apply.\",\n grantWarningAction: \"Buy anyway\",\n\n waitingChanges: (count) =>\n count === 1 ? \"1 change waiting for a plan\" : `${count} changes waiting for a plan`,\n discardWaiting: \"Discard waiting changes\",\n\n notConfigured: \"Payments aren't set up yet. Please try again later.\",\n disabled: \"Subscriptions aren't available here.\",\n providerUnavailable: \"The payment provider can’t be reached right now. Please try again in a moment.\",\n alreadySubscribed: \"You already have a subscription. Change your plan above.\",\n planNotSold: \"This plan isn’t offered for that billing period or currency. Please choose another.\",\n notSubscribed: \"There’s no running subscription to change. Choose a plan to subscribe.\",\n planUnchanged: \"That’s already your plan. Pick another plan or billing period.\",\n};\n\n/** The `billing` namespace, resolved: English, then the provider, then `labels` — which\n * may be partial at every depth (`{ status: { comped: \"Beta\" } }`). */\nexport function useBillingLabels(labels?: LabelOverride<BillingLabels>): BillingLabels {\n return useKitLabels(\"billing\", DEFAULT_BILLING_LABELS, labels);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the payer asked for in the app (0.43, kastlan `18bf4c0`): `"cancel"` after
|
|
3
|
+
* `POST /billing/cancel` answered 204, `"resume"` after `POST /billing/resume` — while the
|
|
4
|
+
* overview still shows the old `cancel_at_period_end`, until the provider's
|
|
5
|
+
* `subscription.updated` has landed (§16.3). `null`: nothing asked, or the row has moved.
|
|
6
|
+
*/
|
|
7
|
+
export type CancelRequested = "cancel" | "resume" | null;
|
|
8
|
+
/** What the page shows for the row as read now — `CancelRequest.view(rowScheduled)`. */
|
|
9
|
+
export interface CancelRequestView {
|
|
10
|
+
/** For `SubscriptionActions`' `cancelRequested`. */
|
|
11
|
+
requested: CancelRequested;
|
|
12
|
+
/** What the plan line says — the asked-for state until the row moves, then the row's:
|
|
13
|
+
* "Cancels on …" at once after a cancel, "Renews on …" after "Keep subscription". */
|
|
14
|
+
cancelScheduled: boolean;
|
|
15
|
+
/** Call on the 204 of a cancel or of a resume. A cancel and its undo both asked before
|
|
16
|
+
* the row moved cancel out. */
|
|
17
|
+
noteAsked: () => void;
|
|
18
|
+
}
|
|
19
|
+
export interface CancelRequest {
|
|
20
|
+
/** Read the overview again every few seconds while this is true — the overview query's
|
|
21
|
+
* `waiting` (kastlan's `useBillingOverview({ waiting })`, a `refetchInterval` inside) —
|
|
22
|
+
* so the page follows the provider. Needs no overview data: pass it to the query. */
|
|
23
|
+
waiting: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The row as read now (the overview's `cancel_at_period_end`; `undefined` while it
|
|
26
|
+
* loads) → what the page shows. Call it during render, after the query: when the row
|
|
27
|
+
* has moved past what was asked, it drops the request (React's "adjust state while
|
|
28
|
+
* rendering" pattern), so `waiting` turns false and the query stops polling.
|
|
29
|
+
*/
|
|
30
|
+
view: (rowScheduled: boolean | null | undefined) => CancelRequestView;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The in-app cancel's follow-up (0.43, kastlan's derivation in `18bf4c0`): after "Cancel
|
|
34
|
+
* subscription now" or "Keep subscription" the server answers 204 at once, but the row's
|
|
35
|
+
* `cancel_at_period_end` only changes when the provider's webhook lands. Until then the
|
|
36
|
+
* page shows what was asked for, and a second press would ask the provider again.
|
|
37
|
+
*
|
|
38
|
+
* The state lives in the hook and needs no overview at construction, so it goes BEFORE
|
|
39
|
+
* the overview query, which reads `waiting`, and the view comes after it — the overview
|
|
40
|
+
* read once (build review 0.43, kastlan B2; kastlan `subscription-section.tsx:74-92`):
|
|
41
|
+
*
|
|
42
|
+
* const request = useCancelRequest();
|
|
43
|
+
* const { data } = useBillingOverview({ waiting: request.waiting });
|
|
44
|
+
* const view = request.view(data?.cancel_at_period_end);
|
|
45
|
+
* <p>{standingLine({ ...data, cancel_at_period_end: view.cancelScheduled })}</p>
|
|
46
|
+
* <SubscriptionActions
|
|
47
|
+
* cancelAtPeriodEnd={data.cancel_at_period_end}
|
|
48
|
+
* cancelRequested={view.requested}
|
|
49
|
+
* onResume={() => resume.mutate(undefined, { onSuccess: view.noteAsked })}
|
|
50
|
+
* …
|
|
51
|
+
* />
|
|
52
|
+
*/
|
|
53
|
+
export declare function useCancelRequest(): CancelRequest;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
function useCancelRequest() {
|
|
4
|
+
const [asked, setAsked] = useState(null);
|
|
5
|
+
const view = (row) => {
|
|
6
|
+
if (row === void 0 || row === null) return { requested: null, cancelScheduled: false, noteAsked: () => {
|
|
7
|
+
} };
|
|
8
|
+
if (asked !== null && asked.scheduled !== row) setAsked(null);
|
|
9
|
+
const open = asked !== null && asked.scheduled === row;
|
|
10
|
+
return {
|
|
11
|
+
requested: open ? row ? "resume" : "cancel" : null,
|
|
12
|
+
cancelScheduled: open ? !row : row,
|
|
13
|
+
noteAsked: () => setAsked((a) => a !== null && a.scheduled === row ? null : { scheduled: row })
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
return { waiting: asked !== null, view };
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
useCancelRequest
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=cancel-request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/billing/cancel-request.ts"],"sourcesContent":["import { useState } from \"react\";\n\n/**\n * What the payer asked for in the app (0.43, kastlan `18bf4c0`): `\"cancel\"` after\n * `POST /billing/cancel` answered 204, `\"resume\"` after `POST /billing/resume` — while the\n * overview still shows the old `cancel_at_period_end`, until the provider's\n * `subscription.updated` has landed (§16.3). `null`: nothing asked, or the row has moved.\n */\nexport type CancelRequested = \"cancel\" | \"resume\" | null;\n\n/** What the page shows for the row as read now — `CancelRequest.view(rowScheduled)`. */\nexport interface CancelRequestView {\n /** For `SubscriptionActions`' `cancelRequested`. */\n requested: CancelRequested;\n /** What the plan line says — the asked-for state until the row moves, then the row's:\n * \"Cancels on …\" at once after a cancel, \"Renews on …\" after \"Keep subscription\". */\n cancelScheduled: boolean;\n /** Call on the 204 of a cancel or of a resume. A cancel and its undo both asked before\n * the row moved cancel out. */\n noteAsked: () => void;\n}\n\nexport interface CancelRequest {\n /** Read the overview again every few seconds while this is true — the overview query's\n * `waiting` (kastlan's `useBillingOverview({ waiting })`, a `refetchInterval` inside) —\n * so the page follows the provider. Needs no overview data: pass it to the query. */\n waiting: boolean;\n /**\n * The row as read now (the overview's `cancel_at_period_end`; `undefined` while it\n * loads) → what the page shows. Call it during render, after the query: when the row\n * has moved past what was asked, it drops the request (React's \"adjust state while\n * rendering\" pattern), so `waiting` turns false and the query stops polling.\n */\n view: (rowScheduled: boolean | null | undefined) => CancelRequestView;\n}\n\n/**\n * The in-app cancel's follow-up (0.43, kastlan's derivation in `18bf4c0`): after \"Cancel\n * subscription now\" or \"Keep subscription\" the server answers 204 at once, but the row's\n * `cancel_at_period_end` only changes when the provider's webhook lands. Until then the\n * page shows what was asked for, and a second press would ask the provider again.\n *\n * The state lives in the hook and needs no overview at construction, so it goes BEFORE\n * the overview query, which reads `waiting`, and the view comes after it — the overview\n * read once (build review 0.43, kastlan B2; kastlan `subscription-section.tsx:74-92`):\n *\n * const request = useCancelRequest();\n * const { data } = useBillingOverview({ waiting: request.waiting });\n * const view = request.view(data?.cancel_at_period_end);\n * <p>{standingLine({ ...data, cancel_at_period_end: view.cancelScheduled })}</p>\n * <SubscriptionActions\n * cancelAtPeriodEnd={data.cancel_at_period_end}\n * cancelRequested={view.requested}\n * onResume={() => resume.mutate(undefined, { onSuccess: view.noteAsked })}\n * …\n * />\n */\nexport function useCancelRequest(): CancelRequest {\n // What was asked, kept with the row's state it was asked against.\n const [asked, setAsked] = useState<{ scheduled: boolean } | null>(null);\n const view = (row: boolean | null | undefined): CancelRequestView => {\n // Not read yet: nothing to show against, and nothing to drop.\n if (row === undefined || row === null) return { requested: null, cancelScheduled: false, noteAsked: () => {} };\n // The row has moved: it says the rest. Adjusted during render, React's pattern for\n // state that follows a prop.\n if (asked !== null && asked.scheduled !== row) setAsked(null);\n const open = asked !== null && asked.scheduled === row;\n return {\n requested: open ? (row ? \"resume\" : \"cancel\") : null,\n cancelScheduled: open ? !row : row,\n noteAsked: () => setAsked((a) => (a !== null && a.scheduled === row ? null : { scheduled: row })),\n };\n };\n return { waiting: asked !== null, view };\n}\n"],"mappings":";AAAA,SAAS,gBAAgB;AAyDlB,SAAS,mBAAkC;AAEhD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwC,IAAI;AACtE,QAAM,OAAO,CAAC,QAAuD;AAEnE,QAAI,QAAQ,UAAa,QAAQ,KAAM,QAAO,EAAE,WAAW,MAAM,iBAAiB,OAAO,WAAW,MAAM;AAAA,IAAC,EAAE;AAG7G,QAAI,UAAU,QAAQ,MAAM,cAAc,IAAK,UAAS,IAAI;AAC5D,UAAM,OAAO,UAAU,QAAQ,MAAM,cAAc;AACnD,WAAO;AAAA,MACL,WAAW,OAAQ,MAAM,WAAW,WAAY;AAAA,MAChD,iBAAiB,OAAO,CAAC,MAAM;AAAA,MAC/B,WAAW,MAAM,SAAS,CAAC,MAAO,MAAM,QAAQ,EAAE,cAAc,MAAM,OAAO,EAAE,WAAW,IAAI,CAAE;AAAA,IAClG;AAAA,EACF;AACA,SAAO,EAAE,SAAS,UAAU,MAAM,KAAK;AACzC;","names":[]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The provider's hosted portal in a NEW TAB (0.43, docs/billing-harmonization.md §17):
|
|
3
|
+
* "Payment and invoices", the portal's "Cancel subscription" and the payment-failed
|
|
4
|
+
* banner's "Update payment method" open at the provider while the app stays open where
|
|
5
|
+
* it was. kastlan's `usePortal` (feat/kit-0.42, `72c3177`) is the reference.
|
|
6
|
+
*
|
|
7
|
+
* The checkout is NOT one of them (§17.4): it stays in the same tab, since the page waits
|
|
8
|
+
* for the payer's return (`useCheckoutProcessing`).
|
|
9
|
+
*/
|
|
10
|
+
/** How the page went: in a tab of its own, or — the browser refused one — in this one. */
|
|
11
|
+
export type ProviderPageWay = "new-tab" | "same-tab";
|
|
12
|
+
export interface OpenProviderPageOptions {
|
|
13
|
+
/** The window to open from and to fall back to. Default the global `window` (a test's
|
|
14
|
+
* or an iframe's own). */
|
|
15
|
+
window?: Window;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Opens the provider's page in a new tab, from INSIDE the click: call it in the click
|
|
19
|
+
* handler itself, before any `await` — a `window.open` after one is blocked as a pop-up.
|
|
20
|
+
*
|
|
21
|
+
* 1. opens `about:blank` synchronously and sets its `opener` to `null`, so the provider's
|
|
22
|
+
* page cannot reach back into the app;
|
|
23
|
+
* 2. awaits `fetchUrl` — the app's `POST /billing/portal` with its target, answering the
|
|
24
|
+
* `url` (the kit sends no request);
|
|
25
|
+
* 3. points the tab at the URL;
|
|
26
|
+
* 4. on a refusal closes the tab again and rethrows, so the app shows its error;
|
|
27
|
+
* 5. where the browser refused the tab (a pop-up blocker), or it was closed while the
|
|
28
|
+
* URL was on its way, the app's own page goes there instead.
|
|
29
|
+
*
|
|
30
|
+
* <SubscriptionActions
|
|
31
|
+
* onPortal={(target) => {
|
|
32
|
+
* openProviderPage(() => api.billingPortal({ target }).then((r) => r.url)).catch(showError);
|
|
33
|
+
* }}
|
|
34
|
+
* providerName="Paddle"
|
|
35
|
+
* appName="Kastlan"
|
|
36
|
+
* />
|
|
37
|
+
*
|
|
38
|
+
* Resolves with the way it went.
|
|
39
|
+
*/
|
|
40
|
+
export declare function openProviderPage(fetchUrl: () => Promise<string>, options?: OpenProviderPageOptions): Promise<ProviderPageWay>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
async function openProviderPage(fetchUrl, options = {}) {
|
|
3
|
+
const win = options.window ?? window;
|
|
4
|
+
let tab;
|
|
5
|
+
try {
|
|
6
|
+
tab = win.open("about:blank", "_blank");
|
|
7
|
+
} catch {
|
|
8
|
+
tab = null;
|
|
9
|
+
}
|
|
10
|
+
if (tab) {
|
|
11
|
+
try {
|
|
12
|
+
tab.opener = null;
|
|
13
|
+
} catch {
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
let url;
|
|
17
|
+
try {
|
|
18
|
+
url = await fetchUrl();
|
|
19
|
+
} catch (error) {
|
|
20
|
+
tab?.close();
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
if (tab && !tab.closed) {
|
|
24
|
+
tab.location.href = url;
|
|
25
|
+
return "new-tab";
|
|
26
|
+
}
|
|
27
|
+
win.location.assign(url);
|
|
28
|
+
return "same-tab";
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
openProviderPage
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=provider-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/billing/provider-page.ts"],"sourcesContent":["/**\n * The provider's hosted portal in a NEW TAB (0.43, docs/billing-harmonization.md §17):\n * \"Payment and invoices\", the portal's \"Cancel subscription\" and the payment-failed\n * banner's \"Update payment method\" open at the provider while the app stays open where\n * it was. kastlan's `usePortal` (feat/kit-0.42, `72c3177`) is the reference.\n *\n * The checkout is NOT one of them (§17.4): it stays in the same tab, since the page waits\n * for the payer's return (`useCheckoutProcessing`).\n */\n\n/** How the page went: in a tab of its own, or — the browser refused one — in this one. */\nexport type ProviderPageWay = \"new-tab\" | \"same-tab\";\n\nexport interface OpenProviderPageOptions {\n /** The window to open from and to fall back to. Default the global `window` (a test's\n * or an iframe's own). */\n window?: Window;\n}\n\n/**\n * Opens the provider's page in a new tab, from INSIDE the click: call it in the click\n * handler itself, before any `await` — a `window.open` after one is blocked as a pop-up.\n *\n * 1. opens `about:blank` synchronously and sets its `opener` to `null`, so the provider's\n * page cannot reach back into the app;\n * 2. awaits `fetchUrl` — the app's `POST /billing/portal` with its target, answering the\n * `url` (the kit sends no request);\n * 3. points the tab at the URL;\n * 4. on a refusal closes the tab again and rethrows, so the app shows its error;\n * 5. where the browser refused the tab (a pop-up blocker), or it was closed while the\n * URL was on its way, the app's own page goes there instead.\n *\n * <SubscriptionActions\n * onPortal={(target) => {\n * openProviderPage(() => api.billingPortal({ target }).then((r) => r.url)).catch(showError);\n * }}\n * providerName=\"Paddle\"\n * appName=\"Kastlan\"\n * />\n *\n * Resolves with the way it went.\n */\nexport async function openProviderPage(\n fetchUrl: () => Promise<string>,\n options: OpenProviderPageOptions = {},\n): Promise<ProviderPageWay> {\n const win = options.window ?? window;\n let tab: Window | null;\n try {\n tab = win.open(\"about:blank\", \"_blank\");\n } catch {\n tab = null;\n }\n if (tab) {\n try {\n // The provider's page must not reach back into the app.\n tab.opener = null;\n } catch {\n /* a browser that guards `opener`: the tab is still ours to point */\n }\n }\n let url: string;\n try {\n url = await fetchUrl();\n } catch (error) {\n tab?.close();\n throw error;\n }\n if (tab && !tab.closed) {\n tab.location.href = url;\n return \"new-tab\";\n }\n win.location.assign(url);\n return \"same-tab\";\n}\n"],"mappings":";AA0CA,eAAsB,iBACpB,UACA,UAAmC,CAAC,GACV;AAC1B,QAAM,MAAM,QAAQ,UAAU;AAC9B,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,KAAK,eAAe,QAAQ;AAAA,EACxC,QAAQ;AACN,UAAM;AAAA,EACR;AACA,MAAI,KAAK;AACP,QAAI;AAEF,UAAI,SAAS;AAAA,IACf,QAAQ;AAAA,IAER;AAAA,EACF;AACA,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,SAAS;AAAA,EACvB,SAAS,OAAO;AACd,SAAK,MAAM;AACX,UAAM;AAAA,EACR;AACA,MAAI,OAAO,CAAC,IAAI,QAAQ;AACtB,QAAI,SAAS,OAAO;AACpB,WAAO;AAAA,EACT;AACA,MAAI,SAAS,OAAO,GAAG;AACvB,SAAO;AACT;","names":[]}
|
|
@@ -2,6 +2,12 @@ import type { ComponentPropsWithRef } from "react";
|
|
|
2
2
|
import type { LabelOverride } from "../i18n/kit-labels.js";
|
|
3
3
|
import type { BillingLabels } from "./billing-labels.js";
|
|
4
4
|
import type { BillingPortalAccess, PortalTarget } from "./billing-overview.js";
|
|
5
|
+
import type { CancelRequested } from "./cancel-request.js";
|
|
6
|
+
/** The portal action's mark (§17.3): the external-link glyph after the words, and "(opens
|
|
7
|
+
* in a new tab)" for a reader — on a link, `Button`'s `external` says the latter. */
|
|
8
|
+
export declare function ExternalMark({ reader }: {
|
|
9
|
+
reader?: string;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
5
11
|
export interface SubscriptionActionsProps extends Omit<ComponentPropsWithRef<"div">, "children"> {
|
|
6
12
|
/** "Payment and invoices", as a link — or {@link onManage}. Without either, none. */
|
|
7
13
|
manageHref?: string;
|
|
@@ -57,6 +63,25 @@ export interface SubscriptionActionsProps extends Omit<ComponentPropsWithRef<"di
|
|
|
57
63
|
* the cancel's place while {@link cancelAtPeriodEnd} is true. Without it, the cancel
|
|
58
64
|
* stays as before. */
|
|
59
65
|
onResume?: () => void;
|
|
66
|
+
/**
|
|
67
|
+
* 0.43 (kastlan `18bf4c0`): what was asked in the app and the overview does not show
|
|
68
|
+
* yet — `useCancelRequest().view(row).requested`. `"cancel"`: shown as scheduled ("Keep
|
|
69
|
+
* subscription" with {@link onResume}; without it, no second "Cancel subscription").
|
|
70
|
+
* `"resume"`: shown as not scheduled. `null` (default): {@link cancelAtPeriodEnd} says.
|
|
71
|
+
*/
|
|
72
|
+
cancelRequested?: CancelRequested;
|
|
73
|
+
/**
|
|
74
|
+
* 0.43 (§17): the portal opens in a NEW TAB — the app's handler calls
|
|
75
|
+
* `openProviderPage` — so its actions carry the external-link mark, a link opens with
|
|
76
|
+
* `target="_blank"`, and the `portalOpensNewTab` note shows under them. Default true;
|
|
77
|
+
* `false` for an app whose portal still opens in the same tab.
|
|
78
|
+
*/
|
|
79
|
+
portalNewTab?: boolean;
|
|
80
|
+
/** 0.43: the provider's name in the note. Default `"Paddle"` (§2 decision 16). */
|
|
81
|
+
providerName?: string;
|
|
82
|
+
/** 0.43: the app's name in the note ("Kastlan stays open here"). Without it, "this page
|
|
83
|
+
* stays open". */
|
|
84
|
+
appName?: string;
|
|
60
85
|
/** Which one is under way: its spinner, no second press. `"resume"` since 0.42. */
|
|
61
86
|
pending?: "manage" | "cancel" | "resume" | null;
|
|
62
87
|
/** Prop > `<UiKitProvider labels={{ billing }}>` > English. */
|
|
@@ -83,5 +108,11 @@ export interface SubscriptionActionsProps extends Omit<ComponentPropsWithRef<"di
|
|
|
83
108
|
* "Cancel subscription" is the in-app cancel (`onCancelInApp`). While a cancel is
|
|
84
109
|
* scheduled (`cancelAtPeriodEnd`) and `onResume` is given, "Keep subscription" stands in
|
|
85
110
|
* the cancel's place, whoever the viewer is.
|
|
111
|
+
*
|
|
112
|
+
* 0.43 (§17): the portal opens in a new tab (`openProviderPage` in the app's handler):
|
|
113
|
+
* its actions carry the external-link mark, and a muted note says so — "Payment and
|
|
114
|
+
* invoices open at {provider} in a new tab; {app} stays open here." The in-app cancel and
|
|
115
|
+
* "Keep subscription" stay unmarked. After an in-app cancel or resume, `cancelRequested`
|
|
116
|
+
* (`useCancelRequest`) shows what was asked until the provider's webhook lands.
|
|
86
117
|
*/
|
|
87
|
-
export declare function SubscriptionActions({ manageHref, onManage, cancelHref, onCancel, onPortal, atProvider, portal, buyerName, onCancelInApp, cancelAtPeriodEnd, onResume, pending, labels: labelsProp, className, ...rest }: SubscriptionActionsProps): import("react").JSX.Element | null;
|
|
118
|
+
export declare function SubscriptionActions({ manageHref, onManage, cancelHref, onCancel, onPortal, atProvider, portal, buyerName, onCancelInApp, cancelAtPeriodEnd, onResume, cancelRequested, portalNewTab, providerName, appName, pending, labels: labelsProp, className, ...rest }: SubscriptionActionsProps): import("react").JSX.Element | null;
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ExternalLink } from "lucide-react";
|
|
4
|
+
import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels.js";
|
|
3
5
|
import { cn } from "../lib/cn.js";
|
|
4
6
|
import { Button } from "../components/button.js";
|
|
5
7
|
import { useBillingLabels } from "./billing-labels.js";
|
|
8
|
+
function ExternalMark({ reader }) {
|
|
9
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
10
|
+
/* @__PURE__ */ jsx(ExternalLink, { "aria-hidden": true, "data-external": "", className: "size-4 shrink-0 rtl:-scale-x-100" }),
|
|
11
|
+
reader !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
12
|
+
" ",
|
|
13
|
+
/* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
|
|
14
|
+
"(",
|
|
15
|
+
reader,
|
|
16
|
+
")"
|
|
17
|
+
] })
|
|
18
|
+
] })
|
|
19
|
+
] });
|
|
20
|
+
}
|
|
6
21
|
function SubscriptionActions({
|
|
7
22
|
manageHref,
|
|
8
23
|
onManage,
|
|
@@ -15,12 +30,17 @@ function SubscriptionActions({
|
|
|
15
30
|
onCancelInApp,
|
|
16
31
|
cancelAtPeriodEnd = false,
|
|
17
32
|
onResume,
|
|
33
|
+
cancelRequested = null,
|
|
34
|
+
portalNewTab = true,
|
|
35
|
+
providerName = "Paddle",
|
|
36
|
+
appName,
|
|
18
37
|
pending,
|
|
19
38
|
labels: labelsProp,
|
|
20
39
|
className,
|
|
21
40
|
...rest
|
|
22
41
|
}) {
|
|
23
42
|
const labels = useBillingLabels(labelsProp);
|
|
43
|
+
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
24
44
|
if (!atProvider) {
|
|
25
45
|
return /* @__PURE__ */ jsx("div", { ...rest, "data-at-provider": "false", className: cn("text-sm text-muted", className), children: /* @__PURE__ */ jsx("p", { children: labels.notAtProvider }) });
|
|
26
46
|
}
|
|
@@ -28,15 +48,48 @@ function SubscriptionActions({
|
|
|
28
48
|
const manageCall = onManage ?? (onPortal ? () => onPortal("overview") : void 0);
|
|
29
49
|
const portalCancel = onCancel ?? (onPortal ? () => onPortal("cancel") : void 0);
|
|
30
50
|
const cancelCall = buyerOnly ? onCancelInApp : portalCancel ?? (cancelHref === void 0 ? onCancelInApp : void 0);
|
|
31
|
-
const
|
|
32
|
-
const
|
|
51
|
+
const scheduled = cancelRequested === "cancel" ? true : cancelRequested === "resume" ? false : cancelAtPeriodEnd;
|
|
52
|
+
const newTab = portalNewTab && !buyerOnly;
|
|
53
|
+
const words = (text, external, link) => external ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
54
|
+
text,
|
|
55
|
+
/* @__PURE__ */ jsx(ExternalMark, { reader: link ? void 0 : common.opensInNewTab })
|
|
56
|
+
] }) : text;
|
|
57
|
+
const portalCancelShown = !buyerOnly && (cancelHref !== void 0 || portalCancel !== void 0);
|
|
58
|
+
const manage = buyerOnly ? /* @__PURE__ */ jsx("p", { className: "basis-full text-sm text-muted", "data-buyer-holds": "", children: labels.buyerHolds(buyerName || void 0) }) : manageHref !== void 0 ? /* @__PURE__ */ jsx(Button, { href: manageHref, variant: "secondary", external: newTab || void 0, "data-portal-action": "manage", children: words(labels.manage, newTab, true) }) : manageCall !== void 0 ? /* @__PURE__ */ jsx(
|
|
59
|
+
Button,
|
|
60
|
+
{
|
|
61
|
+
type: "button",
|
|
62
|
+
variant: "secondary",
|
|
63
|
+
onClick: manageCall,
|
|
64
|
+
pending: pending === "manage",
|
|
65
|
+
className: newTab ? "relative" : void 0,
|
|
66
|
+
"data-portal-action": "manage",
|
|
67
|
+
children: words(labels.manage, newTab, false)
|
|
68
|
+
}
|
|
69
|
+
) : null;
|
|
70
|
+
const cancel = scheduled && onResume !== void 0 ? /* @__PURE__ */ jsx(Button, { type: "button", variant: "link", onClick: onResume, pending: pending === "resume", "data-keep-subscription": "", children: labels.keepSubscription }) : cancelRequested === "cancel" ? null : cancelHref !== void 0 && !buyerOnly ? /* @__PURE__ */ jsx(Button, { href: cancelHref, variant: "link", tone: "danger", external: newTab || void 0, "data-portal-action": "cancel", children: words(labels.cancel, newTab, true) }) : cancelCall !== void 0 ? /* @__PURE__ */ jsx(
|
|
71
|
+
Button,
|
|
72
|
+
{
|
|
73
|
+
type: "button",
|
|
74
|
+
variant: "link",
|
|
75
|
+
tone: "danger",
|
|
76
|
+
onClick: cancelCall,
|
|
77
|
+
pending: pending === "cancel",
|
|
78
|
+
className: newTab && portalCancelShown ? "relative" : void 0,
|
|
79
|
+
"data-portal-action": portalCancelShown ? "cancel" : void 0,
|
|
80
|
+
children: words(labels.cancel, newTab && portalCancelShown, false)
|
|
81
|
+
}
|
|
82
|
+
) : null;
|
|
33
83
|
if (!manage && !cancel) return null;
|
|
84
|
+
const portalShown = !buyerOnly && (manage !== null || cancel !== null && portalCancelShown && !(scheduled && onResume));
|
|
34
85
|
return /* @__PURE__ */ jsxs("div", { ...rest, className: cn("flex flex-wrap items-center gap-x-4 gap-y-2", className), children: [
|
|
35
86
|
manage,
|
|
36
|
-
cancel
|
|
87
|
+
cancel,
|
|
88
|
+
newTab && portalShown && /* @__PURE__ */ jsx("p", { className: "basis-full text-sm text-muted", "data-portal-note": "", children: labels.portalOpensNewTab(providerName, appName || void 0) })
|
|
37
89
|
] });
|
|
38
90
|
}
|
|
39
91
|
export {
|
|
92
|
+
ExternalMark,
|
|
40
93
|
SubscriptionActions
|
|
41
94
|
};
|
|
42
95
|
//# sourceMappingURL=subscription-actions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/billing/subscription-actions.tsx"],"sourcesContent":["import type { ComponentPropsWithRef } from \"react\";\n\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { Button } from \"../components/button\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport type { BillingPortalAccess, PortalTarget } from \"./billing-overview\";\n\nexport interface SubscriptionActionsProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n /** \"Payment and invoices\", as a link — or {@link onManage}. Without either, none. */\n manageHref?: string;\n /** \"Payment and invoices\", as the app's call: `POST /billing/portal` with\n * `{target: \"overview\"}`, then off to its `url` (§4, §14.5). The kit sends no\n * request. */\n onManage?: () => void;\n /** \"Cancel subscription\", as a link — or {@link onCancel}. Without either, none. */\n cancelHref?: string;\n /** \"Cancel subscription\", as the app's call: the portal again, with `{target:\n * \"cancel\"}` — its cancel link, where the provider's cancel and withdrawal functions\n * are (§12.26, §14.5). Not the overview. */\n onCancel?: () => void;\n /**\n * Both, as one call that says where to ({@link PortalTarget}, the body of\n * `POST /billing/portal`): `\"overview\"` for \"Payment and invoices\",\n * `\"cancel\"` for \"Cancel subscription\" — so the target can't be mixed up (kastlan's\n * cancel opened the overview). {@link onManage} and {@link onCancel} win over it.\n */\n onPortal?: (target: PortalTarget) => void;\n /**\n * The overview's `at_provider` (§14.5): whether the payer has reached the provider.\n * `false`: no customer there yet, so nothing to open — a caption (`notAtProvider`)\n * stands in place of both actions. `true`: both show, WHATEVER the status or source —\n * a payer who bought under a grant keeps the way to the invoices and to \"Cancel\"\n * (§12.26). Default true (0.32's behaviour).\n */\n atProvider?: boolean;\n /**\n * 0.42 (§16.2, server-kit 0.14): the overview's `portal`. `\"buyer_only\"` — the row has\n * a buyer and the viewer isn't them, so the portal would answer `403\n * billing_portal_buyer_only`: \"Payment and invoices\" becomes a caption naming the buyer\n * (`buyerHolds`, no button), and \"Cancel subscription\" calls {@link onCancelInApp}\n * (no portal at all — `cancelHref`, `onCancel` and `onPortal` are not used). `\"open\"`\n * (default): as before.\n */\n portal?: BillingPortalAccess;\n /** 0.42: the overview's `buyer_name`, for the caption under `\"buyer_only\"`. Empty: \"…with\n * the admin who bought the subscription\". */\n buyerName?: string | null;\n /**\n * 0.42 (§16.2 decision 2): \"Cancel subscription\" in the app — open\n * `CancelSubscriptionConfirm`, whose Confirm is `POST /billing/cancel`. The cancel under\n * `\"buyer_only\"`. Under `\"open\"` it is the last resort, used only when no portal cancel\n * (`cancelHref`, `onCancel`, `onPortal`) is given — a person-payer app that mounts the\n * route and not the portal's cancel.\n */\n onCancelInApp?: () => void;\n /** 0.42 (§16.7 C): the overview's `cancel_at_period_end` — a cancel is scheduled. With\n * {@link onResume}, \"Keep subscription\" takes the cancel's place. */\n cancelAtPeriodEnd?: boolean;\n /** 0.42 (§16.7 C): \"Keep subscription\", the app's `POST /billing/resume` — shown in\n * the cancel's place while {@link cancelAtPeriodEnd} is true. Without it, the cancel\n * stays as before. */\n onResume?: () => void;\n /** Which one is under way: its spinner, no second press. `\"resume\"` since 0.42. */\n pending?: \"manage\" | \"cancel\" | \"resume\" | null;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/**\n * The two ways to the provider's hosted portal on the subscription page\n * (docs/billing-harmonization.md §2.8, §7, §12.26): \"Payment and invoices\" — the payment\n * method and the invoices live there, never in the app — and a VISIBLE \"Cancel\n * subscription\". Consumer law wants the second in plain sight: Germany's cancellation\n * button (§312k BGB) and the EU withdrawal function (Directive (EU) 2023/2673, from\n * 19 June 2026) are in the portal, and the app's page must not hide the way there behind\n * \"manage\".\n *\n * Neither takes the write lock: a read-only payer must be able to pay, leave or cancel\n * (§3.3).\n *\n * Before the payer has reached the provider (`atProvider={false}`, a trial without a\n * card, a beta) there is no portal to open: the part says so in a caption instead of\n * offering two buttons that could only end in `409 billing_not_at_provider`.\n *\n * 0.42 (§16): where the company's buyer is another admin (`portal=\"buyer_only\"`), the\n * portal stays the buyer's — \"Payment and invoices\" is a caption that names them, and\n * \"Cancel subscription\" is the in-app cancel (`onCancelInApp`). While a cancel is\n * scheduled (`cancelAtPeriodEnd`) and `onResume` is given, \"Keep subscription\" stands in\n * the cancel's place, whoever the viewer is.\n */\nexport function SubscriptionActions({\n manageHref,\n onManage,\n cancelHref,\n onCancel,\n onPortal,\n atProvider = true,\n portal = \"open\",\n buyerName,\n onCancelInApp,\n cancelAtPeriodEnd = false,\n onResume,\n pending,\n labels: labelsProp,\n className,\n ...rest\n}: SubscriptionActionsProps) {\n const labels = useBillingLabels(labelsProp);\n if (!atProvider) {\n return (\n <div {...rest} data-at-provider=\"false\" className={cn(\"text-sm text-muted\", className)}>\n <p>{labels.notAtProvider}</p>\n </div>\n );\n }\n const buyerOnly = portal === \"buyer_only\";\n const manageCall = onManage ?? (onPortal ? () => onPortal(\"overview\") : undefined);\n const portalCancel = onCancel ?? (onPortal ? () => onPortal(\"cancel\") : undefined);\n // Under \"buyer_only\" the in-app cancel only; under \"open\" the portal's first, the\n // in-app cancel only where no way to the portal's is given.\n const cancelCall = buyerOnly ? onCancelInApp : (portalCancel ?? (cancelHref === undefined ? onCancelInApp : undefined));\n const manage = buyerOnly ? (\n <p className=\"basis-full text-sm text-muted\" data-buyer-holds=\"\">\n {labels.buyerHolds(buyerName || undefined)}\n </p>\n ) : manageHref !== undefined ? (\n <Button href={manageHref} variant=\"secondary\">\n {labels.manage}\n </Button>\n ) : manageCall !== undefined ? (\n <Button type=\"button\" variant=\"secondary\" onClick={manageCall} pending={pending === \"manage\"}>\n {labels.manage}\n </Button>\n ) : null;\n const cancel =\n cancelAtPeriodEnd && onResume !== undefined ? (\n <Button type=\"button\" variant=\"link\" onClick={onResume} pending={pending === \"resume\"} data-keep-subscription=\"\">\n {labels.keepSubscription}\n </Button>\n ) : cancelHref !== undefined && !buyerOnly ? (\n <Button href={cancelHref} variant=\"link\" tone=\"danger\">\n {labels.cancel}\n </Button>\n ) : cancelCall !== undefined ? (\n <Button type=\"button\" variant=\"link\" tone=\"danger\" onClick={cancelCall} pending={pending === \"cancel\"}>\n {labels.cancel}\n </Button>\n ) : null;\n if (!manage && !cancel) return null;\n return (\n <div {...rest} className={cn(\"flex flex-wrap items-center gap-x-4 gap-y-2\", className)}>\n {manage}\n {cancel}\n </div>\n );\n}\n"],"mappings":";AAiHQ,cAuCJ,YAvCI;AA9GR,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB,SAAS,wBAAwB;AAuF1B,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA,oBAAoB;AAAA,EACpB;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA,GAAG;AACL,GAA6B;AAC3B,QAAM,SAAS,iBAAiB,UAAU;AAC1C,MAAI,CAAC,YAAY;AACf,WACE,oBAAC,SAAK,GAAG,MAAM,oBAAiB,SAAQ,WAAW,GAAG,sBAAsB,SAAS,GACnF,8BAAC,OAAG,iBAAO,eAAc,GAC3B;AAAA,EAEJ;AACA,QAAM,YAAY,WAAW;AAC7B,QAAM,aAAa,aAAa,WAAW,MAAM,SAAS,UAAU,IAAI;AACxE,QAAM,eAAe,aAAa,WAAW,MAAM,SAAS,QAAQ,IAAI;AAGxE,QAAM,aAAa,YAAY,gBAAiB,iBAAiB,eAAe,SAAY,gBAAgB;AAC5G,QAAM,SAAS,YACb,oBAAC,OAAE,WAAU,iCAAgC,oBAAiB,IAC3D,iBAAO,WAAW,aAAa,MAAS,GAC3C,IACE,eAAe,SACjB,oBAAC,UAAO,MAAM,YAAY,SAAQ,aAC/B,iBAAO,QACV,IACE,eAAe,SACjB,oBAAC,UAAO,MAAK,UAAS,SAAQ,aAAY,SAAS,YAAY,SAAS,YAAY,UACjF,iBAAO,QACV,IACE;AACJ,QAAM,SACJ,qBAAqB,aAAa,SAChC,oBAAC,UAAO,MAAK,UAAS,SAAQ,QAAO,SAAS,UAAU,SAAS,YAAY,UAAU,0BAAuB,IAC3G,iBAAO,kBACV,IACE,eAAe,UAAa,CAAC,YAC/B,oBAAC,UAAO,MAAM,YAAY,SAAQ,QAAO,MAAK,UAC3C,iBAAO,QACV,IACE,eAAe,SACjB,oBAAC,UAAO,MAAK,UAAS,SAAQ,QAAO,MAAK,UAAS,SAAS,YAAY,SAAS,YAAY,UAC1F,iBAAO,QACV,IACE;AACN,MAAI,CAAC,UAAU,CAAC,OAAQ,QAAO;AAC/B,SACE,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,+CAA+C,SAAS,GAClF;AAAA;AAAA,IACA;AAAA,KACH;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/billing/subscription-actions.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { ExternalLink } from \"lucide-react\";\n\nimport { DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { Button } from \"../components/button\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport type { BillingPortalAccess, PortalTarget } from \"./billing-overview\";\nimport type { CancelRequested } from \"./cancel-request\";\n\n/** The portal action's mark (§17.3): the external-link glyph after the words, and \"(opens\n * in a new tab)\" for a reader — on a link, `Button`'s `external` says the latter. */\nexport function ExternalMark({ reader }: { reader?: string }) {\n return (\n <>\n <ExternalLink aria-hidden data-external=\"\" className=\"size-4 shrink-0 rtl:-scale-x-100\" />\n {reader !== undefined && (\n <>\n {\" \"}\n <span className=\"sr-only\">({reader})</span>\n </>\n )}\n </>\n );\n}\n\nexport interface SubscriptionActionsProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n /** \"Payment and invoices\", as a link — or {@link onManage}. Without either, none. */\n manageHref?: string;\n /** \"Payment and invoices\", as the app's call: `POST /billing/portal` with\n * `{target: \"overview\"}`, then off to its `url` (§4, §14.5). The kit sends no\n * request. */\n onManage?: () => void;\n /** \"Cancel subscription\", as a link — or {@link onCancel}. Without either, none. */\n cancelHref?: string;\n /** \"Cancel subscription\", as the app's call: the portal again, with `{target:\n * \"cancel\"}` — its cancel link, where the provider's cancel and withdrawal functions\n * are (§12.26, §14.5). Not the overview. */\n onCancel?: () => void;\n /**\n * Both, as one call that says where to ({@link PortalTarget}, the body of\n * `POST /billing/portal`): `\"overview\"` for \"Payment and invoices\",\n * `\"cancel\"` for \"Cancel subscription\" — so the target can't be mixed up (kastlan's\n * cancel opened the overview). {@link onManage} and {@link onCancel} win over it.\n */\n onPortal?: (target: PortalTarget) => void;\n /**\n * The overview's `at_provider` (§14.5): whether the payer has reached the provider.\n * `false`: no customer there yet, so nothing to open — a caption (`notAtProvider`)\n * stands in place of both actions. `true`: both show, WHATEVER the status or source —\n * a payer who bought under a grant keeps the way to the invoices and to \"Cancel\"\n * (§12.26). Default true (0.32's behaviour).\n */\n atProvider?: boolean;\n /**\n * 0.42 (§16.2, server-kit 0.14): the overview's `portal`. `\"buyer_only\"` — the row has\n * a buyer and the viewer isn't them, so the portal would answer `403\n * billing_portal_buyer_only`: \"Payment and invoices\" becomes a caption naming the buyer\n * (`buyerHolds`, no button), and \"Cancel subscription\" calls {@link onCancelInApp}\n * (no portal at all — `cancelHref`, `onCancel` and `onPortal` are not used). `\"open\"`\n * (default): as before.\n */\n portal?: BillingPortalAccess;\n /** 0.42: the overview's `buyer_name`, for the caption under `\"buyer_only\"`. Empty: \"…with\n * the admin who bought the subscription\". */\n buyerName?: string | null;\n /**\n * 0.42 (§16.2 decision 2): \"Cancel subscription\" in the app — open\n * `CancelSubscriptionConfirm`, whose Confirm is `POST /billing/cancel`. The cancel under\n * `\"buyer_only\"`. Under `\"open\"` it is the last resort, used only when no portal cancel\n * (`cancelHref`, `onCancel`, `onPortal`) is given — a person-payer app that mounts the\n * route and not the portal's cancel.\n */\n onCancelInApp?: () => void;\n /** 0.42 (§16.7 C): the overview's `cancel_at_period_end` — a cancel is scheduled. With\n * {@link onResume}, \"Keep subscription\" takes the cancel's place. */\n cancelAtPeriodEnd?: boolean;\n /** 0.42 (§16.7 C): \"Keep subscription\", the app's `POST /billing/resume` — shown in\n * the cancel's place while {@link cancelAtPeriodEnd} is true. Without it, the cancel\n * stays as before. */\n onResume?: () => void;\n /**\n * 0.43 (kastlan `18bf4c0`): what was asked in the app and the overview does not show\n * yet — `useCancelRequest().view(row).requested`. `\"cancel\"`: shown as scheduled (\"Keep\n * subscription\" with {@link onResume}; without it, no second \"Cancel subscription\").\n * `\"resume\"`: shown as not scheduled. `null` (default): {@link cancelAtPeriodEnd} says.\n */\n cancelRequested?: CancelRequested;\n /**\n * 0.43 (§17): the portal opens in a NEW TAB — the app's handler calls\n * `openProviderPage` — so its actions carry the external-link mark, a link opens with\n * `target=\"_blank\"`, and the `portalOpensNewTab` note shows under them. Default true;\n * `false` for an app whose portal still opens in the same tab.\n */\n portalNewTab?: boolean;\n /** 0.43: the provider's name in the note. Default `\"Paddle\"` (§2 decision 16). */\n providerName?: string;\n /** 0.43: the app's name in the note (\"Kastlan stays open here\"). Without it, \"this page\n * stays open\". */\n appName?: string;\n /** Which one is under way: its spinner, no second press. `\"resume\"` since 0.42. */\n pending?: \"manage\" | \"cancel\" | \"resume\" | null;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/**\n * The two ways to the provider's hosted portal on the subscription page\n * (docs/billing-harmonization.md §2.8, §7, §12.26): \"Payment and invoices\" — the payment\n * method and the invoices live there, never in the app — and a VISIBLE \"Cancel\n * subscription\". Consumer law wants the second in plain sight: Germany's cancellation\n * button (§312k BGB) and the EU withdrawal function (Directive (EU) 2023/2673, from\n * 19 June 2026) are in the portal, and the app's page must not hide the way there behind\n * \"manage\".\n *\n * Neither takes the write lock: a read-only payer must be able to pay, leave or cancel\n * (§3.3).\n *\n * Before the payer has reached the provider (`atProvider={false}`, a trial without a\n * card, a beta) there is no portal to open: the part says so in a caption instead of\n * offering two buttons that could only end in `409 billing_not_at_provider`.\n *\n * 0.42 (§16): where the company's buyer is another admin (`portal=\"buyer_only\"`), the\n * portal stays the buyer's — \"Payment and invoices\" is a caption that names them, and\n * \"Cancel subscription\" is the in-app cancel (`onCancelInApp`). While a cancel is\n * scheduled (`cancelAtPeriodEnd`) and `onResume` is given, \"Keep subscription\" stands in\n * the cancel's place, whoever the viewer is.\n *\n * 0.43 (§17): the portal opens in a new tab (`openProviderPage` in the app's handler):\n * its actions carry the external-link mark, and a muted note says so — \"Payment and\n * invoices open at {provider} in a new tab; {app} stays open here.\" The in-app cancel and\n * \"Keep subscription\" stay unmarked. After an in-app cancel or resume, `cancelRequested`\n * (`useCancelRequest`) shows what was asked until the provider's webhook lands.\n */\nexport function SubscriptionActions({\n manageHref,\n onManage,\n cancelHref,\n onCancel,\n onPortal,\n atProvider = true,\n portal = \"open\",\n buyerName,\n onCancelInApp,\n cancelAtPeriodEnd = false,\n onResume,\n cancelRequested = null,\n portalNewTab = true,\n providerName = \"Paddle\",\n appName,\n pending,\n labels: labelsProp,\n className,\n ...rest\n}: SubscriptionActionsProps) {\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n if (!atProvider) {\n return (\n <div {...rest} data-at-provider=\"false\" className={cn(\"text-sm text-muted\", className)}>\n <p>{labels.notAtProvider}</p>\n </div>\n );\n }\n const buyerOnly = portal === \"buyer_only\";\n const manageCall = onManage ?? (onPortal ? () => onPortal(\"overview\") : undefined);\n const portalCancel = onCancel ?? (onPortal ? () => onPortal(\"cancel\") : undefined);\n // Under \"buyer_only\" the in-app cancel only; under \"open\" the portal's first, the\n // in-app cancel only where no way to the portal's is given.\n const cancelCall = buyerOnly ? onCancelInApp : (portalCancel ?? (cancelHref === undefined ? onCancelInApp : undefined));\n // 0.43: what was asked in the app wins over the row until the row moves.\n const scheduled = cancelRequested === \"cancel\" ? true : cancelRequested === \"resume\" ? false : cancelAtPeriodEnd;\n // 0.43 (§17): the portal's actions are marked, a link opens a tab of its own.\n const newTab = portalNewTab && !buyerOnly;\n const words = (text: string, external: boolean, link: boolean): ReactNode =>\n external ? (\n <>\n {text}\n <ExternalMark reader={link ? undefined : common.opensInNewTab} />\n </>\n ) : (\n text\n );\n const portalCancelShown = !buyerOnly && (cancelHref !== undefined || portalCancel !== undefined);\n const manage = buyerOnly ? (\n <p className=\"basis-full text-sm text-muted\" data-buyer-holds=\"\">\n {labels.buyerHolds(buyerName || undefined)}\n </p>\n ) : manageHref !== undefined ? (\n <Button href={manageHref} variant=\"secondary\" external={newTab || undefined} data-portal-action=\"manage\">\n {words(labels.manage, newTab, true)}\n </Button>\n ) : manageCall !== undefined ? (\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={manageCall}\n pending={pending === \"manage\"}\n className={newTab ? \"relative\" : undefined}\n data-portal-action=\"manage\"\n >\n {words(labels.manage, newTab, false)}\n </Button>\n ) : null;\n const cancel =\n scheduled && onResume !== undefined ? (\n <Button type=\"button\" variant=\"link\" onClick={onResume} pending={pending === \"resume\"} data-keep-subscription=\"\">\n {labels.keepSubscription}\n </Button>\n ) : cancelRequested === \"cancel\" ? null : cancelHref !== undefined && !buyerOnly ? (\n <Button href={cancelHref} variant=\"link\" tone=\"danger\" external={newTab || undefined} data-portal-action=\"cancel\">\n {words(labels.cancel, newTab, true)}\n </Button>\n ) : cancelCall !== undefined ? (\n <Button\n type=\"button\"\n variant=\"link\"\n tone=\"danger\"\n onClick={cancelCall}\n pending={pending === \"cancel\"}\n className={newTab && portalCancelShown ? \"relative\" : undefined}\n data-portal-action={portalCancelShown ? \"cancel\" : undefined}\n >\n {words(labels.cancel, newTab && portalCancelShown, false)}\n </Button>\n ) : null;\n if (!manage && !cancel) return null;\n // The note while a way to the portal shows: \"Payment and invoices open at Paddle in a\n // new tab; Kastlan stays open here.\"\n const portalShown = !buyerOnly && (manage !== null || (cancel !== null && portalCancelShown && !(scheduled && onResume)));\n return (\n <div {...rest} className={cn(\"flex flex-wrap items-center gap-x-4 gap-y-2\", className)}>\n {manage}\n {cancel}\n {newTab && portalShown && (\n <p className=\"basis-full text-sm text-muted\" data-portal-note=\"\">\n {labels.portalOpensNewTab(providerName, appName || undefined)}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -38,7 +38,14 @@ interface AccountChipLook {
|
|
|
38
38
|
export interface RoleDefinition {
|
|
39
39
|
label: string;
|
|
40
40
|
tone?: ChipTone;
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* A lucide component or an element — Kurvenschmiede marks its admins with a shield.
|
|
43
|
+
* Unlike {@link href} and {@link variant}, it shows EVERYWHERE the role is drawn: in
|
|
44
|
+
* {@link AccountRoleChip} and in {@link RoleChip} (the roster's column) alike. That is
|
|
45
|
+
* intended (0.43, keksdose's question): the glyph is part of how the role reads, so the
|
|
46
|
+
* shield on the header's chip is the shield in the users list. An app that wants the
|
|
47
|
+
* glyph in one place only hands the other a second vocabulary without `icon`.
|
|
48
|
+
*/
|
|
42
49
|
icon?: LucideIcon | ReactElement;
|
|
43
50
|
/**
|
|
44
51
|
* 0.42.0 (docs/admin-harmonization.md §2.1, settings §12.3): the role's tool — an
|