@eifi1/ui-kit 0.32.2 → 0.33.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 +16 -8
- package/dist/account/delete-account-setting.d.ts +10 -10
- package/dist/account/delete-account-setting.js +2 -1
- package/dist/account/delete-account-setting.js.map +1 -1
- package/dist/account/email-change-setting.d.ts +5 -0
- package/dist/account/email-change-setting.js +4 -3
- package/dist/account/email-change-setting.js.map +1 -1
- package/dist/account/sessions-setting.d.ts +4 -0
- package/dist/account/sessions-setting.js +3 -2
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.d.ts +10 -10
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/invitations-panel.d.ts +10 -10
- package/dist/admin/invitations-panel.js +4 -3
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/plan-change.d.ts +100 -0
- package/dist/admin/plan-change.js +208 -0
- package/dist/admin/plan-change.js.map +1 -0
- package/dist/admin/reviewer-scope-editor.d.ts +10 -10
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/admin/role-select.d.ts +7 -7
- package/dist/admin/role-select.js.map +1 -1
- package/dist/admin/transfer-ownership-dialog.d.ts +10 -10
- package/dist/admin/user-roster.d.ts +7 -7
- package/dist/admin/user-roster.js +2 -2
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/auth-errors.d.ts +13 -2
- package/dist/auth/auth-errors.js +5 -1
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/complete-name-dialog.d.ts +4 -1
- package/dist/auth/complete-name-dialog.js +2 -1
- package/dist/auth/complete-name-dialog.js.map +1 -1
- package/dist/auth/register-form.d.ts +10 -10
- package/dist/billing/billing-banner.d.ts +13 -11
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/billing-labels.d.ts +10 -10
- package/dist/billing/billing-labels.js +10 -1
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/billing-lock.d.ts +39 -12
- package/dist/billing/billing-lock.js +9 -3
- package/dist/billing/billing-lock.js.map +1 -1
- package/dist/billing/billing-overview.d.ts +154 -0
- package/dist/billing/billing-overview.js +2 -0
- package/dist/billing/billing-overview.js.map +1 -0
- package/dist/billing/billing-standing.d.ts +10 -10
- package/dist/billing/checkout-processing.d.ts +217 -0
- package/dist/billing/checkout-processing.js +198 -0
- package/dist/billing/checkout-processing.js.map +1 -0
- package/dist/billing/checkout-return.d.ts +51 -0
- package/dist/billing/checkout-return.js +43 -0
- package/dist/billing/checkout-return.js.map +1 -0
- package/dist/billing/pay-page.d.ts +210 -0
- package/dist/billing/pay-page.js +113 -0
- package/dist/billing/pay-page.js.map +1 -0
- package/dist/billing/plan-card.d.ts +10 -10
- package/dist/billing/plan-limit-notice.d.ts +10 -10
- package/dist/billing/plan-limit-toast.d.ts +143 -0
- package/dist/billing/plan-limit-toast.js +40 -0
- package/dist/billing/plan-limit-toast.js.map +1 -0
- package/dist/billing/subscription-actions.d.ts +38 -15
- package/dist/billing/subscription-actions.js +9 -2
- package/dist/billing/subscription-actions.js.map +1 -1
- package/dist/billing/subscription-status-chip.d.ts +9 -9
- package/dist/bin/eifi1-pay-page.mjs +166 -0
- package/dist/chart.d.ts +10 -10
- package/dist/components/account-chips.d.ts +6 -1
- package/dist/components/account-settings.d.ts +4 -1
- package/dist/components/account-settings.js +2 -1
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +10 -10
- package/dist/components/autocomplete.d.ts +2 -1
- package/dist/components/autocomplete.js +3 -1
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +10 -10
- package/dist/components/button-group.js +1 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +10 -10
- package/dist/components/calculator.js +3 -3
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/checkbox-group.d.ts +2 -1
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/checkbox.d.ts +2 -1
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +19 -17
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +3 -1
- package/dist/components/choice-card.js +1 -1
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox.d.ts +4 -3
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +10 -10
- package/dist/components/confirm-dialog.js +6 -3
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +11 -11
- package/dist/components/country-select.d.ts +2 -1
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +10 -10
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +12 -1
- package/dist/components/data-table-filter-popover.d.ts +12 -1
- package/dist/components/data-table-filters.d.ts +12 -1
- package/dist/components/data-table-labels.d.ts +12 -1
- package/dist/components/data-table-pagination.d.ts +12 -1
- package/dist/components/data-table.d.ts +12 -1
- package/dist/components/data-table.js +3 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +6 -4
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +3 -2
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +10 -10
- package/dist/components/field-parts.d.ts +32 -4
- package/dist/components/field-parts.js +54 -19
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/file-button.d.ts +10 -10
- package/dist/components/file-button.js +3 -2
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +11 -11
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.js +5 -5
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +9 -9
- package/dist/components/form-actions.js +3 -3
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +10 -10
- package/dist/components/inline-edit-field.d.ts +2 -1
- package/dist/components/inline-edit-field.js +6 -6
- package/dist/components/inline-edit-field.js.map +1 -1
- package/dist/components/language-select.d.ts +10 -10
- package/dist/components/legal-page.d.ts +10 -10
- package/dist/components/legal.d.ts +10 -10
- package/dist/components/list.d.ts +2 -1
- package/dist/components/list.js +4 -5
- package/dist/components/list.js.map +1 -1
- package/dist/components/money-field.d.ts +10 -10
- package/dist/components/month-picker.js +54 -44
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +3 -2
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +10 -10
- package/dist/components/number-input.d.ts +10 -10
- package/dist/components/numpad-sheet.d.ts +10 -10
- package/dist/components/numpad-sheet.js +2 -2
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/phone-input.d.ts +10 -10
- package/dist/components/row-actions.d.ts +5 -5
- package/dist/components/row-actions.js +130 -32
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/series-chart.d.ts +10 -10
- package/dist/components/settings-fields.d.ts +10 -10
- package/dist/components/share-card.d.ts +7 -7
- package/dist/components/share-card.js +26 -10
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/status-dot.d.ts +1 -0
- package/dist/components/status-dot.js +2 -2
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +2 -1
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +88 -17
- package/dist/components/swipeable-row.js +46 -9
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/switch.d.ts +2 -1
- package/dist/components/switch.js.map +1 -1
- package/dist/components/table.d.ts +2 -1
- package/dist/components/table.js +6 -0
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +9 -9
- package/dist/components/time-input.d.ts +10 -10
- package/dist/components/toggle-group.d.ts +2 -1
- package/dist/components/toggle-group.js +1 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/translation-review-editor.d.ts +4 -0
- package/dist/components/translation-review-editor.js +4 -3
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review-parts.d.ts +1 -0
- package/dist/components/translation-review.d.ts +1 -0
- package/dist/components/translation-review.js +15 -18
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/ui.d.ts +9 -9
- package/dist/components/ui.js +47 -27
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +12 -1
- package/dist/components/user-avatar.d.ts +1 -0
- package/dist/components/write-lock.d.ts +101 -13
- package/dist/components/write-lock.js +37 -13
- package/dist/components/write-lock.js.map +1 -1
- package/dist/{data-table-labels-kpAAVi6g.d.ts → data-table-labels-BJ5JE_X2.d.ts} +17 -0
- package/dist/data-table.d.ts +12 -1
- package/dist/{delete-account-setting-Dy4kJgUm.d.ts → delete-account-setting-DiPX4Vue.d.ts} +355 -41
- package/dist/feedback/feedback-attachment.d.ts +10 -10
- package/dist/feedback/feedback-attachment.js +7 -7
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-context.d.ts +10 -10
- package/dist/feedback/feedback-crash.d.ts +10 -10
- package/dist/feedback/feedback-dialog.d.ts +10 -10
- package/dist/feedback/feedback-inbox.d.ts +9 -9
- package/dist/feedback/feedback-inbox.js +7 -3
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-labels.d.ts +10 -10
- package/dist/feedback/feedback-menu.d.ts +3 -3
- package/dist/feedback/feedback-record.d.ts +10 -10
- package/dist/feedback/feedback-row-detail.d.ts +10 -10
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +10 -10
- package/dist/feedback/feedback-submit.d.ts +10 -10
- package/dist/feedback/feedback-swipe.d.ts +10 -10
- package/dist/feedback/feedback-swipe.js +3 -6
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +10 -10
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +10 -10
- package/dist/feedback.d.ts +10 -10
- package/dist/hooks/use-file-drop.d.ts +10 -10
- package/dist/i18n/defaults.d.ts +10 -10
- package/dist/i18n/defaults.js +2 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +10 -10
- package/dist/i18n/german.js +34 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +4 -4
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +10 -10
- package/dist/i18n/locales/de-CH.d.ts +10 -10
- package/dist/i18n/locales/en.d.ts +10 -10
- package/dist/i18n/locales/es.d.ts +10 -10
- package/dist/i18n/locales/es.js +34 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +10 -10
- package/dist/i18n/locales/fr.js +35 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +10 -10
- package/dist/i18n/locales/hu.js +36 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +10 -10
- package/dist/i18n/locales/it.js +34 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +10 -10
- package/dist/i18n/locales/zh.js +34 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +10 -10
- package/dist/i18n/review.js +8 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.js +40 -2
- package/dist/index.js.map +1 -1
- package/dist/landing/landing-actions.d.ts +10 -10
- package/dist/landing/landing-sections.d.ts +10 -10
- package/dist/landing/public-header.d.ts +10 -10
- package/dist/landing/routing.d.ts +10 -10
- package/dist/pay/.well-known/apple-developer-merchantid-domain-association +1 -0
- package/dist/pay/index.html +30 -0
- package/dist/pay/pay.css +126 -0
- package/dist/pay/pay.js +198 -0
- package/dist/rhf/fields.d.ts +10 -10
- package/dist/rhf/form.d.ts +10 -10
- package/dist/rhf.d.ts +10 -10
- package/dist/settings/settings-section.d.ts +10 -10
- package/dist/shell/app-shell.d.ts +9 -9
- package/dist/shell/top-bar-brand.d.ts +10 -10
- package/dist/shell/topbar-action-menu.d.ts +1 -0
- package/dist/shell.d.ts +10 -10
- package/dist/testing/css-variables.d.ts +74 -0
- package/dist/testing/css-variables.generated.d.ts +7 -0
- package/dist/testing/css-variables.generated.js +8 -0
- package/dist/testing/css-variables.generated.js.map +1 -0
- package/dist/testing/css-variables.js +93 -0
- package/dist/testing/css-variables.js.map +1 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +12 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/theme/contrast-tokens.d.ts +44 -1
- package/dist/theme/contrast-tokens.js +21 -0
- package/dist/theme/contrast-tokens.js.map +1 -1
- package/dist/theme/contrast.d.ts +2 -2
- package/dist/theme/palette-derive.js +60 -11
- package/dist/theme/palette-derive.js.map +1 -1
- package/dist/theme/palette-presets.js +45 -43
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +11 -11
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard.d.ts +10 -10
- package/package.json +10 -2
- package/src/account/delete-account-setting.tsx +6 -1
- package/src/account/email-change-setting.tsx +9 -3
- package/src/account/sessions-setting.tsx +7 -2
- package/src/admin/admin-action-confirm.tsx +2 -1
- package/src/admin/invitations-panel.tsx +9 -3
- package/src/admin/plan-change.tsx +459 -0
- package/src/admin/reviewer-scope-editor.tsx +2 -1
- package/src/admin/role-select.tsx +2 -1
- package/src/admin/user-roster.tsx +5 -3
- package/src/auth/auth-errors.ts +25 -2
- package/src/auth/complete-name-dialog.tsx +6 -2
- package/src/billing/billing-banner.tsx +3 -1
- package/src/billing/billing-labels.ts +45 -0
- package/src/billing/billing-lock.ts +36 -3
- package/src/billing/billing-overview.ts +58 -0
- package/src/billing/checkout-processing.ts +391 -0
- package/src/billing/checkout-return.ts +90 -0
- package/src/billing/pay-page.ts +242 -0
- package/src/billing/plan-limit-toast.tsx +91 -0
- package/src/billing/subscription-actions.tsx +41 -8
- package/src/components/account-settings.tsx +6 -2
- package/src/components/autocomplete.tsx +5 -2
- package/src/components/button-group.tsx +4 -1
- package/src/components/calculator.tsx +10 -6
- package/src/components/checkbox-group.tsx +2 -2
- package/src/components/checkbox.tsx +2 -1
- package/src/components/chip.tsx +37 -32
- package/src/components/choice-card.tsx +4 -2
- package/src/components/combobox.tsx +4 -4
- package/src/components/confirm-dialog.tsx +13 -6
- package/src/components/country-select.tsx +2 -2
- package/src/components/danger-confirm.tsx +2 -2
- package/src/components/data-table.tsx +20 -9
- package/src/components/date-picker.tsx +10 -7
- package/src/components/entity-combobox.tsx +3 -3
- package/src/components/field-parts.tsx +61 -14
- package/src/components/file-button.tsx +5 -4
- package/src/components/file-dropzone.tsx +2 -2
- package/src/components/floating-panel.tsx +7 -5
- package/src/components/form-actions.tsx +12 -5
- package/src/components/inline-edit-field.tsx +13 -13
- package/src/components/list.tsx +10 -9
- package/src/components/month-picker.tsx +8 -2
- package/src/components/multi-entity-combobox.tsx +3 -3
- package/src/components/numpad-sheet.tsx +8 -4
- package/src/components/row-actions.tsx +208 -58
- package/src/components/share-card.tsx +67 -13
- package/src/components/status-dot.tsx +7 -7
- package/src/components/swatch-picker.tsx +2 -2
- package/src/components/swipeable-row.tsx +138 -27
- package/src/components/switch.tsx +2 -1
- package/src/components/table.tsx +8 -1
- package/src/components/toggle-group.tsx +5 -3
- package/src/components/translation-review-editor.tsx +8 -3
- package/src/components/translation-review.tsx +18 -22
- package/src/components/ui.tsx +92 -43
- package/src/components/write-lock.tsx +150 -24
- package/src/feedback/feedback-attachment.tsx +13 -9
- package/src/feedback/feedback-inbox.tsx +10 -6
- package/src/feedback/feedback-row-detail.tsx +3 -2
- package/src/feedback/feedback-swipe.tsx +6 -11
- package/src/feedback/feedback-table.tsx +2 -1
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/german.ts +38 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/es.ts +34 -0
- package/src/i18n/locales/fr.ts +39 -0
- package/src/i18n/locales/hu.ts +37 -0
- package/src/i18n/locales/it.ts +35 -0
- package/src/i18n/locales/zh.ts +33 -0
- package/src/i18n/review.ts +7 -0
- package/src/index.ts +48 -4
- package/src/testing/css-variables.generated.ts +112 -0
- package/src/testing/css-variables.ts +191 -0
- package/src/testing/index.ts +13 -0
- package/src/theme/contrast-tokens.ts +53 -2
- package/src/theme/palette-derive.ts +87 -10
- package/src/theme/palette-presets.ts +45 -43
- package/src/wizard/stepper-nav.tsx +2 -1
- package/tokens.css +306 -46
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The way back from the provider's checkout (docs/billing-harmonization.md §14.4,
|
|
3
|
+
* decision 20): `?checkout=done` on the app's subscription page — kastlan's
|
|
4
|
+
* `/admin/billing`, keksdose's and Kurvenschmiede's `/settings/subscription`.
|
|
5
|
+
*
|
|
6
|
+
* The checkout request carries NO success URL: a redirect the client chooses is an open
|
|
7
|
+
* redirect, and Paddle's create-transaction takes none. The way back is configuration
|
|
8
|
+
* instead — the pay page's Paddle.js `successUrl` (`checkoutReturnUrl(returnUrl)`), or a
|
|
9
|
+
* hosted checkout's redirect in Paddle's dashboard — and the page that lands reads the
|
|
10
|
+
* marker with {@link isCheckoutReturn}, then drops it with `replace`
|
|
11
|
+
* ({@link withoutCheckoutReturn}) so a reload or a shared link does not count twice.
|
|
12
|
+
*
|
|
13
|
+
* Pure and router-agnostic, and imports nothing: the pay page (`dist/pay/`, no React)
|
|
14
|
+
* bundles this module too. server-kit names the same two values
|
|
15
|
+
* (`CHECKOUT_RETURN_PARAM`, `CHECKOUT_RETURN_VALUE`, `checkout_return_url`) for the
|
|
16
|
+
* deploy notes and its tests.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** The query parameter of the way back. */
|
|
20
|
+
export const CHECKOUT_RETURN_PARAM = "checkout";
|
|
21
|
+
/** Its one value: the buyer came back from a completed checkout. */
|
|
22
|
+
export const CHECKOUT_RETURN_VALUE = "done";
|
|
23
|
+
|
|
24
|
+
/** The query of `where`, however the app holds it: a search string ("?a=1"), a whole
|
|
25
|
+
* URL, `URLSearchParams`, or a location (`{ search }`). */
|
|
26
|
+
function paramsOf(where: string | URLSearchParams | { search: string }): URLSearchParams {
|
|
27
|
+
if (where instanceof URLSearchParams) return where;
|
|
28
|
+
const text = typeof where === "string" ? where : where.search;
|
|
29
|
+
const query = text.includes("?") ? text.slice(text.indexOf("?") + 1) : text;
|
|
30
|
+
// A whole URL's hash is not part of its query.
|
|
31
|
+
return new URLSearchParams(query.split("#")[0]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether the page was reached as the way back from a checkout: `?checkout=done`.
|
|
36
|
+
*
|
|
37
|
+
* const returned = isCheckoutReturn(location.search); // react-router's useLocation()
|
|
38
|
+
* const returned = isCheckoutReturn(searchParams); // useSearchParams()[0]
|
|
39
|
+
*/
|
|
40
|
+
export function isCheckoutReturn(where: string | URLSearchParams | { search: string }): boolean {
|
|
41
|
+
return paramsOf(where).get(CHECKOUT_RETURN_PARAM) === CHECKOUT_RETURN_VALUE;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The same parameters without the marker — every other one kept, in order. A new
|
|
46
|
+
* object; the one passed in is not changed:
|
|
47
|
+
*
|
|
48
|
+
* setSearchParams((p) => withoutCheckoutReturn(p), { replace: true });
|
|
49
|
+
*/
|
|
50
|
+
export function withoutCheckoutReturn(params: URLSearchParams): URLSearchParams {
|
|
51
|
+
const next = new URLSearchParams(params);
|
|
52
|
+
next.delete(CHECKOUT_RETURN_PARAM);
|
|
53
|
+
return next;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The subscription page's way-back URL: `page` with `checkout=done` in its query —
|
|
58
|
+
* after any query it already has, before its hash. A marker already there is replaced,
|
|
59
|
+
* not doubled.
|
|
60
|
+
*
|
|
61
|
+
* checkoutReturnUrl("https://keksdose.app/settings/subscription")
|
|
62
|
+
* // → "https://keksdose.app/settings/subscription?checkout=done"
|
|
63
|
+
*
|
|
64
|
+
* What the pay page hands Paddle.js as `successUrl`, and what a hosted checkout's
|
|
65
|
+
* redirect is set to in Paddle's dashboard.
|
|
66
|
+
*/
|
|
67
|
+
export function checkoutReturnUrl(page: string): string {
|
|
68
|
+
const hashAt = page.indexOf("#");
|
|
69
|
+
const hash = hashAt === -1 ? "" : page.slice(hashAt);
|
|
70
|
+
const beforeHash = hashAt === -1 ? page : page.slice(0, hashAt);
|
|
71
|
+
const queryAt = beforeHash.indexOf("?");
|
|
72
|
+
const path = queryAt === -1 ? beforeHash : beforeHash.slice(0, queryAt);
|
|
73
|
+
// Kept as written, part by part: a URLSearchParams round trip would re-encode the
|
|
74
|
+
// page's own parameters (a space as "+").
|
|
75
|
+
const kept = (queryAt === -1 ? "" : beforeHash.slice(queryAt + 1))
|
|
76
|
+
.split("&")
|
|
77
|
+
.filter((part) => part !== "" && decodeKey(part) !== CHECKOUT_RETURN_PARAM);
|
|
78
|
+
kept.push(`${CHECKOUT_RETURN_PARAM}=${CHECKOUT_RETURN_VALUE}`);
|
|
79
|
+
return `${path}?${kept.join("&")}${hash}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The decoded key of one `key=value` part of a query. */
|
|
83
|
+
function decodeKey(part: string): string {
|
|
84
|
+
const key = part.split("=")[0].replace(/\+/g, " ");
|
|
85
|
+
try {
|
|
86
|
+
return decodeURIComponent(key);
|
|
87
|
+
} catch {
|
|
88
|
+
return key;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type { UiKitLabels } from "../i18n/kit-labels";
|
|
2
|
+
import { checkoutReturnUrl } from "./checkout-return";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The kit's pay page (docs/billing-harmonization.md §14.4, decision 19 as amended): a
|
|
6
|
+
* standalone static page that ui-kit ships as `dist/pay/`, and that each app serves on
|
|
7
|
+
* its own pay host, `pay.<app domain>` — a separate origin, approved in Paddle, where
|
|
8
|
+
* Paddle.js shares the origin with nothing: no sign-in, no app code, no storage.
|
|
9
|
+
*
|
|
10
|
+
* Paddle's create-transaction answers `checkout.url`: the pay page's URL plus
|
|
11
|
+
* `?_ptxn=<txn>`, and Paddle.js on that page opens the transaction by itself. The app
|
|
12
|
+
* sends the buyer there with {@link payPageUrl}, which adds the buyer's language; the
|
|
13
|
+
* page maps it to Paddle's checkout locale ({@link paddleLocale}). Everything else the
|
|
14
|
+
* page needs — the client-side token, sandbox or live, the way back — is the app's
|
|
15
|
+
* `pay-config.json`, written at its web build by the `eifi1-pay-page` script.
|
|
16
|
+
*
|
|
17
|
+
* Pure, and React-free: the page's own script (`pay-page/pay.ts`) and that build script
|
|
18
|
+
* bundle this module. The exports below the barrel's four are theirs.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Paddle's two environments: the sandbox's client-side tokens start `test_`, live
|
|
22
|
+
* ones `live_`. */
|
|
23
|
+
export type PaddleEnvironment = "sandbox" | "live";
|
|
24
|
+
|
|
25
|
+
/** The pay page's language parameter: `?lang=<kit locale>`. */
|
|
26
|
+
export const PAY_PAGE_LANG_PARAM = "lang";
|
|
27
|
+
|
|
28
|
+
/** The kit's seven languages (`KitLanguageCode`), as the page reads them. */
|
|
29
|
+
const PAY_PAGE_LANGUAGES = ["de-CH", "en", "es", "fr", "it", "hu", "zh"] as const;
|
|
30
|
+
|
|
31
|
+
/** One of the kit's languages, as the page speaks it. */
|
|
32
|
+
export type PayPageLanguage = (typeof PAY_PAGE_LANGUAGES)[number];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The kit's language → Paddle's checkout locale (`Paddle.Checkout.open`,
|
|
36
|
+
* `settings.locale`, read 2026-10-09: ar, zh-Hans, zh-TW, da, nl, en, fr, de, it, ja, ko,
|
|
37
|
+
* no, pl, pt, pt-BR, tr, ru, es, sv). Paddle has no Hungarian: a Hungarian buyer gets
|
|
38
|
+
* English always, not Paddle's guess from the browser. The page's own lines stay in the
|
|
39
|
+
* kit's language, Hungarian included.
|
|
40
|
+
*/
|
|
41
|
+
const PADDLE_LOCALES: Readonly<Record<PayPageLanguage, string>> = {
|
|
42
|
+
en: "en",
|
|
43
|
+
"de-CH": "de",
|
|
44
|
+
fr: "fr",
|
|
45
|
+
it: "it",
|
|
46
|
+
es: "es",
|
|
47
|
+
zh: "zh-Hans",
|
|
48
|
+
hu: "en",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Paddle's transaction parameter on a pay page link. @internal */
|
|
52
|
+
export const PAY_PAGE_TRANSACTION_PARAM = "_ptxn";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The kit language one tag names, or undefined — `resolveLanguage`'s rule
|
|
56
|
+
* (src/i18n/languages.ts), restated here because that module loads the seven catalogues
|
|
57
|
+
* and the page must stay a few kilobytes: exact first, ignoring case and reading `_` as
|
|
58
|
+
* `-`; then by language, so every German is `de-CH`, every Chinese `zh` (Simplified),
|
|
59
|
+
* and any regional form its base (`fr-CA` → `fr`).
|
|
60
|
+
*/
|
|
61
|
+
function kitLanguageOf(tag: unknown): PayPageLanguage | undefined {
|
|
62
|
+
if (typeof tag !== "string") return undefined;
|
|
63
|
+
const clean = tag.trim().replace(/_/g, "-");
|
|
64
|
+
if (!clean) return undefined;
|
|
65
|
+
const lower = clean.toLowerCase();
|
|
66
|
+
const exact = PAY_PAGE_LANGUAGES.find((code) => code.toLowerCase() === lower);
|
|
67
|
+
if (exact) return exact;
|
|
68
|
+
const language = lower.split("-")[0];
|
|
69
|
+
return PAY_PAGE_LANGUAGES.find((code) => code.split("-")[0].toLowerCase() === language);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The kit's locale → Paddle's checkout locale: `de-CH` → `de`, `zh` → `zh-Hans`, `hu` →
|
|
74
|
+
* `en` (Paddle has no Hungarian), the others as they are. Any tag is read as the kit
|
|
75
|
+
* reads it (`de-AT` is the one German, `zh-TW` the one Chinese); one the kit doesn't
|
|
76
|
+
* ship is English.
|
|
77
|
+
*/
|
|
78
|
+
export function paddleLocale(kitLocale: string): string {
|
|
79
|
+
return PADDLE_LOCALES[kitLanguageOf(kitLocale) ?? "en"];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The checkout's `{url}` with the buyer's language: `lang=<kit locale>` on a pay page
|
|
84
|
+
* link (one that carries `_ptxn`); any other URL — a hosted checkout's, which takes its
|
|
85
|
+
* own `locale` — unchanged, as is a locale the kit doesn't ship.
|
|
86
|
+
*
|
|
87
|
+
* const { url } = await api.checkout({ plan, interval, currency });
|
|
88
|
+
* noteCheckoutStarted(user.id, overview);
|
|
89
|
+
* window.location.assign(payPageUrl(url, i18n.language));
|
|
90
|
+
*
|
|
91
|
+
* A wrong `lang` costs nothing but the language: the page falls back to the browser's.
|
|
92
|
+
*/
|
|
93
|
+
export function payPageUrl(checkoutUrl: string, locale: string): string {
|
|
94
|
+
const language = kitLanguageOf(locale);
|
|
95
|
+
if (!language) return checkoutUrl;
|
|
96
|
+
let url: URL;
|
|
97
|
+
try {
|
|
98
|
+
url = new URL(checkoutUrl);
|
|
99
|
+
} catch {
|
|
100
|
+
return checkoutUrl;
|
|
101
|
+
}
|
|
102
|
+
if (!url.searchParams.has(PAY_PAGE_TRANSACTION_PARAM)) return checkoutUrl;
|
|
103
|
+
url.searchParams.set(PAY_PAGE_LANG_PARAM, language);
|
|
104
|
+
return url.toString();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The language the page speaks: its `lang` parameter, else the browser's languages in
|
|
109
|
+
* their order, each matched to the kit's; else English. @internal
|
|
110
|
+
*/
|
|
111
|
+
export function payPageLanguage(
|
|
112
|
+
lang: string | null | undefined,
|
|
113
|
+
browserLanguages: readonly string[] = [],
|
|
114
|
+
): PayPageLanguage {
|
|
115
|
+
for (const candidate of [lang, ...browserLanguages]) {
|
|
116
|
+
const found = kitLanguageOf(candidate);
|
|
117
|
+
if (found) return found;
|
|
118
|
+
}
|
|
119
|
+
return "en";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* ── The page's words ──────────────────────────────────────────────────────── */
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The page's words in one language — built into `pay.js` at the kit's build from the
|
|
126
|
+
* seven catalogues, since the page has no React and no provider. @internal
|
|
127
|
+
*/
|
|
128
|
+
export interface PayPageStrings {
|
|
129
|
+
/** `billing.payOpening`, while the checkout opens. */
|
|
130
|
+
opening: string;
|
|
131
|
+
/** `billing.payNothing`: no `_ptxn`, or no token (billing off). */
|
|
132
|
+
nothing: string;
|
|
133
|
+
/** `billing.payFailed`: Paddle.js didn't load, or the configuration is wrong. */
|
|
134
|
+
failed: string;
|
|
135
|
+
/** `billing.payBack`: the buyer closed the checkout. */
|
|
136
|
+
closed: string;
|
|
137
|
+
/** `common.back`: the link to the app's subscription page. */
|
|
138
|
+
back: string;
|
|
139
|
+
/** `legal.links.terms` and `legal.links.privacy`: the footer's links. */
|
|
140
|
+
terms: string;
|
|
141
|
+
privacy: string;
|
|
142
|
+
/** `legal.navLabel`: the footer's `<nav>`. */
|
|
143
|
+
legal: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The page's words, read from one of the kit's catalogues. @internal */
|
|
147
|
+
export function payPageStrings(labels: UiKitLabels): PayPageStrings {
|
|
148
|
+
return {
|
|
149
|
+
opening: labels.billing.payOpening,
|
|
150
|
+
nothing: labels.billing.payNothing,
|
|
151
|
+
failed: labels.billing.payFailed,
|
|
152
|
+
closed: labels.billing.payBack,
|
|
153
|
+
back: labels.common.back,
|
|
154
|
+
terms: labels.legal.links.terms,
|
|
155
|
+
privacy: labels.legal.links.privacy,
|
|
156
|
+
legal: labels.legal.navLabel,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* ── pay-config.json ───────────────────────────────────────────────────────── */
|
|
161
|
+
|
|
162
|
+
/** The configuration file beside the page, written at each app's web build. @internal */
|
|
163
|
+
export const PAY_CONFIG_FILE = "pay-config.json";
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* `pay-config.json`: what the page needs that differs per app and deployment. Read by
|
|
167
|
+
* the page from its own origin — never from the query, where Paddle's own links (it
|
|
168
|
+
* puts only `_ptxn` there) would lack it and a return URL would be an open redirect on
|
|
169
|
+
* an approved payment domain. @internal
|
|
170
|
+
*/
|
|
171
|
+
export interface PayPageConfig {
|
|
172
|
+
/** Paddle's client-side token (`test_…` for the sandbox, `live_…` for live); null
|
|
173
|
+
* while billing is off, and the page says `payNothing`. */
|
|
174
|
+
token: string | null;
|
|
175
|
+
environment: PaddleEnvironment | null;
|
|
176
|
+
/** The app's subscription page. Paddle.js's `successUrl` is
|
|
177
|
+
* `checkoutReturnUrl(returnUrl)`; "Back" goes here. */
|
|
178
|
+
returnUrl: string;
|
|
179
|
+
appName: string;
|
|
180
|
+
termsUrl: string;
|
|
181
|
+
privacyUrl: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Paddle's client-side token prefixes, per environment. */
|
|
185
|
+
const TOKEN_PREFIX: Readonly<Record<PaddleEnvironment, string>> = { sandbox: "test_", live: "live_" };
|
|
186
|
+
|
|
187
|
+
/** `https://`, or `http://` on this machine only (`localhost`, `127.0.0.1`, `[::1]`). */
|
|
188
|
+
function isPageUrl(value: unknown): boolean {
|
|
189
|
+
if (typeof value !== "string") return false;
|
|
190
|
+
let url: URL;
|
|
191
|
+
try {
|
|
192
|
+
url = new URL(value);
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
if (url.protocol === "https:") return true;
|
|
197
|
+
return url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* What is wrong with a `pay-config.json`, one line each — `[]` when nothing is. The
|
|
202
|
+
* build script refuses to write a file with any (so a wrong pairing fails the build,
|
|
203
|
+
* not a buyer), and the page says `payFailed` for one it reads:
|
|
204
|
+
*
|
|
205
|
+
* - a token whose prefix disagrees with the environment (`test_` ↔ sandbox, `live_` ↔
|
|
206
|
+
* live), or a token without an environment;
|
|
207
|
+
* - a return, terms or privacy URL that isn't `https://` (`http://` for localhost only);
|
|
208
|
+
* - no app name. @internal
|
|
209
|
+
*/
|
|
210
|
+
export function payPageConfigProblems(config: unknown): string[] {
|
|
211
|
+
if (typeof config !== "object" || config === null) return ["the configuration is not an object"];
|
|
212
|
+
const c = config as Record<string, unknown>;
|
|
213
|
+
const problems: string[] = [];
|
|
214
|
+
const { token, environment } = c;
|
|
215
|
+
if (environment !== null && environment !== undefined && environment !== "sandbox" && environment !== "live") {
|
|
216
|
+
problems.push(`environment must be "sandbox" or "live", not ${JSON.stringify(environment)}`);
|
|
217
|
+
}
|
|
218
|
+
if (token !== null && token !== undefined) {
|
|
219
|
+
if (typeof token !== "string" || token === "") {
|
|
220
|
+
problems.push("token must be Paddle's client-side token, or null while billing is off");
|
|
221
|
+
} else if (environment !== "sandbox" && environment !== "live") {
|
|
222
|
+
problems.push("a token needs its environment: sandbox or live");
|
|
223
|
+
} else if (!token.startsWith(TOKEN_PREFIX[environment]) || token.length === TOKEN_PREFIX[environment].length) {
|
|
224
|
+
const other = environment === "sandbox" ? "live" : "sandbox";
|
|
225
|
+
problems.push(
|
|
226
|
+
token.startsWith(TOKEN_PREFIX[other])
|
|
227
|
+
? `the token is a ${other} token (${TOKEN_PREFIX[other]}…), but the environment is ${environment}`
|
|
228
|
+
: `a ${environment} token starts with ${TOKEN_PREFIX[environment]}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for (const key of ["returnUrl", "termsUrl", "privacyUrl"] as const) {
|
|
233
|
+
if (!isPageUrl(c[key])) problems.push(`${key} must be an https:// URL (http:// for localhost only)`);
|
|
234
|
+
}
|
|
235
|
+
if (typeof c.appName !== "string" || c.appName.trim() === "") problems.push("appName is missing");
|
|
236
|
+
return problems;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Paddle.js's `successUrl` for a configuration: the way back, `?checkout=done`. @internal */
|
|
240
|
+
export function paySuccessUrl(config: Pick<PayPageConfig, "returnUrl">): string {
|
|
241
|
+
return checkoutReturnUrl(config.returnUrl);
|
|
242
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
import { useKitLocale } from "../i18n/kit-labels";
|
|
4
|
+
import type { LabelOverride } from "../i18n/kit-labels";
|
|
5
|
+
import { documentNavigation } from "../lib/document-navigation";
|
|
6
|
+
import { formatNumber } from "../lib/format";
|
|
7
|
+
import { toast } from "../components/toast";
|
|
8
|
+
import type { ToastAction, ToastId } from "../components/toast";
|
|
9
|
+
import { useBillingLabels } from "./billing-labels";
|
|
10
|
+
import type { BillingLabels } from "./billing-labels";
|
|
11
|
+
import type { PlanLimitRefusal } from "./plan-limit";
|
|
12
|
+
import { planLimitMailto } from "./plan-limit-notice";
|
|
13
|
+
|
|
14
|
+
export interface PlanLimitToastOptions {
|
|
15
|
+
/** `upgrade`: plans are bought — "Choose a plan". `contact`: the operator grants them
|
|
16
|
+
* by hand — "Ask for more", a mail to support. */
|
|
17
|
+
mode: "upgrade" | "contact";
|
|
18
|
+
/** `upgrade`: the app's navigation to its subscription page (the kit has no router).
|
|
19
|
+
* Left out, the toast has no action. */
|
|
20
|
+
onChoosePlan?: () => void;
|
|
21
|
+
/** `contact`: support@ the app's domain, opened as `planLimitMailto(email,
|
|
22
|
+
* contactSubject(name))`. Left out, the toast has no action. */
|
|
23
|
+
email?: string;
|
|
24
|
+
/** The app's word per dimension: `{ budgets: "Budgets", scans: "Scans this month" }`.
|
|
25
|
+
* A dimension without one is named by its key. */
|
|
26
|
+
dimensionLabels?: Readonly<Record<string, string>>;
|
|
27
|
+
/** How a figure is written — kastlan's storage limit is bytes. Default: a number in
|
|
28
|
+
* the kit's locale. */
|
|
29
|
+
formatValue?: (dimension: string, value: number) => string;
|
|
30
|
+
/** Prop > `<UiKitProvider labels={{ billing }}>` > English. */
|
|
31
|
+
labels?: LabelOverride<BillingLabels>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `PlanLimitNotice`'s words as a toast (docs/billing-harmonization.md §14.11), for a
|
|
36
|
+
* refused create with no room for the notice — a row's button, a share target: "Plan
|
|
37
|
+
* limit reached", what to do, the figure when the refusal has one ("Budgets: 3 of 3"),
|
|
38
|
+
* and the mode's action.
|
|
39
|
+
*
|
|
40
|
+
* const showLimit = usePlanLimitToast({
|
|
41
|
+
* mode: "upgrade",
|
|
42
|
+
* onChoosePlan: () => navigate("/settings/subscription"),
|
|
43
|
+
* dimensionLabels: { budgets: t("billing.dim_budgets") },
|
|
44
|
+
* });
|
|
45
|
+
* catch (err) { const hit = isPlanLimit(err); if (hit) showLimit(hit); }
|
|
46
|
+
*
|
|
47
|
+
* One toast per dimension: its id is `plan-limit:<dimension>`, so a repeat replaces it
|
|
48
|
+
* in place instead of stacking a second (keksdose's own hook stacked). Called with no
|
|
49
|
+
* hit, it says the words without a figure, under `plan-limit`. No `limit` in the
|
|
50
|
+
* refusal, no figure line; keksdose's legacy `plan_budget_limit` reads as `budgets`
|
|
51
|
+
* (`isPlanLimit`, §12.16). Returns the toast's id.
|
|
52
|
+
*/
|
|
53
|
+
export function usePlanLimitToast(options: PlanLimitToastOptions): (hit?: PlanLimitRefusal) => ToastId {
|
|
54
|
+
const { mode, onChoosePlan, email, dimensionLabels, formatValue, labels: labelsProp } = options;
|
|
55
|
+
const labels = useBillingLabels(labelsProp);
|
|
56
|
+
const locale = useKitLocale();
|
|
57
|
+
return useCallback(
|
|
58
|
+
(hit?: PlanLimitRefusal) => {
|
|
59
|
+
const dimension = hit?.dimension ?? "";
|
|
60
|
+
const name = dimensionLabels?.[dimension] ?? dimension;
|
|
61
|
+
const figure = (value: number) =>
|
|
62
|
+
formatValue ? formatValue(dimension, value) : formatNumber(value, { locale });
|
|
63
|
+
// A body without `used` was refused for being AT the limit (PlanLimitNotice's rule).
|
|
64
|
+
const usage =
|
|
65
|
+
hit?.limit !== undefined
|
|
66
|
+
? labels.limitUsageLine(name, figure(hit.used ?? hit.limit), figure(hit.limit))
|
|
67
|
+
: undefined;
|
|
68
|
+
const action: ToastAction | undefined =
|
|
69
|
+
mode === "upgrade"
|
|
70
|
+
? onChoosePlan && { label: labels.choosePlan, onClick: () => onChoosePlan() }
|
|
71
|
+
: email
|
|
72
|
+
? {
|
|
73
|
+
label: labels.askForMore,
|
|
74
|
+
// A mailto: hands the address to the reader's mail client; the page stays.
|
|
75
|
+
onClick: () => documentNavigation.replace(planLimitMailto(email, labels.contactSubject(name))),
|
|
76
|
+
}
|
|
77
|
+
: undefined;
|
|
78
|
+
return toast.warning(labels.limitReached, {
|
|
79
|
+
id: dimension ? `plan-limit:${dimension}` : "plan-limit",
|
|
80
|
+
description: (
|
|
81
|
+
<span data-plan-limit-mode={mode} data-dimension={dimension || undefined}>
|
|
82
|
+
<span className="block">{mode === "upgrade" ? labels.limitUpgrade : labels.limitContact}</span>
|
|
83
|
+
{usage !== undefined && <span className="block tabular-nums">{usage}</span>}
|
|
84
|
+
</span>
|
|
85
|
+
),
|
|
86
|
+
action,
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
[mode, onChoosePlan, email, dimensionLabels, formatValue, labels, locale],
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -5,18 +5,36 @@ import { cn } from "../lib/cn";
|
|
|
5
5
|
import { Button } from "../components/ui";
|
|
6
6
|
import { useBillingLabels } from "./billing-labels";
|
|
7
7
|
import type { BillingLabels } from "./billing-labels";
|
|
8
|
+
import type { PortalTarget } from "./billing-overview";
|
|
8
9
|
|
|
9
10
|
export interface SubscriptionActionsProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
10
11
|
/** "Payment and invoices", as a link — or {@link onManage}. Without either, none. */
|
|
11
12
|
manageHref?: string;
|
|
12
|
-
/** "Payment and invoices", as the app's call: `POST /billing/portal
|
|
13
|
-
* `url` (§4). The kit sends no
|
|
13
|
+
/** "Payment and invoices", as the app's call: `POST /billing/portal` with
|
|
14
|
+
* `{target: "overview"}`, then off to its `url` (§4, §14.5). The kit sends no
|
|
15
|
+
* request. */
|
|
14
16
|
onManage?: () => void;
|
|
15
17
|
/** "Cancel subscription", as a link — or {@link onCancel}. Without either, none. */
|
|
16
18
|
cancelHref?: string;
|
|
17
|
-
/** "Cancel subscription", as the app's call
|
|
18
|
-
* provider's cancel and withdrawal functions
|
|
19
|
+
/** "Cancel subscription", as the app's call: the portal again, with `{target:
|
|
20
|
+
* "cancel"}` — its cancel link, where the provider's cancel and withdrawal functions
|
|
21
|
+
* are (§12.26, §14.5). Not the overview. */
|
|
19
22
|
onCancel?: () => void;
|
|
23
|
+
/**
|
|
24
|
+
* Both, as one call that says where to ({@link PortalTarget}, the body of
|
|
25
|
+
* `POST /billing/portal`): `"overview"` for "Payment and invoices",
|
|
26
|
+
* `"cancel"` for "Cancel subscription" — so the target can't be mixed up (kastlan's
|
|
27
|
+
* cancel opened the overview). {@link onManage} and {@link onCancel} win over it.
|
|
28
|
+
*/
|
|
29
|
+
onPortal?: (target: PortalTarget) => void;
|
|
30
|
+
/**
|
|
31
|
+
* The overview's `at_provider` (§14.5): whether the payer has reached the provider.
|
|
32
|
+
* `false`: no customer there yet, so nothing to open — a caption (`notAtProvider`)
|
|
33
|
+
* stands in place of both actions. `true`: both show, WHATEVER the status or source —
|
|
34
|
+
* a payer who bought under a grant keeps the way to the invoices and to "Cancel"
|
|
35
|
+
* (§12.26). Default true (0.32's behaviour).
|
|
36
|
+
*/
|
|
37
|
+
atProvider?: boolean;
|
|
20
38
|
/** Which of the two is under way: its spinner, no second press. */
|
|
21
39
|
pending?: "manage" | "cancel" | null;
|
|
22
40
|
/** Prop > `<UiKitProvider labels={{ billing }}>` > English. */
|
|
@@ -34,25 +52,40 @@ export interface SubscriptionActionsProps extends Omit<ComponentPropsWithoutRef<
|
|
|
34
52
|
*
|
|
35
53
|
* Neither takes the write lock: a read-only payer must be able to pay, leave or cancel
|
|
36
54
|
* (§3.3).
|
|
55
|
+
*
|
|
56
|
+
* Before the payer has reached the provider (`atProvider={false}`, a trial without a
|
|
57
|
+
* card, a beta) there is no portal to open: the part says so in a caption instead of
|
|
58
|
+
* offering two buttons that could only end in `409 billing_not_at_provider`.
|
|
37
59
|
*/
|
|
38
60
|
export function SubscriptionActions({
|
|
39
61
|
manageHref,
|
|
40
62
|
onManage,
|
|
41
63
|
cancelHref,
|
|
42
64
|
onCancel,
|
|
65
|
+
onPortal,
|
|
66
|
+
atProvider = true,
|
|
43
67
|
pending,
|
|
44
68
|
labels: labelsProp,
|
|
45
69
|
className,
|
|
46
70
|
...rest
|
|
47
71
|
}: SubscriptionActionsProps) {
|
|
48
72
|
const labels = useBillingLabels(labelsProp);
|
|
73
|
+
if (!atProvider) {
|
|
74
|
+
return (
|
|
75
|
+
<div {...rest} data-at-provider="false" className={cn("text-sm text-[var(--text-muted)]", className)}>
|
|
76
|
+
<p>{labels.notAtProvider}</p>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const manageCall = onManage ?? (onPortal ? () => onPortal("overview") : undefined);
|
|
81
|
+
const cancelCall = onCancel ?? (onPortal ? () => onPortal("cancel") : undefined);
|
|
49
82
|
const manage =
|
|
50
83
|
manageHref !== undefined ? (
|
|
51
84
|
<Button href={manageHref} variant="secondary">
|
|
52
85
|
{labels.manage}
|
|
53
86
|
</Button>
|
|
54
|
-
) :
|
|
55
|
-
<Button type="button" variant="secondary" onClick={
|
|
87
|
+
) : manageCall !== undefined ? (
|
|
88
|
+
<Button type="button" variant="secondary" onClick={manageCall} pending={pending === "manage"}>
|
|
56
89
|
{labels.manage}
|
|
57
90
|
</Button>
|
|
58
91
|
) : null;
|
|
@@ -61,8 +94,8 @@ export function SubscriptionActions({
|
|
|
61
94
|
<Button href={cancelHref} variant="link" tone="danger">
|
|
62
95
|
{labels.cancel}
|
|
63
96
|
</Button>
|
|
64
|
-
) :
|
|
65
|
-
<Button type="button" variant="link" tone="danger" onClick={
|
|
97
|
+
) : cancelCall !== undefined ? (
|
|
98
|
+
<Button type="button" variant="link" tone="danger" onClick={cancelCall} pending={pending === "cancel"}>
|
|
66
99
|
{labels.cancel}
|
|
67
100
|
</Button>
|
|
68
101
|
) : null;
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
ProfileSettingLabels,
|
|
23
23
|
TwoFactorSettingLabels,
|
|
24
24
|
} from "./account-settings-labels";
|
|
25
|
+
import { COMMIT_EXCEPT_BILLING } from "./write-lock";
|
|
25
26
|
|
|
26
27
|
// The label types moved beside the namespace's defaults (0.12.0); re-exported here so
|
|
27
28
|
// every existing `import type { ProfileSettingLabels }` keeps resolving.
|
|
@@ -115,7 +116,10 @@ export type ProfileSettingProps = ProfileSettingDisplayNameProps | ProfileSettin
|
|
|
115
116
|
*
|
|
116
117
|
* The two-field form owns its drafts, starting from the saved names and following them
|
|
117
118
|
* when they change; Save waits for a change, holds — saying why — while a name is
|
|
118
|
-
* blank, and is a `commit` (a {@link WriteLockProvider} locks it, keksdose's demo)
|
|
119
|
+
* blank, and is a `commit` (a {@link WriteLockProvider} locks it, keksdose's demo) —
|
|
120
|
+
* every lock but a lapsed plan's (`COMMIT_EXCEPT_BILLING`, 0.33): a profile is the
|
|
121
|
+
* account's own settings, which billing never locks (docs/billing-harmonization.md §3.3,
|
|
122
|
+
* §12.36).
|
|
119
123
|
*/
|
|
120
124
|
export function ProfileSetting(props: ProfileSettingProps) {
|
|
121
125
|
const { name, email, role, memberSince, labels: labelsProp, id } = props;
|
|
@@ -271,7 +275,7 @@ function PersonNameFields({
|
|
|
271
275
|
</div>
|
|
272
276
|
<Button
|
|
273
277
|
type="submit"
|
|
274
|
-
commit
|
|
278
|
+
commit={COMMIT_EXCEPT_BILLING}
|
|
275
279
|
pending={busy}
|
|
276
280
|
// A blank name is a reason the person can act on; an unchanged one is not —
|
|
277
281
|
// there is nothing to save, and the button is simply off.
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
useLockReason,
|
|
34
34
|
} from "./field-parts";
|
|
35
35
|
import { mergeDescribedBy } from "./choice-parts";
|
|
36
|
+
import type { CommitScope } from "./write-lock";
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* `value`/`onChange` are the TEXT's, and `onSelect` is "a suggestion was taken" —
|
|
@@ -161,7 +162,7 @@ export interface AutocompleteProps<V extends string | number = string>
|
|
|
161
162
|
* whose result is only draft state — the form's Save writes it — leaves this off and
|
|
162
163
|
* stays live under the lock. No provider, or an unlocked one: no effect.
|
|
163
164
|
*/
|
|
164
|
-
commit?:
|
|
165
|
+
commit?: CommitScope;
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
function AutocompleteInner<V extends string | number = string>(
|
|
@@ -329,8 +330,10 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
329
330
|
// label's first letters, which float at the same start inset. On the
|
|
330
331
|
// value line it is beside what it describes, and the label keeps the
|
|
331
332
|
// start edge every other label in the form's column shares.
|
|
333
|
+
// At Large the label stands above the field and the strip is gone
|
|
334
|
+
// (0.33, §10.17): the value's line is under FIELD_BASE's `py-2`.
|
|
332
335
|
hasLabel
|
|
333
|
-
? "top-[calc(1rem+1px)] h-5 items-center"
|
|
336
|
+
? "top-[calc(1rem+1px)] h-5 items-center large:top-[calc(0.5rem+1px)]"
|
|
334
337
|
: "top-1/2 -translate-y-1/2",
|
|
335
338
|
)}
|
|
336
339
|
>
|
|
@@ -178,7 +178,10 @@ export interface ButtonGroupLinkProps extends Omit<ComponentPropsWithoutRef<"a">
|
|
|
178
178
|
ref?: Ref<HTMLAnchorElement>;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
// The brand AS TEXT is `--brand-muted` (0.33, docs/colour-roles-harmonization.md §5.5):
|
|
182
|
+
// `--brand` itself is solved to 3:1 and read 2.97:1 on its own wash in the derived dark
|
|
183
|
+
// presets; the muted form holds 4.5:1 on it in every preset.
|
|
184
|
+
const LINK_CURRENT = "bg-[var(--brand-bg)] text-brand-muted hover:bg-[var(--brand-bg-hover)]";
|
|
182
185
|
|
|
183
186
|
/**
|
|
184
187
|
* A {@link ButtonGroup} member that navigates: a link drawn as a {@link Button}, so it
|
|
@@ -73,13 +73,17 @@ const KEYS: Key[] = [
|
|
|
73
73
|
const KEY_BASE =
|
|
74
74
|
"flex h-9 items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-[length:var(--focus-ring-width)] focus:ring-[var(--border-strong)]";
|
|
75
75
|
// Two key families, two fills: digits sit on the inset surface and darken on
|
|
76
|
-
// hover, operators sit a step down on `--
|
|
76
|
+
// hover, operators sit a step down on `--bg-active` and lift toward that surface —
|
|
77
77
|
// the same split `NumberPadSheet` uses for this keypad on a phone. The tokens
|
|
78
78
|
// flip with the theme, so neither needs a `dark:` twin.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
//
|
|
80
|
+
// 0.33 (docs/colour-roles-harmonization.md §12.2, §12.9): the digits hover onto
|
|
81
|
+
// `--bg-active`, 1.11–1.18:1 from the key — `--bg-hover` is mixed against the card and
|
|
82
|
+
// sat only 1.04:1 from the well, which in light is the page. The operators rest on
|
|
83
|
+
// `--bg-active` rather than `--border`: More contrast steps the border until it is a
|
|
84
|
+
// 3:1 LINE, and as a fill under the label that left 2.2–2.7:1.
|
|
85
|
+
const KEY_DIGIT = "bg-surface-2 text-primary hover:bg-active";
|
|
86
|
+
const KEY_ACCENT = "bg-active text-secondary hover:bg-surface-2";
|
|
83
87
|
|
|
84
88
|
function seed(initial: string): string {
|
|
85
89
|
const t = initial.trim();
|
|
@@ -186,7 +190,7 @@ function CalculatorPanel({
|
|
|
186
190
|
// "C" is a convention, not a name: read aloud it is the letter.
|
|
187
191
|
aria-label={labels.clear}
|
|
188
192
|
onClick={clearAll}
|
|
189
|
-
className={cn(KEY_BASE, "col-span-2
|
|
193
|
+
className={cn(KEY_BASE, "col-span-2", KEY_ACCENT)}
|
|
190
194
|
>
|
|
191
195
|
C
|
|
192
196
|
</button>
|
|
@@ -3,7 +3,7 @@ import type { ReactNode } from "react";
|
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { Checkbox } from "./checkbox";
|
|
5
5
|
import { hasMessage, mergeDescribedBy } from "./choice-parts";
|
|
6
|
-
import { useCommitReason } from "./write-lock";
|
|
6
|
+
import { useCommitReason, type CommitScope } from "./write-lock";
|
|
7
7
|
import { RequiredStarOnLegend } from "./field-parts";
|
|
8
8
|
|
|
9
9
|
/** One box of a {@link CheckboxGroup}. */
|
|
@@ -76,7 +76,7 @@ export interface CheckboxGroupProps<T extends string = string> {
|
|
|
76
76
|
* per-locale grants). Under a locked {@link WriteLockProvider} every box is locked
|
|
77
77
|
* the focusable way with the lock's reason. See {@link Checkbox}'s `commit`.
|
|
78
78
|
*/
|
|
79
|
-
commit?:
|
|
79
|
+
commit?: CommitScope;
|
|
80
80
|
/** Why no box can be changed — every box gets it, and it wins over `disabled` (the
|
|
81
81
|
* boxes stay focusable to say it). See {@link Checkbox}'s `disabledReason`. */
|
|
82
82
|
disabledReason?: ReactNode;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type DisabledReasonDisplay,
|
|
13
13
|
} from "./field-parts";
|
|
14
14
|
import { FOCUS_RING } from "./focus-ring";
|
|
15
|
+
import type { CommitScope } from "./write-lock";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* A checkbox that is still `<input type="checkbox">`.
|
|
@@ -134,7 +135,7 @@ export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement
|
|
|
134
135
|
* locked the `disabledReason` way with the lock's reason (which wins over its own).
|
|
135
136
|
* No provider, or an unlocked one: no effect. Button's `commit`, for keksdose K3.
|
|
136
137
|
*/
|
|
137
|
-
commit?:
|
|
138
|
+
commit?: CommitScope;
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
/**
|