@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
|
@@ -129,6 +129,13 @@ export interface BillingLabels {
|
|
|
129
129
|
usage: (used: string, limit: string) => string;
|
|
130
130
|
/** The subject of the `contact` mail; `dimension` is the app's name for it ("Budgets"). */
|
|
131
131
|
contactSubject: (dimension: string) => string;
|
|
132
|
+
/**
|
|
133
|
+
* The figure line of the limit's toast (`usePlanLimitToast`, §14.11) and of an
|
|
134
|
+
* operator's plan change over the new limit (§14.12): "Budgets: 3 of 3". A key of its
|
|
135
|
+
* own rather than `${dimension}: ${usage}`, because French puts a space before the
|
|
136
|
+
* colon. Both figures are formatted already.
|
|
137
|
+
*/
|
|
138
|
+
limitUsageLine: (dimension: string, used: string, limit: string) => string;
|
|
132
139
|
|
|
133
140
|
/* ── The provider's portal (§7, §12.26) ── */
|
|
134
141
|
/** The portal: payment method and invoices (§2.8, the provider hosts both). */
|
|
@@ -139,6 +146,24 @@ export interface BillingLabels {
|
|
|
139
146
|
* as unambiguously — a translator should use exactly that.
|
|
140
147
|
*/
|
|
141
148
|
cancel: string;
|
|
149
|
+
/**
|
|
150
|
+
* In place of both while the payer hasn't reached the provider (the overview's
|
|
151
|
+
* `at_provider` is false, §14.5): there is no customer to open a portal for yet. Also
|
|
152
|
+
* the words for `409 billing_not_at_provider`, a stale page that asked anyway.
|
|
153
|
+
*/
|
|
154
|
+
notAtProvider: string;
|
|
155
|
+
|
|
156
|
+
/* ── The pay page (§14.4) ── */
|
|
157
|
+
/* Built into the static page `dist/pay/` at the kit's build: it has no React and no
|
|
158
|
+
provider, so an app's override never reaches it. */
|
|
159
|
+
/** While the checkout opens. */
|
|
160
|
+
payOpening: string;
|
|
161
|
+
/** A link without a transaction, or billing switched off: nothing to pay. */
|
|
162
|
+
payNothing: string;
|
|
163
|
+
/** Paddle's script didn't load, or the page's configuration is wrong. */
|
|
164
|
+
payFailed: string;
|
|
165
|
+
/** The buyer closed the checkout without paying. */
|
|
166
|
+
payBack: string;
|
|
142
167
|
|
|
143
168
|
/* ── Offline (§12.5) ── */
|
|
144
169
|
/** Changes refused as `billing_read_only` inside a sync reply, kept queued and sent
|
|
@@ -159,6 +184,16 @@ export interface BillingLabels {
|
|
|
159
184
|
/** `billing_disabled`: billing is switched off on this server (§2.9), for a page or a
|
|
160
185
|
* link that reached it anyway. */
|
|
161
186
|
disabled: string;
|
|
187
|
+
/* 0.33.0 — server-kit 0.7's refusals (§14.2). `billing_not_at_provider` is
|
|
188
|
+
{@link notAtProvider}'s: a stale page that asked for the portal anyway. */
|
|
189
|
+
/** `billing_provider_unavailable` (502): the provider didn't answer. Try again. */
|
|
190
|
+
providerUnavailable: string;
|
|
191
|
+
/** `billing_already_subscribed` (409): a second checkout while a subscription runs
|
|
192
|
+
* (§14.6) — plan changes go through the portal. */
|
|
193
|
+
alreadySubscribed: string;
|
|
194
|
+
/** `billing_plan_not_sold` (422): the catalogue doesn't sell this plan with that
|
|
195
|
+
* period or currency. */
|
|
196
|
+
planNotSold: string;
|
|
162
197
|
}
|
|
163
198
|
|
|
164
199
|
/** "today", "tomorrow", "in 5 days". */
|
|
@@ -221,9 +256,16 @@ export const DEFAULT_BILLING_LABELS: BillingLabels = {
|
|
|
221
256
|
askForMore: "Ask for more",
|
|
222
257
|
usage: (used, limit) => `${used} of ${limit}`,
|
|
223
258
|
contactSubject: (dimension) => `Plan limit: ${dimension}`,
|
|
259
|
+
limitUsageLine: (dimension, used, limit) => `${dimension}: ${used} of ${limit}`,
|
|
224
260
|
|
|
225
261
|
manage: "Payment and invoices",
|
|
226
262
|
cancel: "Cancel subscription",
|
|
263
|
+
notAtProvider: "Nothing to manage yet. Payments and invoices appear here once you have a paid plan.",
|
|
264
|
+
|
|
265
|
+
payOpening: "Opening the secure checkout…",
|
|
266
|
+
payNothing: "There’s no payment to make here. To choose a plan, go back to the app.",
|
|
267
|
+
payFailed: "The checkout couldn’t be opened. Please go back and try again in a moment.",
|
|
268
|
+
payBack: "The checkout was closed. You can go back, or reload this page to open it again.",
|
|
227
269
|
|
|
228
270
|
waitingChanges: (count) =>
|
|
229
271
|
count === 1 ? "1 change waiting for a plan" : `${count} changes waiting for a plan`,
|
|
@@ -231,6 +273,9 @@ export const DEFAULT_BILLING_LABELS: BillingLabels = {
|
|
|
231
273
|
|
|
232
274
|
notConfigured: "Payments aren't set up yet. Please try again later.",
|
|
233
275
|
disabled: "Subscriptions aren't available here.",
|
|
276
|
+
providerUnavailable: "The payment provider can’t be reached right now. Please try again in a moment.",
|
|
277
|
+
alreadySubscribed: "You already have a subscription. To change your plan, go to “Payment and invoices”.",
|
|
278
|
+
planNotSold: "This plan isn’t offered for that billing period or currency. Please choose another.",
|
|
234
279
|
};
|
|
235
280
|
|
|
236
281
|
/** The `billing` namespace, resolved: English, then the provider, then `labels` — which
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
|
|
3
3
|
import type { LabelOverride } from "../i18n/kit-labels";
|
|
4
|
+
import type { WriteLockHold, WriteLockKind } from "../components/write-lock";
|
|
4
5
|
import { useBillingLabels } from "./billing-labels";
|
|
5
6
|
import type { BillingLabels } from "./billing-labels";
|
|
6
7
|
|
|
7
|
-
/** One reason an app may lock writing: whether it locks now,
|
|
8
|
+
/** One reason an app may lock writing: whether it locks now, its one sentence, and what
|
|
9
|
+
* kind of lock it is. */
|
|
8
10
|
export interface WriteLockSource {
|
|
9
11
|
locked: boolean;
|
|
10
12
|
/** The sentence a locked commit shows ("Not possible in the demo."). */
|
|
11
13
|
reason?: ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* `"demo"`, `"billing"`, or the app's own (§12.36): a control's `commit` may be exempt
|
|
16
|
+
* from a kind — `COMMIT_EXCEPT_BILLING`. Left out, no control can be exempt from it.
|
|
17
|
+
*/
|
|
18
|
+
kind?: WriteLockKind;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
/** What {@link combineWriteLocks} answers: the props of one `WriteLockProvider`. */
|
|
@@ -16,6 +23,9 @@ export interface CombinedWriteLock {
|
|
|
16
23
|
locked: boolean;
|
|
17
24
|
/** The first locked source's reason; `undefined` while nothing locks. */
|
|
18
25
|
reason: ReactNode | undefined;
|
|
26
|
+
/** EVERY locked source, kind and reason, in the order given (§12.36) — so a control
|
|
27
|
+
* exempt from billing under the demo's lock and billing's still finds the demo's. */
|
|
28
|
+
holds: WriteLockHold[];
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
/**
|
|
@@ -41,14 +51,21 @@ export interface CombinedWriteLock {
|
|
|
41
51
|
* not two joined: a tooltip answers "why can't I?" once, and in practice one source locks
|
|
42
52
|
* at a time — demo users have no subscription row and are never gated (§12.7). `null`,
|
|
43
53
|
* `undefined` and `false` entries are skipped, for `cond && { … }`.
|
|
54
|
+
*
|
|
55
|
+
* 0.33 (§12.36): name each source's `kind` — `{ locked, reason, kind: "demo" }`,
|
|
56
|
+
* {@link useBillingWriteLock} for billing — and `holds` carries every locked one, in
|
|
57
|
+
* order, so the kit's parts that a lapsed plan still allows (removing access, the
|
|
58
|
+
* account's own settings, an operator's plan change) reopen under billing by themselves
|
|
59
|
+
* while the demo's lock still holds them.
|
|
44
60
|
*/
|
|
45
61
|
export function combineWriteLocks(
|
|
46
62
|
sources: ReadonlyArray<WriteLockSource | null | undefined | false>,
|
|
47
63
|
): CombinedWriteLock {
|
|
64
|
+
const holds: WriteLockHold[] = [];
|
|
48
65
|
for (const source of sources) {
|
|
49
|
-
if (source && source.locked)
|
|
66
|
+
if (source && source.locked) holds.push({ kind: source.kind, reason: source.reason });
|
|
50
67
|
}
|
|
51
|
-
return { locked:
|
|
68
|
+
return { locked: holds.length > 0, reason: holds[0]?.reason, holds };
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
export interface BillingLockReasonOptions {
|
|
@@ -73,3 +90,19 @@ export function useBillingLockReason(options: BillingLockReasonOptions = {}): st
|
|
|
73
90
|
const labels = useBillingLabels(options.labels);
|
|
74
91
|
return options.guest ? labels.guestReadOnly(options.item) : labels.lockReason;
|
|
75
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Billing's lock as one source for {@link combineWriteLocks}: `{ locked, reason, kind:
|
|
96
|
+
* "billing" }`, the reason the payer's or a guest's as {@link useBillingLockReason} says
|
|
97
|
+
* it (§12.36). The kind is what lets the kit's parts that §3.3 and §12.13 allow stay live
|
|
98
|
+
* under it.
|
|
99
|
+
*
|
|
100
|
+
* <WriteLockProvider {...combineWriteLocks([
|
|
101
|
+
* { locked: demoReadOnly, reason: demo.writeLocked, kind: "demo" },
|
|
102
|
+
* useBillingWriteLock(budget.locked === "billing", { guest: !isOwner, item: budget.name }),
|
|
103
|
+
* ])}>
|
|
104
|
+
*/
|
|
105
|
+
export function useBillingWriteLock(locked: boolean, options: BillingLockReasonOptions = {}): WriteLockSource {
|
|
106
|
+
const reason = useBillingLockReason(options);
|
|
107
|
+
return { locked, reason, kind: "billing" };
|
|
108
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { DateInput } from "../lib/format";
|
|
2
|
+
import type { SubscriptionStatus } from "./billing-labels";
|
|
3
|
+
import type { BillingCurrency } from "./plan-price";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The billing API's shapes the kit's parts read (docs/billing-harmonization.md §4,
|
|
7
|
+
* §14.5) — server-kit 0.7's schemas, field for field under their wire names, for an app
|
|
8
|
+
* whose client is hand-written (kastlan's `features/billing/types.ts`, Kurvenschmiede's
|
|
9
|
+
* `shared/types/billing.ts`, which no mirror test covers).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Where a row's current state came from (server-kit's `SubscriptionSource`, §3.2). */
|
|
13
|
+
export type SubscriptionSource = "trial" | "provider" | "manual" | "beta";
|
|
14
|
+
|
|
15
|
+
/** `GET /billing/overview` (§4): where the payer's subscription stands, what the plan
|
|
16
|
+
* allows and what is used. Only ever the payer's own (§12.6). */
|
|
17
|
+
export interface BillingOverview {
|
|
18
|
+
/** The plan's code, lowercase (§3.1). */
|
|
19
|
+
plan: string;
|
|
20
|
+
status: SubscriptionStatus;
|
|
21
|
+
source: SubscriptionSource;
|
|
22
|
+
/** False: read-only (§3.3). */
|
|
23
|
+
in_good_standing: boolean;
|
|
24
|
+
trial_ends_at: DateInput;
|
|
25
|
+
/** When the free grant ends; null: no end. */
|
|
26
|
+
comped_until: DateInput;
|
|
27
|
+
current_period_end: DateInput;
|
|
28
|
+
cancel_at_period_end: boolean;
|
|
29
|
+
/** Dimension → limit; null for unlimited. */
|
|
30
|
+
limits: Record<string, number | null>;
|
|
31
|
+
/** What the payer has, per dimension (§12.15). */
|
|
32
|
+
usage: Record<string, number>;
|
|
33
|
+
currency: BillingCurrency;
|
|
34
|
+
/**
|
|
35
|
+
* 0.7 (§14.5): the provider has a customer for this payer, whatever the status or the
|
|
36
|
+
* source — `SubscriptionActions`' `atProvider`. False: "nothing to manage yet", and a
|
|
37
|
+
* portal request would answer `409 billing_not_at_provider`.
|
|
38
|
+
*/
|
|
39
|
+
at_provider: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Where in the provider's portal a request lands (server-kit's `PortalTarget`, §14.5):
|
|
44
|
+
*
|
|
45
|
+
* - `overview` — "Payment and invoices";
|
|
46
|
+
* - `cancel` — "Cancel subscription": the subscription's cancel link (§12.26);
|
|
47
|
+
* - `payment_method` — the payment-failed banner's "Update payment method".
|
|
48
|
+
*
|
|
49
|
+
* The server answers the overview where Paddle gives no such link (a paused or cancelled
|
|
50
|
+
* subscription).
|
|
51
|
+
*/
|
|
52
|
+
export type PortalTarget = "overview" | "cancel" | "payment_method";
|
|
53
|
+
|
|
54
|
+
/** `POST /billing/portal`'s optional body (server-kit's `PortalRequest`, §14.5); left
|
|
55
|
+
* out, the overview. The answer is `{url}`, a fresh link that is never stored. */
|
|
56
|
+
export interface PortalRequest {
|
|
57
|
+
target?: PortalTarget;
|
|
58
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { useCallback, useEffect, useEffectEvent, useLayoutEffect, useRef, useSyncExternalStore } from "react";
|
|
2
|
+
|
|
3
|
+
import { toDate } from "../lib/format";
|
|
4
|
+
import type { DateInput } from "../lib/format";
|
|
5
|
+
import { readStored, writeStored } from "../lib/safe-storage";
|
|
6
|
+
import type { BillingStanding } from "./billing-standing";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* "Payment processing" after the way back from a checkout (docs/billing-harmonization.md
|
|
10
|
+
* §12.21, §14.4; keksdose's `features/billing/checkout-return.ts`, made router-agnostic).
|
|
11
|
+
*
|
|
12
|
+
* The provider tells the app about a payment by WEBHOOK, and the webhook may land after
|
|
13
|
+
* the buyer is back: Cloud Run scales to zero, and a cold start can outlast the
|
|
14
|
+
* provider's redirect. Without this, someone who has just paid comes back to a page that
|
|
15
|
+
* still says their trial has ended. So the return is recognised (`?checkout=done`,
|
|
16
|
+
* `isCheckoutReturn`), the page says "your payment is being processed"
|
|
17
|
+
* (`BillingBanner kind="processing"`), and the overview is asked again every few seconds
|
|
18
|
+
* until it has moved.
|
|
19
|
+
*
|
|
20
|
+
* **"Landed" is judged against the overview at departure** ({@link checkoutFingerprint},
|
|
21
|
+
* noted by {@link noteCheckoutStarted}), not by "is it `active` yet": a trial and a beta
|
|
22
|
+
* grant can both buy, and a running free grant keeps its status whatever the provider
|
|
23
|
+
* says (§12.11) — only its period end moves — so "active" alone would wait for ever on
|
|
24
|
+
* one of them. A return with no departure on record (another tab, another device) waits
|
|
25
|
+
* for `active`. kastlan's 60-second poll for `source === "provider"` is the case this
|
|
26
|
+
* replaces: a payer who bought under a grant never matched it.
|
|
27
|
+
*
|
|
28
|
+
* **Bounded:** after `timeoutMs` (10 minutes) the device stops asking, and the page says
|
|
29
|
+
* what the overview says, whatever that is — a checkout abandoned at the provider must
|
|
30
|
+
* not leave "processing" up for good.
|
|
31
|
+
*
|
|
32
|
+
* **Remembered across the whole-page return, per payer.** The departure goes into the
|
|
33
|
+
* browser's storage through the kit's safe storage (a throwing storage — Safari's
|
|
34
|
+
* private mode — keeps it in memory for this page instead), under `storageKey`; a mark
|
|
35
|
+
* names its payer, so it never speaks for another account on a shared device. It is a
|
|
36
|
+
* browser entry on the app's origin: an app whose privacy text lists its entries names
|
|
37
|
+
* it (Kurvenschmiede passes its own key).
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/** The overview fields a completed checkout changes (wire names, `GET /billing/overview`).
|
|
41
|
+
* Pass the overview as it came; the other fields are ignored. */
|
|
42
|
+
export type CheckoutOverview = Pick<BillingStanding, "status" | "in_good_standing"> & {
|
|
43
|
+
plan: string;
|
|
44
|
+
source: string;
|
|
45
|
+
current_period_end?: DateInput;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** How long a return keeps the page in "processing" before it trusts the overview. */
|
|
49
|
+
const PROCESSING_FOR = 10 * 60_000;
|
|
50
|
+
/** How often the overview is asked meanwhile. */
|
|
51
|
+
const POLL_EVERY = 4_000;
|
|
52
|
+
/** The browser entry, unless the app names its own. */
|
|
53
|
+
const STORAGE_KEY = "eifi1-billing-checkout";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* What of the overview a completed checkout changes, as one string: the plan (read
|
|
57
|
+
* without case, §12.16), the status, the source, the standing and the period's end (read
|
|
58
|
+
* as an instant, so `…Z` and `…+00:00` agree).
|
|
59
|
+
*/
|
|
60
|
+
export function checkoutFingerprint(o: CheckoutOverview): string {
|
|
61
|
+
const end = toDate(o.current_period_end ?? null);
|
|
62
|
+
return [
|
|
63
|
+
String(o.plan ?? "").toLowerCase(),
|
|
64
|
+
o.status,
|
|
65
|
+
o.source,
|
|
66
|
+
String(o.in_good_standing),
|
|
67
|
+
end ? end.toISOString() : "",
|
|
68
|
+
].join("|");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether the overview `now` shows the checkout: it differs from the fingerprint taken
|
|
73
|
+
* at departure (`before`), or — with no departure on record — it is `active`.
|
|
74
|
+
*/
|
|
75
|
+
export function checkoutLanded(before: string | null, now: CheckoutOverview): boolean {
|
|
76
|
+
return before === null ? now.status === "active" : checkoutFingerprint(now) !== before;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* ── The store: one per storage key, shared by every mounted instance ─────── */
|
|
80
|
+
|
|
81
|
+
interface PendingCheckout {
|
|
82
|
+
/** Whose checkout: a user's or a company's id, as a string. */
|
|
83
|
+
payer: string;
|
|
84
|
+
/** The overview's fingerprint at departure, or null when it was not known. */
|
|
85
|
+
before: string | null;
|
|
86
|
+
/** When the buyer came back; null while they are (or were) at the provider. */
|
|
87
|
+
returnedAt: number | null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** One mounted hook: what the shared timers call, kept current on every render. */
|
|
91
|
+
interface Instance {
|
|
92
|
+
payer: string | null;
|
|
93
|
+
refetch: () => unknown;
|
|
94
|
+
onLanded: (() => void) | undefined;
|
|
95
|
+
timeoutMs: number;
|
|
96
|
+
pollMs: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface CheckoutStore {
|
|
100
|
+
key: string;
|
|
101
|
+
pending: PendingCheckout | null;
|
|
102
|
+
listeners: Set<() => void>;
|
|
103
|
+
/** The mounted instances, in mount order: the last is the latest. */
|
|
104
|
+
instances: Instance[];
|
|
105
|
+
poll: ReturnType<typeof setInterval> | null;
|
|
106
|
+
pollEvery: number;
|
|
107
|
+
expiry: ReturnType<typeof setTimeout> | null;
|
|
108
|
+
expiryAt: number | null;
|
|
109
|
+
/** An instance consumed the marker in this commit; the others leave it be. */
|
|
110
|
+
consuming: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Module-level, so the banner's instance and the subscription page's share one state,
|
|
114
|
+
* one poll and one landing (keksdose mounts the hook twice, §14.4). */
|
|
115
|
+
const stores = new Map<string, CheckoutStore>();
|
|
116
|
+
|
|
117
|
+
function readPending(key: string): PendingCheckout | null {
|
|
118
|
+
const raw = readStored(key);
|
|
119
|
+
if (raw === null) return null;
|
|
120
|
+
try {
|
|
121
|
+
const value = JSON.parse(raw) as Partial<PendingCheckout> | null;
|
|
122
|
+
if (
|
|
123
|
+
value &&
|
|
124
|
+
typeof value.payer === "string" &&
|
|
125
|
+
(value.before === null || typeof value.before === "string") &&
|
|
126
|
+
(value.returnedAt === null || typeof value.returnedAt === "number")
|
|
127
|
+
) {
|
|
128
|
+
return { payer: value.payer, before: value.before, returnedAt: value.returnedAt };
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// Not ours, or damaged: as if nothing was noted.
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Drop the browser entry. lib/safe-storage reads and writes; removing needs the same
|
|
137
|
+
* guard (a blocked storage throws on access). */
|
|
138
|
+
function forget(key: string): void {
|
|
139
|
+
if (typeof window === "undefined") return;
|
|
140
|
+
try {
|
|
141
|
+
window.localStorage.removeItem(key);
|
|
142
|
+
} catch {
|
|
143
|
+
// ignore (private mode, partitioned storage)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function storeOf(key: string): CheckoutStore {
|
|
148
|
+
let store = stores.get(key);
|
|
149
|
+
if (!store) {
|
|
150
|
+
store = {
|
|
151
|
+
key,
|
|
152
|
+
pending: readPending(key),
|
|
153
|
+
listeners: new Set(),
|
|
154
|
+
instances: [],
|
|
155
|
+
poll: null,
|
|
156
|
+
pollEvery: 0,
|
|
157
|
+
expiry: null,
|
|
158
|
+
expiryAt: null,
|
|
159
|
+
consuming: false,
|
|
160
|
+
};
|
|
161
|
+
stores.set(key, store);
|
|
162
|
+
}
|
|
163
|
+
return store;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function setPending(store: CheckoutStore, next: PendingCheckout | null): void {
|
|
167
|
+
store.pending = next;
|
|
168
|
+
if (next) writeStored(store.key, JSON.stringify(next));
|
|
169
|
+
else forget(store.key);
|
|
170
|
+
for (const listener of store.listeners) listener();
|
|
171
|
+
sync(store);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The latest mounted instance's refetch, failures swallowed: a poll that fails is
|
|
175
|
+
* simply asked again at the next tick. */
|
|
176
|
+
function refetchLatest(store: CheckoutStore): void {
|
|
177
|
+
const latest = store.instances.at(-1);
|
|
178
|
+
if (!latest) return;
|
|
179
|
+
try {
|
|
180
|
+
const result = latest.refetch() as PromiseLike<unknown> | undefined;
|
|
181
|
+
if (result && typeof result.then === "function") result.then(undefined, () => {});
|
|
182
|
+
} catch {
|
|
183
|
+
// as above
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Arms or stops the store's two timers — the bound and the poll — for its state. */
|
|
188
|
+
function sync(store: CheckoutStore): void {
|
|
189
|
+
const pending = store.pending;
|
|
190
|
+
const latest = store.instances.at(-1);
|
|
191
|
+
const returnedAt = pending?.returnedAt ?? null;
|
|
192
|
+
|
|
193
|
+
if (returnedAt === null) {
|
|
194
|
+
if (store.expiry) clearTimeout(store.expiry);
|
|
195
|
+
store.expiry = null;
|
|
196
|
+
store.expiryAt = null;
|
|
197
|
+
} else {
|
|
198
|
+
const at = returnedAt + (latest?.timeoutMs ?? PROCESSING_FOR);
|
|
199
|
+
if (at <= Date.now()) {
|
|
200
|
+
setPending(store, null);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (store.expiryAt !== at) {
|
|
204
|
+
if (store.expiry) clearTimeout(store.expiry);
|
|
205
|
+
store.expiryAt = at;
|
|
206
|
+
store.expiry = setTimeout(() => {
|
|
207
|
+
store.expiry = null;
|
|
208
|
+
store.expiryAt = null;
|
|
209
|
+
if (store.pending === pending) setPending(store, null);
|
|
210
|
+
}, at - Date.now());
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// One poll for every instance, while a mounted one is the mark's payer.
|
|
215
|
+
const watching = returnedAt !== null && store.instances.some((i) => i.payer === pending!.payer);
|
|
216
|
+
const every = latest?.pollMs ?? POLL_EVERY;
|
|
217
|
+
if (!watching) {
|
|
218
|
+
if (store.poll) clearInterval(store.poll);
|
|
219
|
+
store.poll = null;
|
|
220
|
+
} else if (!store.poll || store.pollEvery !== every) {
|
|
221
|
+
if (store.poll) clearInterval(store.poll);
|
|
222
|
+
store.pollEvery = every;
|
|
223
|
+
store.poll = setInterval(() => refetchLatest(store), every);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** A mounted instance joins the store's timers; the latest is the one they call. */
|
|
228
|
+
function register(store: CheckoutStore, instance: Instance): () => void {
|
|
229
|
+
store.instances.push(instance);
|
|
230
|
+
return () => {
|
|
231
|
+
store.instances = store.instances.filter((i) => i !== instance);
|
|
232
|
+
sync(store);
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Whether this instance is the one to consume a return marker: the first in a commit is.
|
|
238
|
+
* Another instance in the same commit sees the same marker and leaves it be; the claim
|
|
239
|
+
* lapses once the commit's effects have run.
|
|
240
|
+
*/
|
|
241
|
+
function claimReturn(store: CheckoutStore): boolean {
|
|
242
|
+
if (store.consuming) return false;
|
|
243
|
+
store.consuming = true;
|
|
244
|
+
void Promise.resolve().then(() => {
|
|
245
|
+
store.consuming = false;
|
|
246
|
+
});
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Note the departure for a checkout — just before the page leaves for the provider (or
|
|
252
|
+
* for the pay page, `payPageUrl(url, locale)`):
|
|
253
|
+
*
|
|
254
|
+
* noteCheckoutStarted(user.id, overview.data);
|
|
255
|
+
* window.location.assign(payPageUrl(url, i18n.language));
|
|
256
|
+
*
|
|
257
|
+
* `payer` is the user's or the company's id: a mark never speaks for another payer.
|
|
258
|
+
* `overview` is what the page shows now; left out, the return waits for `active`. Pass
|
|
259
|
+
* the same `storageKey` as the hook's.
|
|
260
|
+
*/
|
|
261
|
+
export function noteCheckoutStarted(
|
|
262
|
+
payer: string | number,
|
|
263
|
+
overview?: CheckoutOverview,
|
|
264
|
+
opts: { storageKey?: string } = {},
|
|
265
|
+
): void {
|
|
266
|
+
const store = storeOf(opts.storageKey ?? STORAGE_KEY);
|
|
267
|
+
setPending(store, {
|
|
268
|
+
payer: String(payer),
|
|
269
|
+
before: overview ? checkoutFingerprint(overview) : null,
|
|
270
|
+
returnedAt: null,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export interface CheckoutProcessingOptions {
|
|
275
|
+
/** The signed-in payer — the user's or the company's id; null while unknown (the
|
|
276
|
+
* marker then waits in the URL). */
|
|
277
|
+
payer: string | number | null;
|
|
278
|
+
/** The page was reached as the way back: `isCheckoutReturn(location.search)`. */
|
|
279
|
+
returned: boolean;
|
|
280
|
+
/** Drop the marker, with replace — `setSearchParams((p) => withoutCheckoutReturn(p),
|
|
281
|
+
* { replace: true })`. Called once per return, by the first instance that sees it. */
|
|
282
|
+
onConsumed: () => void;
|
|
283
|
+
/** The overview query's data, judged here against the departure. */
|
|
284
|
+
overview: CheckoutOverview | undefined;
|
|
285
|
+
/** The overview query's refetch: the hook polls with it — one timer for every
|
|
286
|
+
* instance, with the latest mounted one's — and "Check again" calls it. */
|
|
287
|
+
refetch: () => unknown;
|
|
288
|
+
/** Once per landing: refresh what a new standing changes — the plans, the items whose
|
|
289
|
+
* `locked` may have lifted. The overview itself is already fresh. */
|
|
290
|
+
onLanded?: () => void;
|
|
291
|
+
/** How long a return keeps "processing". Default 600 000 (10 minutes). */
|
|
292
|
+
timeoutMs?: number;
|
|
293
|
+
/** How often the overview is asked meanwhile. Default 4 000. */
|
|
294
|
+
pollMs?: number;
|
|
295
|
+
/** The browser entry. Default `"eifi1-billing-checkout"`. */
|
|
296
|
+
storageKey?: string;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Whether this payer is back from a checkout whose payment the overview has not shown
|
|
301
|
+
* yet — and the "Check again" for the banner that says so:
|
|
302
|
+
*
|
|
303
|
+
* const [params, setParams] = useSearchParams();
|
|
304
|
+
* const overview = useQuery(billingOverviewQuery);
|
|
305
|
+
* const { processing, checkAgain } = useCheckoutProcessing({
|
|
306
|
+
* payer: user?.id ?? null,
|
|
307
|
+
* returned: isCheckoutReturn(params),
|
|
308
|
+
* onConsumed: () => setParams((p) => withoutCheckoutReturn(p), { replace: true }),
|
|
309
|
+
* overview: overview.data,
|
|
310
|
+
* refetch: overview.refetch,
|
|
311
|
+
* onLanded: () => void qc.invalidateQueries({ queryKey: ["budgets"] }),
|
|
312
|
+
* });
|
|
313
|
+
* processing && <BillingBanner kind="processing" onAction={checkAgain} />
|
|
314
|
+
*
|
|
315
|
+
* The kit has no router: the app says whether the URL returned and how to drop the
|
|
316
|
+
* marker. Mount it wherever the return lands and the banner shows — every instance with
|
|
317
|
+
* the same `storageKey` shares one state: the first that sees the marker consumes it,
|
|
318
|
+
* one timer polls, and `onLanded` runs once. The app's `refetchOnWindowFocus` doesn't
|
|
319
|
+
* matter.
|
|
320
|
+
*/
|
|
321
|
+
export function useCheckoutProcessing(options: CheckoutProcessingOptions): {
|
|
322
|
+
processing: boolean;
|
|
323
|
+
checkAgain: () => void;
|
|
324
|
+
} {
|
|
325
|
+
const {
|
|
326
|
+
payer,
|
|
327
|
+
returned,
|
|
328
|
+
onConsumed,
|
|
329
|
+
overview,
|
|
330
|
+
refetch,
|
|
331
|
+
onLanded,
|
|
332
|
+
timeoutMs = PROCESSING_FOR,
|
|
333
|
+
pollMs = POLL_EVERY,
|
|
334
|
+
storageKey = STORAGE_KEY,
|
|
335
|
+
} = options;
|
|
336
|
+
const payerKey = payer === null || payer === undefined ? null : String(payer);
|
|
337
|
+
const store = storeOf(storageKey);
|
|
338
|
+
const subscribe = useCallback(
|
|
339
|
+
(notify: () => void) => {
|
|
340
|
+
store.listeners.add(notify);
|
|
341
|
+
return () => {
|
|
342
|
+
store.listeners.delete(notify);
|
|
343
|
+
};
|
|
344
|
+
},
|
|
345
|
+
[store],
|
|
346
|
+
);
|
|
347
|
+
const snapshot = () => store.pending;
|
|
348
|
+
const pending = useSyncExternalStore(subscribe, snapshot, snapshot);
|
|
349
|
+
const processing = payerKey !== null && pending?.payer === payerKey && pending.returnedAt !== null;
|
|
350
|
+
|
|
351
|
+
// This instance, as the shared timers see it. Registered before paint, so a mark that
|
|
352
|
+
// ran out while the page was closed is dropped before "processing" can flash.
|
|
353
|
+
const instanceRef = useRef<Instance>({ payer: payerKey, refetch, onLanded, timeoutMs, pollMs });
|
|
354
|
+
useLayoutEffect(() => register(store, instanceRef.current), [store]);
|
|
355
|
+
useLayoutEffect(() => {
|
|
356
|
+
Object.assign(instanceRef.current, { payer: payerKey, refetch, onLanded, timeoutMs, pollMs });
|
|
357
|
+
sync(store);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// The return: note it, then drop the marker so a reload does not count it twice.
|
|
361
|
+
const consumed = useEffectEvent(() => onConsumed());
|
|
362
|
+
useEffect(() => {
|
|
363
|
+
if (!returned || payerKey === null || !claimReturn(store)) return;
|
|
364
|
+
const before = store.pending?.payer === payerKey ? store.pending.before : null;
|
|
365
|
+
setPending(store, { payer: payerKey, before, returnedAt: Date.now() });
|
|
366
|
+
consumed();
|
|
367
|
+
}, [returned, payerKey, store]);
|
|
368
|
+
|
|
369
|
+
// Caught up: the overview has moved since the departure. Judged against the store as
|
|
370
|
+
// it is NOW, so a second instance in the same commit finds the mark gone.
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
if (!processing || !overview) return;
|
|
373
|
+
const live = store.pending;
|
|
374
|
+
if (!live || live.payer !== payerKey || live.returnedAt === null) return;
|
|
375
|
+
if (!checkoutLanded(live.before, overview)) return;
|
|
376
|
+
setPending(store, null);
|
|
377
|
+
const latest = [...store.instances].reverse().find((i) => i.onLanded);
|
|
378
|
+
latest?.onLanded?.();
|
|
379
|
+
}, [processing, overview, payerKey, store]);
|
|
380
|
+
|
|
381
|
+
const checkAgain = useCallback(() => {
|
|
382
|
+
try {
|
|
383
|
+
const result = instanceRef.current.refetch() as PromiseLike<unknown> | undefined;
|
|
384
|
+
if (result && typeof result.then === "function") result.then(undefined, () => {});
|
|
385
|
+
} catch {
|
|
386
|
+
// The banner stays; the next poll asks again.
|
|
387
|
+
}
|
|
388
|
+
}, []);
|
|
389
|
+
|
|
390
|
+
return { processing, checkAgain };
|
|
391
|
+
}
|