@softure-ai/billing 0.0.0-stage → 0.1.5
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/LICENSE +21 -0
- package/README.md +699 -2
- package/dist/calendar.d.ts +26 -0
- package/dist/calendar.d.ts.map +1 -0
- package/dist/calendar.js +81 -0
- package/dist/calendar.js.map +1 -0
- package/dist/contract.d.ts +187 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +6 -0
- package/dist/contract.js.map +1 -0
- package/dist/currency-digits.d.ts +3 -0
- package/dist/currency-digits.d.ts.map +1 -0
- package/dist/currency-digits.js +32 -0
- package/dist/currency-digits.js.map +1 -0
- package/dist/entitlement.d.ts +25 -0
- package/dist/entitlement.d.ts.map +1 -0
- package/dist/entitlement.js +75 -0
- package/dist/entitlement.js.map +1 -0
- package/dist/fields.d.ts +25 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +27 -0
- package/dist/fields.js.map +1 -0
- package/dist/index.d.ts +274 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/invoice.d.ts +29 -0
- package/dist/invoice.d.ts.map +1 -0
- package/dist/invoice.js +35 -0
- package/dist/invoice.js.map +1 -0
- package/dist/mailing/index.d.ts +2 -0
- package/dist/mailing/index.d.ts.map +1 -0
- package/dist/mailing/index.js +4 -0
- package/dist/mailing/index.js.map +1 -0
- package/dist/mailing/reminder-mail.d.ts +50 -0
- package/dist/mailing/reminder-mail.d.ts.map +1 -0
- package/dist/mailing/reminder-mail.js +71 -0
- package/dist/mailing/reminder-mail.js.map +1 -0
- package/dist/manual.d.ts +12 -0
- package/dist/manual.d.ts.map +1 -0
- package/dist/manual.js +22 -0
- package/dist/manual.js.map +1 -0
- package/dist/messages/en.d.ts +176 -0
- package/dist/messages/en.d.ts.map +1 -0
- package/dist/messages/en.js +151 -0
- package/dist/messages/en.js.map +1 -0
- package/dist/messages/index.d.ts +359 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/index.js +14 -0
- package/dist/messages/index.js.map +1 -0
- package/dist/messages/pl.d.ts +3 -0
- package/dist/messages/pl.d.ts.map +1 -0
- package/dist/messages/pl.js +151 -0
- package/dist/messages/pl.js.map +1 -0
- package/dist/next/access.d.ts +16 -0
- package/dist/next/access.d.ts.map +1 -0
- package/dist/next/access.js +24 -0
- package/dist/next/access.js.map +1 -0
- package/dist/next/actions.d.ts +24 -0
- package/dist/next/actions.d.ts.map +1 -0
- package/dist/next/actions.js +158 -0
- package/dist/next/actions.js.map +1 -0
- package/dist/next/context.d.ts +4 -0
- package/dist/next/context.d.ts.map +1 -0
- package/dist/next/context.js +17 -0
- package/dist/next/context.js.map +1 -0
- package/dist/next/current-entitlement.d.ts +18 -0
- package/dist/next/current-entitlement.d.ts.map +1 -0
- package/dist/next/current-entitlement.js +26 -0
- package/dist/next/current-entitlement.js.map +1 -0
- package/dist/next/index.d.ts +8 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/index.js +12 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/pages.d.ts +24 -0
- package/dist/next/pages.d.ts.map +1 -0
- package/dist/next/pages.js +166 -0
- package/dist/next/pages.js.map +1 -0
- package/dist/next/pricing.d.ts +12 -0
- package/dist/next/pricing.d.ts.map +1 -0
- package/dist/next/pricing.js +20 -0
- package/dist/next/pricing.js.map +1 -0
- package/dist/next/route.d.ts +11 -0
- package/dist/next/route.d.ts.map +1 -0
- package/dist/next/route.js +56 -0
- package/dist/next/route.js.map +1 -0
- package/dist/options.d.ts +85 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +121 -0
- package/dist/options.js.map +1 -0
- package/dist/payment.d.ts +61 -0
- package/dist/payment.d.ts.map +1 -0
- package/dist/payment.js +12 -0
- package/dist/payment.js.map +1 -0
- package/dist/plans.d.ts +20 -0
- package/dist/plans.d.ts.map +1 -0
- package/dist/plans.js +53 -0
- package/dist/plans.js.map +1 -0
- package/dist/price.d.ts +12 -0
- package/dist/price.d.ts.map +1 -0
- package/dist/price.js +37 -0
- package/dist/price.js.map +1 -0
- package/dist/refund.d.ts +70 -0
- package/dist/refund.d.ts.map +1 -0
- package/dist/refund.js +109 -0
- package/dist/refund.js.map +1 -0
- package/dist/reminder.d.ts +30 -0
- package/dist/reminder.d.ts.map +1 -0
- package/dist/reminder.js +34 -0
- package/dist/reminder.js.map +1 -0
- package/dist/schema.d.ts +1023 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +77 -0
- package/dist/schema.js.map +1 -0
- package/dist/scripts/entitlement-scripts.d.ts +17 -0
- package/dist/scripts/entitlement-scripts.d.ts.map +1 -0
- package/dist/scripts/entitlement-scripts.js +167 -0
- package/dist/scripts/entitlement-scripts.js.map +1 -0
- package/dist/scripts/index.d.ts +3 -0
- package/dist/scripts/index.d.ts.map +1 -0
- package/dist/scripts/index.js +5 -0
- package/dist/scripts/index.js.map +1 -0
- package/dist/scripts/plan-scripts.d.ts +19 -0
- package/dist/scripts/plan-scripts.d.ts.map +1 -0
- package/dist/scripts/plan-scripts.js +106 -0
- package/dist/scripts/plan-scripts.js.map +1 -0
- package/dist/server/entitlements.d.ts +69 -0
- package/dist/server/entitlements.d.ts.map +1 -0
- package/dist/server/entitlements.js +202 -0
- package/dist/server/entitlements.js.map +1 -0
- package/dist/server/grants.d.ts +74 -0
- package/dist/server/grants.d.ts.map +1 -0
- package/dist/server/grants.js +174 -0
- package/dist/server/grants.js.map +1 -0
- package/dist/server/health.d.ts +3 -0
- package/dist/server/health.d.ts.map +1 -0
- package/dist/server/health.js +17 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +14 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/options.d.ts +20 -0
- package/dist/server/options.d.ts.map +1 -0
- package/dist/server/options.js +38 -0
- package/dist/server/options.js.map +1 -0
- package/dist/server/payments.d.ts +140 -0
- package/dist/server/payments.d.ts.map +1 -0
- package/dist/server/payments.js +339 -0
- package/dist/server/payments.js.map +1 -0
- package/dist/server/plans.d.ts +50 -0
- package/dist/server/plans.d.ts.map +1 -0
- package/dist/server/plans.js +129 -0
- package/dist/server/plans.js.map +1 -0
- package/dist/server/privacy.d.ts +77 -0
- package/dist/server/privacy.d.ts.map +1 -0
- package/dist/server/privacy.js +110 -0
- package/dist/server/privacy.js.map +1 -0
- package/dist/server/reminders.d.ts +20 -0
- package/dist/server/reminders.d.ts.map +1 -0
- package/dist/server/reminders.js +85 -0
- package/dist/server/reminders.js.map +1 -0
- package/dist/server/requests.d.ts +80 -0
- package/dist/server/requests.d.ts.map +1 -0
- package/dist/server/requests.js +155 -0
- package/dist/server/requests.js.map +1 -0
- package/dist/server/setup.d.ts +9 -0
- package/dist/server/setup.d.ts.map +1 -0
- package/dist/server/setup.js +34 -0
- package/dist/server/setup.js.map +1 -0
- package/dist/server/take-back.d.ts +80 -0
- package/dist/server/take-back.d.ts.map +1 -0
- package/dist/server/take-back.js +138 -0
- package/dist/server/take-back.js.map +1 -0
- package/dist/server/user-id.d.ts +4 -0
- package/dist/server/user-id.d.ts.map +1 -0
- package/dist/server/user-id.js +11 -0
- package/dist/server/user-id.js.map +1 -0
- package/dist/stripe-currency.d.ts +27 -0
- package/dist/stripe-currency.d.ts.map +1 -0
- package/dist/stripe-currency.js +57 -0
- package/dist/stripe-currency.js.map +1 -0
- package/dist/stripe-webhook.d.ts +101 -0
- package/dist/stripe-webhook.d.ts.map +1 -0
- package/dist/stripe-webhook.js +209 -0
- package/dist/stripe-webhook.js.map +1 -0
- package/dist/stripe.d.ts +25 -0
- package/dist/stripe.d.ts.map +1 -0
- package/dist/stripe.js +116 -0
- package/dist/stripe.js.map +1 -0
- package/dist/ui/access-badge.d.ts +16 -0
- package/dist/ui/access-badge.d.ts.map +1 -0
- package/dist/ui/access-badge.js +43 -0
- package/dist/ui/access-badge.js.map +1 -0
- package/dist/ui/access-notice.d.ts +20 -0
- package/dist/ui/access-notice.d.ts.map +1 -0
- package/dist/ui/access-notice.js +41 -0
- package/dist/ui/access-notice.js.map +1 -0
- package/dist/ui/format.d.ts +11 -0
- package/dist/ui/format.d.ts.map +1 -0
- package/dist/ui/format.js +21 -0
- package/dist/ui/format.js.map +1 -0
- package/dist/ui/grant-form.d.ts +18 -0
- package/dist/ui/grant-form.d.ts.map +1 -0
- package/dist/ui/grant-form.js +23 -0
- package/dist/ui/grant-form.js.map +1 -0
- package/dist/ui/grant-history.d.ts +39 -0
- package/dist/ui/grant-history.d.ts.map +1 -0
- package/dist/ui/grant-history.js +36 -0
- package/dist/ui/grant-history.js.map +1 -0
- package/dist/ui/index.d.ts +9 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +12 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/payment-form.d.ts +23 -0
- package/dist/ui/payment-form.d.ts.map +1 -0
- package/dist/ui/payment-form.js +34 -0
- package/dist/ui/payment-form.js.map +1 -0
- package/dist/ui/payment-requests.d.ts +30 -0
- package/dist/ui/payment-requests.d.ts.map +1 -0
- package/dist/ui/payment-requests.js +31 -0
- package/dist/ui/payment-requests.js.map +1 -0
- package/dist/ui/pricing-tiles.d.ts +22 -0
- package/dist/ui/pricing-tiles.d.ts.map +1 -0
- package/dist/ui/pricing-tiles.js +40 -0
- package/dist/ui/pricing-tiles.js.map +1 -0
- package/migrations/0001_create_entitlements.sql +16 -0
- package/migrations/0002_create_payments.sql +30 -0
- package/migrations/0003_record_payment_grants.sql +25 -0
- package/migrations/0004_create_requests_and_grants.sql +58 -0
- package/migrations/0005_record_refunded_amounts.sql +18 -0
- package/migrations/0006_record_request_handover_and_prices.sql +38 -0
- package/migrations/0007_record_failed_refunds.sql +27 -0
- package/migrations/0008_record_request_handover_claims.sql +9 -0
- package/migrations/0009_record_pending_charge_states.sql +16 -0
- package/module.json +23 -0
- package/package.json +81 -4
- package/src/calendar.ts +90 -0
- package/src/contract.ts +181 -0
- package/src/currency-digits.ts +37 -0
- package/src/entitlement.ts +84 -0
- package/src/fields.ts +37 -0
- package/src/index.ts +163 -0
- package/src/invoice.ts +58 -0
- package/src/mailing/index.ts +11 -0
- package/src/mailing/reminder-mail.ts +108 -0
- package/src/manual.ts +31 -0
- package/src/messages/en.ts +150 -0
- package/src/messages/index.ts +18 -0
- package/src/messages/pl.ts +152 -0
- package/src/next/access.tsx +55 -0
- package/src/next/actions.ts +176 -0
- package/src/next/context.ts +18 -0
- package/src/next/current-entitlement.ts +36 -0
- package/src/next/index.ts +11 -0
- package/src/next/next-modules.d.ts +21 -0
- package/src/next/pages.tsx +267 -0
- package/src/next/pricing.tsx +39 -0
- package/src/next/route.ts +57 -0
- package/src/options.ts +128 -0
- package/src/payment.ts +77 -0
- package/src/plans.ts +63 -0
- package/src/price.ts +50 -0
- package/src/refund.ts +135 -0
- package/src/reminder.ts +52 -0
- package/src/schema.ts +86 -0
- package/src/scripts/entitlement-scripts.ts +188 -0
- package/src/scripts/index.ts +11 -0
- package/src/scripts/plan-scripts.ts +143 -0
- package/src/server/entitlements.ts +227 -0
- package/src/server/grants.ts +227 -0
- package/src/server/health.ts +18 -0
- package/src/server/index.ts +83 -0
- package/src/server/options.ts +52 -0
- package/src/server/payments.ts +434 -0
- package/src/server/plans.ts +143 -0
- package/src/server/privacy.ts +189 -0
- package/src/server/reminders.ts +113 -0
- package/src/server/requests.ts +201 -0
- package/src/server/setup.ts +37 -0
- package/src/server/take-back.ts +190 -0
- package/src/server/user-id.ts +12 -0
- package/src/stripe-currency.ts +65 -0
- package/src/stripe-webhook.ts +278 -0
- package/src/stripe.ts +130 -0
- package/src/ui/access-badge.tsx +64 -0
- package/src/ui/access-notice.tsx +73 -0
- package/src/ui/format.ts +25 -0
- package/src/ui/grant-form.tsx +69 -0
- package/src/ui/grant-history.tsx +127 -0
- package/src/ui/index.ts +25 -0
- package/src/ui/payment-form.tsx +111 -0
- package/src/ui/payment-requests.tsx +126 -0
- package/src/ui/pricing-tiles.tsx +105 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button, ButtonLink, type ClassNames, createSlotClassGetter, FormError, type LinkComponentType } from "@softure-ai/ui";
|
|
4
|
+
import { useActionState } from "react";
|
|
5
|
+
import { INITIAL_ADMIN_ACTION_STATE, type AdminActionState } from "../contract.js";
|
|
6
|
+
import { REQUEST_FIELD } from "../fields.js";
|
|
7
|
+
import { getBillingErrorMessage, type BillingMessages } from "../messages/index.js";
|
|
8
|
+
|
|
9
|
+
// The admin page's open invoice requests, each with a Grant and a Dismiss button and a link to its
|
|
10
|
+
// account's history. Rows come prepared from the server (copy and dates in the app's locale); each
|
|
11
|
+
// button submits its row's id to its server action, which checks the admin role first and refreshes
|
|
12
|
+
// the page, so a granted or dismissed request leaves the list.
|
|
13
|
+
|
|
14
|
+
export type AdminAction = (previous: AdminActionState, formData: FormData) => Promise<AdminActionState>;
|
|
15
|
+
|
|
16
|
+
export type PaymentRequestListSlot = "root" | "empty" | "item" | "itemHeader" | "title" | "details" | "actions";
|
|
17
|
+
|
|
18
|
+
/** One open request, prepared on the server. */
|
|
19
|
+
export interface PaymentRequestRow {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
/** The account's email and the plan, e.g. "ada@example.com · Monthly". */
|
|
22
|
+
readonly title: string;
|
|
23
|
+
/** When it was asked for and the invoice details, one line each. */
|
|
24
|
+
readonly details: readonly string[];
|
|
25
|
+
/** The admin page showing this account's history. */
|
|
26
|
+
readonly historyHref: string;
|
|
27
|
+
/** Accessible names of the buttons, naming the request. */
|
|
28
|
+
readonly grantLabel: string;
|
|
29
|
+
readonly dismissLabel: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PaymentRequestListProps {
|
|
33
|
+
readonly requests: readonly PaymentRequestRow[];
|
|
34
|
+
readonly grantAction: AdminAction;
|
|
35
|
+
readonly dismissAction: AdminAction;
|
|
36
|
+
readonly messages: BillingMessages;
|
|
37
|
+
/** The app's link component, e.g. Next's `Link`; a plain `<a>` when omitted. */
|
|
38
|
+
readonly LinkComponent?: LinkComponentType;
|
|
39
|
+
readonly classNames?: ClassNames<PaymentRequestListSlot>;
|
|
40
|
+
readonly unstyled?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const DEFAULT_CLASSES: Readonly<Record<PaymentRequestListSlot, string>> = {
|
|
44
|
+
root: "sft:m-0 sft:flex sft:list-none sft:flex-col sft:gap-3 sft:p-0 sft:font-sans",
|
|
45
|
+
empty: "sft:m-0 sft:font-sans sft:text-sm sft:text-muted",
|
|
46
|
+
item: "sft:flex sft:flex-col sft:gap-1 sft:border-t sft:border-border sft:pt-3 sft:first-of-type:border-t-0 sft:first-of-type:pt-0",
|
|
47
|
+
itemHeader: "sft:flex sft:items-center sft:justify-between sft:gap-2",
|
|
48
|
+
title: "sft:m-0 sft:text-sm sft:font-semibold sft:text-foreground",
|
|
49
|
+
details: "sft:m-0 sft:text-xs sft:text-muted",
|
|
50
|
+
actions: "sft:flex sft:items-center sft:gap-2",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type Slot = (part: PaymentRequestListSlot) => string | undefined;
|
|
54
|
+
|
|
55
|
+
function RequestItem({
|
|
56
|
+
row,
|
|
57
|
+
grantAction,
|
|
58
|
+
dismissAction,
|
|
59
|
+
messages,
|
|
60
|
+
LinkComponent,
|
|
61
|
+
slot,
|
|
62
|
+
unstyled,
|
|
63
|
+
}: {
|
|
64
|
+
row: PaymentRequestRow;
|
|
65
|
+
grantAction: AdminAction;
|
|
66
|
+
dismissAction: AdminAction;
|
|
67
|
+
messages: BillingMessages;
|
|
68
|
+
LinkComponent: LinkComponentType | undefined;
|
|
69
|
+
slot: Slot;
|
|
70
|
+
unstyled: boolean | undefined;
|
|
71
|
+
}) {
|
|
72
|
+
const [granted, grant, isGranting] = useActionState(grantAction, INITIAL_ADMIN_ACTION_STATE);
|
|
73
|
+
const [dismissed, dismiss, isDismissing] = useActionState(dismissAction, INITIAL_ADMIN_ACTION_STATE);
|
|
74
|
+
const copy = messages.admin.requests;
|
|
75
|
+
const failed = granted.status === "error" ? granted : dismissed.status === "error" ? dismissed : null;
|
|
76
|
+
const isPending = isGranting || isDismissing;
|
|
77
|
+
return (
|
|
78
|
+
<li className={slot("item")} data-request-id={row.id}>
|
|
79
|
+
<p className={slot("title")}>{row.title}</p>
|
|
80
|
+
{row.details.map((line) => (
|
|
81
|
+
<p key={line} className={slot("details")}>
|
|
82
|
+
{line}
|
|
83
|
+
</p>
|
|
84
|
+
))}
|
|
85
|
+
<div className={slot("actions")}>
|
|
86
|
+
<form action={grant}>
|
|
87
|
+
<input type="hidden" name={REQUEST_FIELD} value={row.id} />
|
|
88
|
+
<Button type="submit" variant="primary" size="sm" pending={isGranting} disabled={isPending} aria-label={row.grantLabel} unstyled={unstyled}>
|
|
89
|
+
{isGranting ? copy.granting : copy.grant}
|
|
90
|
+
</Button>
|
|
91
|
+
</form>
|
|
92
|
+
<form action={dismiss}>
|
|
93
|
+
<input type="hidden" name={REQUEST_FIELD} value={row.id} />
|
|
94
|
+
<Button type="submit" variant="ghost" size="sm" pending={isDismissing} disabled={isPending} aria-label={row.dismissLabel} unstyled={unstyled}>
|
|
95
|
+
{isDismissing ? copy.dismissing : copy.dismiss}
|
|
96
|
+
</Button>
|
|
97
|
+
</form>
|
|
98
|
+
<ButtonLink href={row.historyHref} variant="ghost" size="sm" LinkComponent={LinkComponent} unstyled={unstyled}>
|
|
99
|
+
{copy.history}
|
|
100
|
+
</ButtonLink>
|
|
101
|
+
</div>
|
|
102
|
+
<FormError message={failed === null ? undefined : getBillingErrorMessage(messages, failed.error)} unstyled={unstyled} />
|
|
103
|
+
</li>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function PaymentRequestList({ requests, grantAction, dismissAction, messages, LinkComponent, classNames, unstyled }: PaymentRequestListProps) {
|
|
108
|
+
const slot = createSlotClassGetter({ defaults: DEFAULT_CLASSES, classNames, unstyled });
|
|
109
|
+
if (requests.length === 0) return <p className={slot("empty")}>{messages.admin.requests.empty}</p>;
|
|
110
|
+
return (
|
|
111
|
+
<ul className={slot("root")} aria-label={messages.admin.requests.title}>
|
|
112
|
+
{requests.map((row) => (
|
|
113
|
+
<RequestItem
|
|
114
|
+
key={row.id}
|
|
115
|
+
row={row}
|
|
116
|
+
grantAction={grantAction}
|
|
117
|
+
dismissAction={dismissAction}
|
|
118
|
+
messages={messages}
|
|
119
|
+
LinkComponent={LinkComponent}
|
|
120
|
+
slot={slot}
|
|
121
|
+
unstyled={unstyled}
|
|
122
|
+
/>
|
|
123
|
+
))}
|
|
124
|
+
</ul>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// The plans of `billing({ plans })` as tiles: name, price, period, description, feature lines and a
|
|
2
|
+
// button to choose the plan. Server-safe and framework-free: it renders from props, and the button
|
|
3
|
+
// goes through an injected `LinkComponent` (`Pricing` and `PaymentPage` in `/next` wire it).
|
|
4
|
+
import { formatMessage, type Locale } from "@softure-ai/core";
|
|
5
|
+
import { ButtonLink, CheckIcon, type ClassNames, createSlotClassGetter, EmptyState, type LinkComponentType } from "@softure-ai/ui";
|
|
6
|
+
import type { Plan } from "../contract.js";
|
|
7
|
+
import type { BillingMessages } from "../messages/index.js";
|
|
8
|
+
import { getLocalizedText } from "../plans.js";
|
|
9
|
+
import { formatPrice } from "../price.js";
|
|
10
|
+
import { formatPeriod } from "./format.js";
|
|
11
|
+
|
|
12
|
+
export type PricingTilesSlot = "root" | "tile" | "badge" | "name" | "description" | "priceRow" | "price" | "period" | "features" | "feature" | "featureIcon" | "action";
|
|
13
|
+
|
|
14
|
+
export interface PricingTilesProps {
|
|
15
|
+
readonly plans: readonly Plan[];
|
|
16
|
+
readonly messages: BillingMessages;
|
|
17
|
+
readonly locale: Locale;
|
|
18
|
+
/** Where a tile's button leads, e.g. the payment page with the plan; no button when omitted. */
|
|
19
|
+
readonly getPlanHref?: (plan: Plan) => string;
|
|
20
|
+
/** The plan already chosen: its tile is marked `aria-current` and has no button. */
|
|
21
|
+
readonly selectedPlanId?: string;
|
|
22
|
+
/** Renders the buttons; a plain `<a>` when omitted. */
|
|
23
|
+
readonly LinkComponent?: LinkComponentType;
|
|
24
|
+
/** Names the list of plans for assistive technology. */
|
|
25
|
+
readonly label?: string;
|
|
26
|
+
readonly classNames?: ClassNames<PricingTilesSlot>;
|
|
27
|
+
readonly unstyled?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TILE_CLASS = "sft:flex sft:flex-col sft:gap-3 sft:rounded-card sft:border sft:bg-surface sft:p-5 sft:font-sans";
|
|
31
|
+
|
|
32
|
+
const DEFAULT_CLASSES: Readonly<Record<PricingTilesSlot, string>> = {
|
|
33
|
+
root: "sft:m-0 sft:flex sft:list-none sft:flex-col sft:gap-4 sft:p-0",
|
|
34
|
+
tile: `${TILE_CLASS} sft:border-border`,
|
|
35
|
+
badge: "sft:m-0 sft:w-fit sft:rounded-full sft:bg-accent-fill sft:px-2.5 sft:py-1 sft:text-xs sft:font-medium sft:text-on-accent",
|
|
36
|
+
name: "sft:m-0 sft:font-heading sft:text-lg sft:font-semibold sft:text-foreground",
|
|
37
|
+
description: "sft:m-0 sft:text-sm sft:text-muted",
|
|
38
|
+
priceRow: "sft:m-0 sft:flex sft:items-baseline sft:gap-2",
|
|
39
|
+
price: "sft:text-2xl sft:font-bold sft:tabular-nums sft:text-foreground",
|
|
40
|
+
period: "sft:text-sm sft:text-muted",
|
|
41
|
+
features: "sft:m-0 sft:flex sft:list-none sft:flex-col sft:gap-1.5 sft:p-0 sft:text-sm sft:text-foreground",
|
|
42
|
+
feature: "sft:flex sft:items-start sft:gap-2",
|
|
43
|
+
featureIcon: "sft:mt-0.5 sft:size-4 sft:shrink-0 sft:text-success",
|
|
44
|
+
action: "sft:flex",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** A featured or chosen tile stands out with a stronger border and a shadow. */
|
|
48
|
+
const PROMINENT_CLASSES: Readonly<Record<PricingTilesSlot, string>> = { ...DEFAULT_CLASSES, tile: `${TILE_CLASS} sft:border-border-strong sft:shadow-2` };
|
|
49
|
+
|
|
50
|
+
export function PricingTiles({ plans, messages, locale, getPlanHref, selectedPlanId, LinkComponent, label, classNames, unstyled }: PricingTilesProps) {
|
|
51
|
+
if (plans.length === 0) return <EmptyState title={messages.pricing.empty} unstyled={unstyled} />;
|
|
52
|
+
const regular = createSlotClassGetter({ defaults: DEFAULT_CLASSES, classNames, unstyled });
|
|
53
|
+
const prominent = createSlotClassGetter({ defaults: PROMINENT_CLASSES, classNames, unstyled });
|
|
54
|
+
return (
|
|
55
|
+
<ul className={regular("root")} aria-label={label}>
|
|
56
|
+
{plans.map((plan) => {
|
|
57
|
+
const name = getLocalizedText(plan.name, locale);
|
|
58
|
+
const isSelected = plan.id === selectedPlanId;
|
|
59
|
+
const href = isSelected ? undefined : getPlanHref?.(plan);
|
|
60
|
+
const slot = plan.isFeatured || isSelected ? prominent : regular;
|
|
61
|
+
return (
|
|
62
|
+
<li
|
|
63
|
+
key={plan.id}
|
|
64
|
+
className={slot("tile")}
|
|
65
|
+
data-plan={plan.id}
|
|
66
|
+
data-featured={plan.isFeatured ? "true" : undefined}
|
|
67
|
+
aria-current={isSelected ? "true" : undefined}
|
|
68
|
+
>
|
|
69
|
+
{plan.isFeatured ? <p className={slot("badge")}>{messages.pricing.featured}</p> : null}
|
|
70
|
+
<h3 className={slot("name")}>{name}</h3>
|
|
71
|
+
<p className={slot("priceRow")}>
|
|
72
|
+
<span className={slot("price")}>{formatPrice(plan.price, locale)}</span>
|
|
73
|
+
<span className={slot("period")}>{formatPeriod(plan.period, locale, messages)}</span>
|
|
74
|
+
</p>
|
|
75
|
+
{plan.description === undefined ? null : <p className={slot("description")}>{getLocalizedText(plan.description, locale)}</p>}
|
|
76
|
+
{plan.features.length === 0 ? null : (
|
|
77
|
+
<ul className={slot("features")}>
|
|
78
|
+
{plan.features.map((feature, index) => (
|
|
79
|
+
// Feature lines have no id; their order in the config is stable.
|
|
80
|
+
<li key={index} className={slot("feature")}>
|
|
81
|
+
<CheckIcon className={slot("featureIcon")} />
|
|
82
|
+
<span>{getLocalizedText(feature, locale)}</span>
|
|
83
|
+
</li>
|
|
84
|
+
))}
|
|
85
|
+
</ul>
|
|
86
|
+
)}
|
|
87
|
+
{href === undefined ? null : (
|
|
88
|
+
<div className={slot("action")}>
|
|
89
|
+
<ButtonLink
|
|
90
|
+
href={href}
|
|
91
|
+
LinkComponent={LinkComponent}
|
|
92
|
+
variant={plan.isFeatured ? "primary" : "secondary"}
|
|
93
|
+
fullWidth
|
|
94
|
+
unstyled={unstyled}
|
|
95
|
+
>
|
|
96
|
+
{formatMessage(messages.pricing.choose, { plan: name })}
|
|
97
|
+
</ButtonLink>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
</li>
|
|
101
|
+
);
|
|
102
|
+
})}
|
|
103
|
+
</ul>
|
|
104
|
+
);
|
|
105
|
+
}
|