@eifi1/ui-kit 0.51.4 → 0.51.6
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 +5 -5
- package/dist/account/account-settings-labels.js.map +1 -1
- package/dist/account/account-settings.js +11 -9
- package/dist/account/account-settings.js.map +1 -1
- package/dist/account/delete-account-setting.js +56 -65
- package/dist/account/delete-account-setting.js.map +1 -1
- package/dist/account/email-change-setting-labels.js.map +1 -1
- package/dist/account/email-change-setting.js.map +1 -1
- package/dist/account/passkeys-setting.js.map +1 -1
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.js +1 -3
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/admin-action-log.js.map +1 -1
- package/dist/admin/admin-area-labels.js +4 -1
- package/dist/admin/admin-area-labels.js.map +1 -1
- package/dist/admin/admin-overview.js +19 -11
- package/dist/admin/admin-overview.js.map +1 -1
- package/dist/admin/admin-sections.js.map +1 -1
- package/dist/admin/admin-tool-links.d.ts +1 -1
- package/dist/admin/admin-tool-links.js +7 -1
- package/dist/admin/admin-tool-links.js.map +1 -1
- package/dist/admin/invitations-panel.js +2 -13
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/mail-test-card.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.js +4 -11
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/admin/role-select.js.map +1 -1
- package/dist/admin/user-roster.js +1 -9
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/accept-invitation.js.map +1 -1
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/forgot-password-form.js +23 -31
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form.js.map +1 -1
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form.js +13 -5
- package/dist/auth/sign-in-form.js.map +1 -1
- package/dist/auth/verify-email.js +1 -8
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/checkout-processing.js.map +1 -1
- package/dist/billing/plan-card.js +91 -85
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-limit-toast.js.map +1 -1
- package/dist/billing/plan-limit.js.map +1 -1
- package/dist/billing/plan-price.js.map +1 -1
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/billing/subscription-actions.js.map +1 -1
- package/dist/bin/eifi1-pay-page.mjs +6 -1
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.js +1 -5
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.js +29 -12
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.js.map +1 -1
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.js +1 -4
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.js +1 -7
- package/dist/components/button.js.map +1 -1
- package/dist/components/calculator.js +5 -5
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/calendar-heatmap.js +4 -10
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/card.js +1 -8
- package/dist/components/card.js.map +1 -1
- package/dist/components/chart-zoom-core.js +3 -17
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/chart.d.ts +1 -1
- package/dist/components/chart.js +4 -22
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox-group.js +1 -4
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/chip-input.js +4 -1
- package/dist/components/chip-input.js.map +1 -1
- package/dist/components/chip.js +10 -5
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.js +58 -74
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/column-mapper-labels.js.map +1 -1
- package/dist/components/column-mapper.js +167 -227
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/combobox-core.js +1 -4
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js +1 -4
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/country-select.d.ts +5 -4
- package/dist/components/country-select.js +3 -19
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/currency-option-list.js +10 -8
- package/dist/components/currency-option-list.js.map +1 -1
- package/dist/components/currency-select.js +1 -6
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.js.map +1 -1
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filter-popover.js +2 -18
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.js +3 -1
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.js +44 -56
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-mobile.js +31 -44
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-row-actions.js +19 -16
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table-virtual.js +4 -1
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/data-table.js +30 -39
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +1 -6
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/description-list.js +10 -10
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.js +9 -2
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.js +17 -27
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.js +25 -35
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/empty-state.js.map +1 -1
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/field-base.js +5 -5
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-parts.d.ts +1 -1
- package/dist/components/field-parts.js +10 -14
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-sync.js +8 -10
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.js +4 -1
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/focus-ring.js.map +1 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/grouped-picker.js +6 -8
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.js +1 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/iban-input-labels.js.map +1 -1
- package/dist/components/icon-button.js +4 -1
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/icon-picker.js +2 -7
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/image-grid.js +41 -50
- package/dist/components/image-grid.js.map +1 -1
- package/dist/components/inline-edit-field.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +2 -8
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +14 -2
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +1 -7
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal-labels.js.map +1 -1
- package/dist/components/legal-page.js +14 -2
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.js +12 -2
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/line-items.js +26 -36
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.js +22 -10
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.js +29 -53
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.js +13 -31
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.js +10 -21
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +3 -16
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/nav-pills.js +10 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.js +1 -7
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.js +1 -4
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/one-time-code-input.js +1 -7
- package/dist/components/one-time-code-input.js.map +1 -1
- package/dist/components/page-contents.js +13 -22
- package/dist/components/page-contents.js.map +1 -1
- package/dist/components/page-header.js +1 -11
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/password-strength-labels.js.map +1 -1
- package/dist/components/password-strength.js +2 -8
- package/dist/components/password-strength.js.map +1 -1
- package/dist/components/phone-input.js +42 -57
- package/dist/components/phone-input.js.map +1 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart.js +1 -8
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/progress-bar.js +16 -9
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.js +1 -8
- package/dist/components/qr-code.js.map +1 -1
- package/dist/components/row-actions.js +5 -1
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/select.js +28 -38
- package/dist/components/select.js.map +1 -1
- package/dist/components/separator.js +1 -5
- package/dist/components/separator.js.map +1 -1
- package/dist/components/series-chart-axes.js +4 -1
- package/dist/components/series-chart-axes.js.map +1 -1
- package/dist/components/series-chart-budget.js.map +1 -1
- package/dist/components/series-chart-ticks.js.map +1 -1
- package/dist/components/series-chart.js +29 -45
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.js +21 -17
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.js +4 -9
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/signature-pad.js +4 -13
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/signed-amount.js +12 -5
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/skeleton.js +2 -17
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/slider.js +13 -36
- package/dist/components/slider.js.map +1 -1
- package/dist/components/sparkline.js.map +1 -1
- package/dist/components/stat-tile.js +14 -10
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swatch-picker.js +14 -4
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +1 -1
- package/dist/components/swipeable-row.js +1 -8
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/table-skeleton.js +4 -1
- package/dist/components/table-skeleton.js.map +1 -1
- package/dist/components/table.js +12 -9
- package/dist/components/table.js.map +1 -1
- package/dist/components/tabs.js +5 -24
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/text.js.map +1 -1
- package/dist/components/tile-radio.js +1 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle-group.js +6 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.js +2 -11
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.js +2 -22
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/translation-review-editor.js +1 -4
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review-parts.d.ts +1 -1
- package/dist/components/translation-review-parts.js +7 -1
- package/dist/components/translation-review-parts.js.map +1 -1
- package/dist/components/translation-review.js +18 -7
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/tree-view.js +14 -10
- package/dist/components/tree-view.js.map +1 -1
- package/dist/components/treemap.d.ts +24 -2
- package/dist/components/treemap.js +28 -4
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/use-table-state.js +14 -11
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.js +3 -26
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/demo/demo-start.js +8 -4
- package/dist/demo/demo-start.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-attachment-labels.js.map +1 -1
- package/dist/feedback/feedback-attachment.js +1 -8
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-capture.d.ts +1 -1
- package/dist/feedback/feedback-capture.js +3 -1
- package/dist/feedback/feedback-capture.js.map +1 -1
- package/dist/feedback/feedback-comment-thread.js +27 -13
- package/dist/feedback/feedback-comment-thread.js.map +1 -1
- package/dist/feedback/feedback-context.js +1 -4
- package/dist/feedback/feedback-context.js.map +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-public-list.js +7 -1
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-record.js +1 -5
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-replies.js +25 -10
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-table.js +4 -1
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread.js +2 -13
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback/feedback-vocabulary.js.map +1 -1
- package/dist/feedback/feedback-vote-button.js.map +1 -1
- package/dist/hooks/use-anchored-rect.js.map +1 -1
- package/dist/hooks/use-document-title.js.map +1 -1
- package/dist/hooks/use-file-drop.js.map +1 -1
- package/dist/hooks/use-file-picker-labels.js.map +1 -1
- package/dist/hooks/use-file-picker.js.map +1 -1
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-overlay-history.js +10 -5
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/hooks/use-row-swipe.d.ts +1 -1
- package/dist/hooks/use-row-swipe.js +1 -6
- package/dist/hooks/use-row-swipe.js.map +1 -1
- package/dist/i18n/defaults.js +1 -4
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +4 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.js +2 -8
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.js.map +1 -1
- package/dist/i18n/locales/en.js +4 -1
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +4 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +4 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +4 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +4 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/plurals.js.map +1 -1
- package/dist/i18n/review.js +13 -7
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +16 -16
- package/dist/index.js +40 -23
- package/dist/index.js.map +1 -1
- package/dist/landing/access.js +1 -4
- package/dist/landing/access.js.map +1 -1
- package/dist/landing/landing-sections.js +35 -38
- package/dist/landing/landing-sections.js.map +1 -1
- package/dist/landing/page-seo.js.map +1 -1
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/clipboard-minus.js.map +1 -1
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/decimal-marks.js.map +1 -1
- package/dist/lib/device-settings.js +2 -1
- package/dist/lib/device-settings.js.map +1 -1
- package/dist/lib/direction.js.map +1 -1
- package/dist/lib/iban.js.map +1 -1
- package/dist/lib/logger.js +1 -5
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/person-name.js.map +1 -1
- package/dist/lib/phone.js.map +1 -1
- package/dist/lib/qr-encode.js +344 -8
- package/dist/lib/qr-encode.js.map +1 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/table-text.js +15 -3
- package/dist/lib/table-text.js.map +1 -1
- package/dist/lib/text-match.d.ts +4 -0
- package/dist/lib/text-match.js.map +1 -1
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/notifications/notification-inbox.js +63 -44
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-parts.js.map +1 -1
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-device-setting.js +6 -1
- package/dist/notifications/push-device-setting.js.map +1 -1
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/push-worker.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/use-push-device.js +4 -1
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/pwa-update-prompt.d.ts +1 -1
- package/dist/offline/pwa-update-prompt.js +7 -1
- package/dist/offline/pwa-update-prompt.js.map +1 -1
- package/dist/offline/query-persistence.js.map +1 -1
- package/dist/offline.d.ts +1 -1
- package/dist/offline.js +6 -1
- package/dist/offline.js.map +1 -1
- package/dist/pay/pay.css +8 -1
- package/dist/rhf/fields.d.ts +1 -2
- package/dist/rhf/fields.js +1 -10
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form-core.js +1 -4
- package/dist/rhf/form-core.js.map +1 -1
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
- package/dist/search/command-palette.js +71 -62
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.js +34 -5
- package/dist/search/global-search.js.map +1 -1
- package/dist/search/search-index.js.map +1 -1
- package/dist/settings/device-reset-setting.js +1 -6
- package/dist/settings/device-reset-setting.js.map +1 -1
- package/dist/settings/settings-heading.js.map +1 -1
- package/dist/settings/settings-layout.js +9 -20
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/settings/use-settings-route.js.map +1 -1
- package/dist/shell/app-shell.js +62 -31
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.js +1 -10
- package/dist/shell/auth-layout.js.map +1 -1
- package/dist/shell/dev-menu.js +12 -6
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/sync-status-indicator.js +7 -6
- package/dist/shell/sync-status-indicator.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.js +1 -10
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell/tour-menu.js +9 -1
- package/dist/shell/tour-menu.js.map +1 -1
- package/dist/sync/db.js.map +1 -1
- package/dist/sync/engine.js +24 -6
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync.d.ts +2 -2
- package/dist/sync.js +7 -1
- package/dist/sync.js.map +1 -1
- package/dist/testing/css-variables.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/react.js.map +1 -1
- package/dist/testing/viewport.js +7 -4
- package/dist/testing/viewport.js.map +1 -1
- package/dist/theme/contrast-tokens.js +13 -2
- package/dist/theme/contrast-tokens.js.map +1 -1
- package/dist/theme/contrast.d.ts +2 -2
- package/dist/theme/palette-derive.d.ts +1 -1
- package/dist/theme/palette-derive.js +18 -29
- package/dist/theme/palette-derive.js.map +1 -1
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +1 -3
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour-dock.js +5 -1
- package/dist/tour/tour-dock.js.map +1 -1
- package/dist/tour/tour-engine.js +1 -4
- package/dist/tour/tour-engine.js.map +1 -1
- package/dist/tour/tour.js +21 -12
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/dist/wizard/stepper-nav.js +3 -24
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +24 -28
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.js.map +1 -1
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +1 -1
- package/dist/wizard/wizard-step.js +1 -8
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +5 -2
- package/src/account/account-settings-labels.ts +1 -2
- package/src/account/account-settings.tsx +11 -17
- package/src/account/delete-account-setting.tsx +1 -6
- package/src/account/email-change-setting-labels.ts +1 -2
- package/src/account/email-change-setting.tsx +3 -1
- package/src/account/passkeys-setting.tsx +1 -3
- package/src/account/sessions-setting.tsx +4 -2
- package/src/admin/admin-action-confirm.tsx +4 -7
- package/src/admin/admin-action-log.tsx +29 -29
- package/src/admin/admin-area-labels.ts +4 -1
- package/src/admin/admin-overview.tsx +8 -2
- package/src/admin/admin-sections.tsx +3 -1
- package/src/admin/admin-tool-links.tsx +7 -1
- package/src/admin/invitations-panel.tsx +5 -11
- package/src/admin/mail-test-card.tsx +1 -2
- package/src/admin/reviewer-scope-editor.tsx +4 -9
- package/src/admin/role-select.tsx +4 -2
- package/src/admin/user-roster.tsx +2 -11
- package/src/auth/accept-invitation.tsx +1 -7
- package/src/auth/auth-errors.ts +1 -6
- package/src/auth/forgot-password-form.tsx +2 -7
- package/src/auth/register-form.tsx +7 -8
- package/src/auth/reset-password-form.tsx +2 -5
- package/src/auth/sign-in-form.tsx +15 -14
- package/src/auth/verify-email.tsx +2 -7
- package/src/billing/billing-banner.tsx +3 -7
- package/src/billing/billing-labels.ts +6 -4
- package/src/billing/checkout-processing.ts +0 -1
- package/src/billing/plan-card.tsx +9 -8
- package/src/billing/plan-limit-toast.tsx +1 -2
- package/src/billing/plan-limit.ts +1 -2
- package/src/billing/plan-price.ts +1 -4
- package/src/billing/plan-switch.tsx +1 -2
- package/src/billing/subscription-actions.tsx +5 -2
- package/src/components/account-chips.tsx +1 -7
- package/src/components/alert-banner.tsx +2 -8
- package/src/components/amount-input.tsx +206 -178
- package/src/components/authed-image.tsx +7 -4
- package/src/components/autocomplete.tsx +7 -9
- package/src/components/bulk-action-bar.tsx +2 -7
- package/src/components/button-group.tsx +1 -2
- package/src/components/button-look.ts +6 -17
- package/src/components/button.tsx +1 -4
- package/src/components/calculator.tsx +6 -9
- package/src/components/calendar-heatmap.tsx +5 -10
- package/src/components/card.tsx +9 -13
- package/src/components/chart-zoom-core.tsx +6 -33
- package/src/components/chart.tsx +8 -31
- package/src/components/checkbox-group.tsx +1 -4
- package/src/components/chip-input.tsx +6 -6
- package/src/components/chip.tsx +14 -26
- package/src/components/choice-card.tsx +4 -18
- package/src/components/column-mapper-labels.ts +3 -10
- package/src/components/column-mapper.tsx +33 -122
- package/src/components/combobox-core.tsx +12 -42
- package/src/components/combobox.tsx +2 -6
- package/src/components/confirm-dialog.tsx +1 -3
- package/src/components/copy-button.tsx +2 -4
- package/src/components/country-select.tsx +9 -21
- package/src/components/currency-option-list.tsx +13 -14
- package/src/components/currency-select.tsx +2 -7
- package/src/components/danger-confirm.tsx +8 -9
- package/src/components/data-table-cells.tsx +1 -2
- package/src/components/data-table-column-settings.tsx +1 -3
- package/src/components/data-table-filter-popover.tsx +5 -19
- package/src/components/data-table-filters.ts +1 -6
- package/src/components/data-table-fit.ts +1 -3
- package/src/components/data-table-groups.tsx +3 -1
- package/src/components/data-table-head.tsx +27 -45
- package/src/components/data-table-mobile.tsx +99 -108
- package/src/components/data-table-pagination.tsx +2 -6
- package/src/components/data-table-row-actions.tsx +25 -24
- package/src/components/data-table-row.tsx +16 -17
- package/src/components/data-table-shared.tsx +10 -16
- package/src/components/data-table-sort.ts +1 -2
- package/src/components/data-table-virtual.ts +9 -4
- package/src/components/data-table.tsx +489 -501
- package/src/components/date-picker.tsx +1 -7
- package/src/components/date-range-picker.tsx +3 -13
- package/src/components/description-list.tsx +7 -13
- package/src/components/dialog-frame.tsx +12 -3
- package/src/components/disclosure.tsx +8 -12
- package/src/components/dropdown.tsx +3 -11
- package/src/components/empty-state.tsx +1 -3
- package/src/components/entity-combobox.tsx +2 -6
- package/src/components/error-boundary.tsx +7 -7
- package/src/components/field-base.tsx +8 -14
- package/src/components/field-parts.tsx +12 -23
- package/src/components/field-sync.tsx +7 -11
- package/src/components/file-button.tsx +7 -3
- package/src/components/file-dropzone.tsx +7 -12
- package/src/components/floating-panel.tsx +44 -51
- package/src/components/focus-ring.ts +1 -2
- package/src/components/form-actions.tsx +1 -4
- package/src/components/grouped-picker.tsx +6 -8
- package/src/components/hover-menu.tsx +1 -4
- package/src/components/iban-input-labels.ts +1 -2
- package/src/components/icon-button.tsx +46 -48
- package/src/components/icon-picker.tsx +4 -17
- package/src/components/image-grid.tsx +1 -6
- package/src/components/inline-edit-field.tsx +1 -5
- package/src/components/inline-entity-combobox.tsx +6 -21
- package/src/components/input.tsx +14 -2
- package/src/components/jump-calendar.tsx +2 -6
- package/src/components/legal-labels.ts +1 -2
- package/src/components/legal-page.tsx +18 -7
- package/src/components/legal.tsx +14 -5
- package/src/components/lightbox.tsx +1 -3
- package/src/components/line-items.tsx +7 -15
- package/src/components/list.tsx +29 -25
- package/src/components/measured-grid.tsx +8 -30
- package/src/components/menu-item.tsx +1 -3
- package/src/components/mini-calendar.tsx +129 -159
- package/src/components/money-field.tsx +4 -2
- package/src/components/month-picker.tsx +14 -21
- package/src/components/multi-entity-combobox.tsx +6 -26
- package/src/components/multi-select.tsx +5 -9
- package/src/components/nav-pills.tsx +10 -2
- package/src/components/number-field.tsx +18 -15
- package/src/components/number-input.tsx +3 -19
- package/src/components/numpad-sheet.tsx +9 -13
- package/src/components/one-time-code-input.tsx +6 -11
- package/src/components/page-contents.tsx +2 -9
- package/src/components/page-header.tsx +5 -10
- package/src/components/password-strength-labels.ts +1 -2
- package/src/components/password-strength.tsx +6 -9
- package/src/components/phone-input.tsx +2 -13
- package/src/components/picker-sheet.tsx +2 -5
- package/src/components/pie-chart.tsx +6 -12
- package/src/components/popover.tsx +2 -1
- package/src/components/progress-bar.tsx +16 -25
- package/src/components/qr-code.tsx +2 -9
- package/src/components/row-actions.tsx +9 -1
- package/src/components/search-field.tsx +4 -5
- package/src/components/select.tsx +2 -10
- package/src/components/separator.tsx +1 -5
- package/src/components/series-chart-axes.ts +5 -5
- package/src/components/series-chart-budget.ts +1 -4
- package/src/components/series-chart-ticks.ts +1 -4
- package/src/components/series-chart.tsx +43 -83
- package/src/components/settings-fields.tsx +20 -15
- package/src/components/share-card.tsx +6 -14
- package/src/components/sign-chip.tsx +16 -16
- package/src/components/signature-pad.tsx +21 -13
- package/src/components/signed-amount.tsx +6 -14
- package/src/components/skeleton.tsx +2 -13
- package/src/components/slider.tsx +9 -20
- package/src/components/sparkline.tsx +2 -13
- package/src/components/stat-tile.tsx +13 -24
- package/src/components/swatch-picker.tsx +8 -3
- package/src/components/swipeable-row.tsx +2 -11
- package/src/components/table-skeleton.tsx +4 -1
- package/src/components/table.tsx +12 -8
- package/src/components/tabs.tsx +24 -39
- package/src/components/text-link.tsx +6 -6
- package/src/components/text.tsx +1 -2
- package/src/components/tile-radio.tsx +6 -19
- package/src/components/time-input.tsx +4 -5
- package/src/components/toast.tsx +3 -12
- package/src/components/toggle-group.tsx +9 -9
- package/src/components/toggle-legend.tsx +4 -16
- package/src/components/tooltip.tsx +9 -48
- package/src/components/translation-review-editor.tsx +4 -4
- package/src/components/translation-review-parts.tsx +7 -1
- package/src/components/translation-review.tsx +15 -16
- package/src/components/tree-view.tsx +17 -20
- package/src/components/treemap.tsx +59 -9
- package/src/components/trigger-aria.ts +1 -4
- package/src/components/use-table-state.ts +16 -17
- package/src/components/user-avatar.tsx +4 -21
- package/src/demo/demo-start.tsx +8 -4
- package/src/feedback/feedback-attachment-labels.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +16 -29
- package/src/feedback/feedback-capture.ts +3 -1
- package/src/feedback/feedback-comment-thread.tsx +9 -6
- package/src/feedback/feedback-context.tsx +1 -4
- package/src/feedback/feedback-dialog.tsx +3 -4
- package/src/feedback/feedback-inbox.tsx +3 -9
- package/src/feedback/feedback-public-list.tsx +7 -1
- package/src/feedback/feedback-record.ts +6 -14
- package/src/feedback/feedback-replies.tsx +10 -5
- package/src/feedback/feedback-row-detail.tsx +2 -5
- package/src/feedback/feedback-status-undo.ts +1 -4
- package/src/feedback/feedback-submit-dialog.tsx +4 -4
- package/src/feedback/feedback-table.tsx +12 -8
- package/src/feedback/feedback-thread.tsx +4 -16
- package/src/feedback/feedback-vocabulary.ts +72 -82
- package/src/feedback/feedback-vote-button.tsx +9 -8
- package/src/hooks/use-anchored-rect.ts +1 -4
- package/src/hooks/use-document-title.ts +3 -1
- package/src/hooks/use-file-drop.ts +1 -3
- package/src/hooks/use-file-picker-labels.ts +1 -2
- package/src/hooks/use-file-picker.tsx +1 -5
- package/src/hooks/use-focus-trap.ts +1 -2
- package/src/hooks/use-overlay-history.ts +10 -5
- package/src/hooks/use-row-swipe.ts +1 -6
- package/src/i18n/defaults.ts +1 -4
- package/src/i18n/german.ts +33 -49
- package/src/i18n/kit-labels.tsx +5 -20
- package/src/i18n/languages.ts +2 -8
- package/src/i18n/locales/en.ts +11 -31
- package/src/i18n/locales/es.ts +35 -49
- package/src/i18n/locales/fr.ts +34 -51
- package/src/i18n/locales/hu.ts +26 -37
- package/src/i18n/locales/it.ts +35 -52
- package/src/i18n/locales/zh.ts +14 -32
- package/src/i18n/plurals.ts +4 -1
- package/src/i18n/review.ts +18 -7
- package/src/i18n/shared-labels.ts +1 -2
- package/src/index.ts +106 -32
- package/src/landing/access.ts +2 -7
- package/src/landing/landing-sections.tsx +14 -15
- package/src/landing/page-seo.ts +1 -3
- package/src/lib/calc.ts +4 -1
- package/src/lib/clipboard-minus.ts +4 -1
- package/src/lib/dates.ts +2 -10
- package/src/lib/decimal-marks.ts +1 -3
- package/src/lib/device-settings.ts +2 -1
- package/src/lib/direction.ts +1 -3
- package/src/lib/iban.ts +89 -9
- package/src/lib/logger.ts +1 -5
- package/src/lib/person-name.ts +1 -3
- package/src/lib/phone.ts +5 -8
- package/src/lib/qr-encode.ts +32 -8
- package/src/lib/session-refresh.ts +5 -6
- package/src/lib/table-text.ts +24 -6
- package/src/lib/text-match.ts +15 -3
- package/src/lib/translation-review.ts +1 -3
- package/src/notifications/notification-inbox.tsx +17 -4
- package/src/notifications/notification-parts.tsx +1 -2
- package/src/notifications/notification-types-setting.tsx +19 -18
- package/src/notifications/notifications-labels.ts +1 -2
- package/src/notifications/push-device-setting.tsx +7 -1
- package/src/notifications/push-devices-setting.tsx +7 -3
- package/src/notifications/push-worker.ts +10 -2
- package/src/notifications/quiet-hours-setting.tsx +4 -1
- package/src/notifications/use-push-device.ts +4 -1
- package/src/offline/pwa-update-prompt.tsx +7 -1
- package/src/offline/query-persistence.ts +9 -6
- package/src/offline.ts +6 -1
- package/src/rhf/fields.tsx +50 -19
- package/src/rhf/form-core.tsx +2 -6
- package/src/rhf/line-items.tsx +5 -7
- package/src/rhf/use-rhf-wizard-step.ts +3 -12
- package/src/search/command-palette.tsx +15 -23
- package/src/search/global-search.tsx +38 -8
- package/src/search/search-index.ts +5 -1
- package/src/settings/device-reset-setting.tsx +5 -8
- package/src/settings/settings-heading.tsx +3 -1
- package/src/settings/settings-layout.tsx +6 -13
- package/src/settings/use-settings-route.ts +4 -3
- package/src/shell/app-shell.tsx +48 -24
- package/src/shell/auth-layout.tsx +1 -6
- package/src/shell/dev-menu.tsx +12 -6
- package/src/shell/option-switcher-menu.tsx +1 -3
- package/src/shell/sync-status-indicator.tsx +14 -12
- package/src/shell/top-bar-parts-labels.ts +1 -3
- package/src/shell/topbar-action-menu.tsx +9 -8
- package/src/shell/topbar-controls.tsx +5 -19
- package/src/shell/tour-menu.tsx +9 -1
- package/src/sync/db.ts +5 -1
- package/src/sync/engine.ts +44 -12
- package/src/sync.ts +16 -2
- package/src/testing/css-variables.ts +1 -2
- package/src/testing/index.ts +1 -6
- package/src/testing/react.ts +1 -2
- package/src/testing/viewport.ts +9 -9
- package/src/theme/contrast-tokens.ts +13 -2
- package/src/theme/palette-derive.ts +24 -50
- package/src/theme/palette-presets.ts +0 -1
- package/src/theme/palette-store.ts +1 -4
- package/src/theme/text-size.ts +1 -1
- package/src/theme/theme-store.ts +2 -6
- package/src/tour/tour-catalogue.tsx +4 -2
- package/src/tour/tour-dock.ts +5 -1
- package/src/tour/tour-engine.ts +2 -6
- package/src/tour/tour.tsx +28 -12
- package/src/tour/use-tour-progress.ts +8 -2
- package/src/tour/use-tour-url-sync.ts +3 -1
- package/src/vite-env.d.ts +1 -4
- package/src/wizard/stepper-nav.tsx +10 -35
- package/src/wizard/types.ts +1 -4
- package/src/wizard/use-wizard.ts +28 -43
- package/src/wizard/validation.ts +1 -5
- package/src/wizard/wizard-context.tsx +2 -6
- package/src/wizard/wizard-step.tsx +1 -8
- package/src/wizard/wizard-summary.tsx +2 -6
- package/tokens.css +393 -152
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/billing/checkout-processing.ts"],"sourcesContent":["import { useCallback, useEffect, useEffectEvent, useLayoutEffect, useRef, useSyncExternalStore } from \"react\";\n\nimport { toDate } from \"../lib/format\";\nimport type { DateInput } from \"../lib/format\";\nimport { readStored, removeStored, writeStored } from \"../lib/safe-storage\";\nimport type { PaidPrice } from \"./billing-overview\";\nimport type { BillingStanding } from \"./billing-standing\";\n\n/**\n * \"Payment processing\" after the way back from a checkout (docs/billing-harmonization.md\n * §12.21, §14.4; keksdose's `features/billing/checkout-return.ts`, made router-agnostic).\n *\n * The provider tells the app about a payment by WEBHOOK, and the webhook may land after\n * the buyer is back: Cloud Run scales to zero, and a cold start can outlast the\n * provider's redirect. Without this, someone who has just paid comes back to a page that\n * still says their trial has ended. So the return is recognised (`?checkout=done`,\n * `isCheckoutReturn`), the page says \"your payment is being processed\"\n * (`BillingBanner kind=\"processing\"`), and the overview is asked again every few seconds\n * until it has moved.\n *\n * **\"Landed\" is judged against the overview at departure** ({@link checkoutFingerprint},\n * noted by {@link noteCheckoutStarted}), not by \"is it `active` yet\": a trial and a beta\n * grant can both buy, and a running free grant keeps its status whatever the provider\n * says (§12.11) — only its period end moves — so \"active\" alone would wait for ever on\n * one of them. A return with no departure on record (another tab, another device) waits\n * for `active`. kastlan's 60-second poll for `source === \"provider\"` is the case this\n * replaces: a payer who bought under a grant never matched it.\n *\n * **Bounded:** after `timeoutMs` (10 minutes) the device stops asking, and the page says\n * what the overview says, whatever that is — a checkout abandoned at the provider must\n * not leave \"processing\" up for good.\n *\n * **Remembered across the whole-page return, per payer.** The departure goes into the\n * browser's storage through the kit's safe storage (a throwing storage — Safari's\n * private mode — keeps it in memory for this page instead), under `storageKey`; a mark\n * names its payer, so it never speaks for another account on a shared device. It is a\n * browser entry on the app's origin: an app whose privacy text lists its entries names\n * it (Kurvenschmiede passes its own key).\n */\n\n/** The overview fields a completed checkout changes (wire names, `GET /billing/overview`).\n * Pass the overview as it came; the other fields are ignored. */\nexport type CheckoutOverview = Pick<BillingStanding, \"status\" | \"in_good_standing\"> & {\n plan: string;\n source: string;\n current_period_end?: DateInput;\n /** 0.39 (§15.2.10): what the payer pays — a plan switch moves only this under a grant,\n * and keeps `active` without one. Left out or null: nothing paid. */\n paid?: Pick<PaidPrice, \"plan\" | \"interval\"> | null;\n};\n\n/** How long a return keeps the page in \"processing\" before it trusts the overview. */\nconst PROCESSING_FOR = 10 * 60_000;\n/** How often the overview is asked meanwhile. */\nconst POLL_EVERY = 4_000;\n/** The browser entry, unless the app names its own. */\nconst STORAGE_KEY = \"eifi1-billing-checkout\";\n\n/**\n * What of the overview a completed checkout — or plan switch — changes, as one string:\n * the plan (read without case, §12.16), the status, the source, the standing, the\n * period's end (read as an instant, so `…Z` and `…+00:00` agree) and, since 0.39, the\n * paid plan and period (`paid`, §15.2.10). A switch keeps `active`, and under a grant\n * the plan too, so only `paid` shows it landed: one storage key serves the checkout and\n * the switch.\n */\nexport function checkoutFingerprint(o: CheckoutOverview): string {\n const end = toDate(o.current_period_end ?? null);\n return [\n String(o.plan ?? \"\").toLowerCase(),\n o.status,\n o.source,\n String(o.in_good_standing),\n end ? end.toISOString() : \"\",\n String(o.paid?.plan ?? \"\").toLowerCase(),\n o.paid?.interval ?? \"\",\n ].join(\"|\");\n}\n\n/**\n * Whether the overview `now` shows the checkout: it differs from the fingerprint taken\n * at departure (`before`), or — with no departure on record — it is `active`.\n */\nexport function checkoutLanded(before: string | null, now: CheckoutOverview): boolean {\n return before === null ? now.status === \"active\" : checkoutFingerprint(now) !== before;\n}\n\n/* ── The store: one per storage key, shared by every mounted instance ─────── */\n\ninterface PendingCheckout {\n /** Whose checkout: a user's or a company's id, as a string. */\n payer: string;\n /** The overview's fingerprint at departure, or null when it was not known. */\n before: string | null;\n /** When the buyer came back; null while they are (or were) at the provider. */\n returnedAt: number | null;\n}\n\n/** One mounted hook: what the shared timers call, kept current on every render. */\ninterface Instance {\n payer: string | null;\n refetch: () => unknown;\n onLanded: (() => void) | undefined;\n timeoutMs: number;\n pollMs: number;\n}\n\ninterface CheckoutStore {\n key: string;\n pending: PendingCheckout | null;\n listeners: Set<() => void>;\n /** The mounted instances, in mount order: the last is the latest. */\n instances: Instance[];\n poll: ReturnType<typeof setInterval> | null;\n pollEvery: number;\n expiry: ReturnType<typeof setTimeout> | null;\n expiryAt: number | null;\n /** An instance consumed the marker in this commit; the others leave it be. */\n consuming: boolean;\n}\n\n/** Module-level, so the banner's instance and the subscription page's share one state,\n * one poll and one landing (keksdose mounts the hook twice, §14.4). */\nconst stores = new Map<string, CheckoutStore>();\n\nfunction readPending(key: string): PendingCheckout | null {\n const raw = readStored(key);\n if (raw === null) return null;\n try {\n const value = JSON.parse(raw) as Partial<PendingCheckout> | null;\n if (\n value &&\n typeof value.payer === \"string\" &&\n (value.before === null || typeof value.before === \"string\") &&\n (value.returnedAt === null || typeof value.returnedAt === \"number\")\n ) {\n return { payer: value.payer, before: value.before, returnedAt: value.returnedAt };\n }\n } catch {\n // Not ours, or damaged: as if nothing was noted.\n }\n return null;\n}\n\n\nfunction storeOf(key: string): CheckoutStore {\n let store = stores.get(key);\n if (!store) {\n store = {\n key,\n pending: readPending(key),\n listeners: new Set(),\n instances: [],\n poll: null,\n pollEvery: 0,\n expiry: null,\n expiryAt: null,\n consuming: false,\n };\n stores.set(key, store);\n }\n return store;\n}\n\nfunction setPending(store: CheckoutStore, next: PendingCheckout | null): void {\n store.pending = next;\n if (next) writeStored(store.key, JSON.stringify(next));\n else removeStored(store.key);\n for (const listener of store.listeners) listener();\n sync(store);\n}\n\n/** The latest mounted instance's refetch, failures swallowed: a poll that fails is\n * simply asked again at the next tick. */\nfunction refetchLatest(store: CheckoutStore): void {\n const latest = store.instances.at(-1);\n if (!latest) return;\n try {\n const result = latest.refetch() as PromiseLike<unknown> | undefined;\n if (result && typeof result.then === \"function\") result.then(undefined, () => {});\n } catch {\n // as above\n }\n}\n\n/** Arms or stops the store's two timers — the bound and the poll — for its state. */\nfunction sync(store: CheckoutStore): void {\n const pending = store.pending;\n const latest = store.instances.at(-1);\n const returnedAt = pending?.returnedAt ?? null;\n\n if (returnedAt === null) {\n if (store.expiry) clearTimeout(store.expiry);\n store.expiry = null;\n store.expiryAt = null;\n } else {\n const at = returnedAt + (latest?.timeoutMs ?? PROCESSING_FOR);\n if (at <= Date.now()) {\n setPending(store, null);\n return;\n }\n if (store.expiryAt !== at) {\n if (store.expiry) clearTimeout(store.expiry);\n store.expiryAt = at;\n store.expiry = setTimeout(() => {\n store.expiry = null;\n store.expiryAt = null;\n if (store.pending === pending) setPending(store, null);\n }, at - Date.now());\n }\n }\n\n // One poll for every instance, while a mounted one is the mark's payer.\n const watching = returnedAt !== null && store.instances.some((i) => i.payer === pending!.payer);\n const every = latest?.pollMs ?? POLL_EVERY;\n if (!watching) {\n if (store.poll) clearInterval(store.poll);\n store.poll = null;\n } else if (!store.poll || store.pollEvery !== every) {\n if (store.poll) clearInterval(store.poll);\n store.pollEvery = every;\n store.poll = setInterval(() => refetchLatest(store), every);\n }\n}\n\n/** A mounted instance joins the store's timers; the latest is the one they call. */\nfunction register(store: CheckoutStore, instance: Instance): () => void {\n store.instances.push(instance);\n return () => {\n store.instances = store.instances.filter((i) => i !== instance);\n sync(store);\n };\n}\n\n/**\n * Whether this instance is the one to consume a return marker: the first in a commit is.\n * Another instance in the same commit sees the same marker and leaves it be; the claim\n * lapses once the commit's effects have run.\n */\nfunction claimReturn(store: CheckoutStore): boolean {\n if (store.consuming) return false;\n store.consuming = true;\n void Promise.resolve().then(() => {\n store.consuming = false;\n });\n return true;\n}\n\n/**\n * Note the departure for a checkout — just before the page leaves for the provider (or\n * for the pay page, `payPageUrl(url, locale)`):\n *\n * noteCheckoutStarted(user.id, overview.data);\n * window.location.assign(payPageUrl(url, i18n.language));\n *\n * `payer` is the user's or the company's id: a mark never speaks for another payer.\n * `overview` is what the page shows now; left out, the return waits for `active`. Pass\n * the same `storageKey` as the hook's.\n *\n * A plan switch (0.39, §15.2.10) is waited for the same way, under the same key: note\n * the departure before `POST /billing/plan`, and once it answers put the return marker\n * on the page's URL yourself (`checkoutReturnUrl`'s `?checkout=done`) — the row follows\n * by webhook, and the fingerprint's `paid` shows it landed.\n */\nexport function noteCheckoutStarted(\n payer: string | number,\n overview?: CheckoutOverview,\n opts: { storageKey?: string } = {},\n): void {\n const store = storeOf(opts.storageKey ?? STORAGE_KEY);\n setPending(store, {\n payer: String(payer),\n before: overview ? checkoutFingerprint(overview) : null,\n returnedAt: null,\n });\n}\n\nexport interface CheckoutProcessingOptions {\n /** The signed-in payer — the user's or the company's id; null while unknown (the\n * marker then waits in the URL). */\n payer: string | number | null;\n /** The page was reached as the way back: `isCheckoutReturn(location.search)`. */\n returned: boolean;\n /** Drop the marker, with replace — `setSearchParams((p) => withoutCheckoutReturn(p),\n * { replace: true })`. Called once per return, by the first instance that sees it. */\n onConsumed: () => void;\n /** The overview query's data, judged here against the departure. */\n overview: CheckoutOverview | undefined;\n /** The overview query's refetch: the hook polls with it — one timer for every\n * instance, with the latest mounted one's — and \"Check again\" calls it. */\n refetch: () => unknown;\n /** Once per landing: refresh what a new standing changes — the plans, the items whose\n * `locked` may have lifted. The overview itself is already fresh. */\n onLanded?: () => void;\n /** How long a return keeps \"processing\". Default 600 000 (10 minutes). */\n timeoutMs?: number;\n /** How often the overview is asked meanwhile. Default 4 000. */\n pollMs?: number;\n /** The browser entry. Default `\"eifi1-billing-checkout\"`. */\n storageKey?: string;\n}\n\n/**\n * Whether this payer is back from a checkout whose payment the overview has not shown\n * yet — and the \"Check again\" for the banner that says so:\n *\n * const [params, setParams] = useSearchParams();\n * const overview = useQuery(billingOverviewQuery);\n * const { processing, checkAgain } = useCheckoutProcessing({\n * payer: user?.id ?? null,\n * returned: isCheckoutReturn(params),\n * onConsumed: () => setParams((p) => withoutCheckoutReturn(p), { replace: true }),\n * overview: overview.data,\n * refetch: overview.refetch,\n * onLanded: () => void qc.invalidateQueries({ queryKey: [\"budgets\"] }),\n * });\n * processing && <BillingBanner kind=\"processing\" onAction={checkAgain} />\n *\n * The kit has no router: the app says whether the URL returned and how to drop the\n * marker. Mount it wherever the return lands and the banner shows — every instance with\n * the same `storageKey` shares one state: the first that sees the marker consumes it,\n * one timer polls, and `onLanded` runs once. The app's `refetchOnWindowFocus` doesn't\n * matter.\n */\nexport function useCheckoutProcessing(options: CheckoutProcessingOptions): {\n processing: boolean;\n checkAgain: () => void;\n} {\n const {\n payer,\n returned,\n onConsumed,\n overview,\n refetch,\n onLanded,\n timeoutMs = PROCESSING_FOR,\n pollMs = POLL_EVERY,\n storageKey = STORAGE_KEY,\n } = options;\n const payerKey = payer === null || payer === undefined ? null : String(payer);\n const store = storeOf(storageKey);\n const subscribe = useCallback(\n (notify: () => void) => {\n store.listeners.add(notify);\n return () => {\n store.listeners.delete(notify);\n };\n },\n [store],\n );\n const snapshot = () => store.pending;\n const pending = useSyncExternalStore(subscribe, snapshot, snapshot);\n const processing = payerKey !== null && pending?.payer === payerKey && pending.returnedAt !== null;\n\n // This instance, as the shared timers see it. Registered before paint, so a mark that\n // ran out while the page was closed is dropped before \"processing\" can flash.\n const instanceRef = useRef<Instance>({ payer: payerKey, refetch, onLanded, timeoutMs, pollMs });\n useLayoutEffect(() => register(store, instanceRef.current), [store]);\n useLayoutEffect(() => {\n Object.assign(instanceRef.current, { payer: payerKey, refetch, onLanded, timeoutMs, pollMs });\n sync(store);\n });\n\n // The return: note it, then drop the marker so a reload does not count it twice.\n const consumed = useEffectEvent(() => onConsumed());\n useEffect(() => {\n if (!returned || payerKey === null || !claimReturn(store)) return;\n const before = store.pending?.payer === payerKey ? store.pending.before : null;\n setPending(store, { payer: payerKey, before, returnedAt: Date.now() });\n consumed();\n }, [returned, payerKey, store]);\n\n // Caught up: the overview has moved since the departure. Judged against the store as\n // it is NOW, so a second instance in the same commit finds the mark gone.\n useEffect(() => {\n if (!processing || !overview) return;\n const live = store.pending;\n if (!live || live.payer !== payerKey || live.returnedAt === null) return;\n if (!checkoutLanded(live.before, overview)) return;\n setPending(store, null);\n const latest = [...store.instances].reverse().find((i) => i.onLanded);\n latest?.onLanded?.();\n }, [processing, overview, payerKey, store]);\n\n const checkAgain = useCallback(() => {\n try {\n const result = instanceRef.current.refetch() as PromiseLike<unknown> | undefined;\n if (result && typeof result.then === \"function\") result.then(undefined, () => {});\n } catch {\n // The banner stays; the next poll asks again.\n }\n }, []);\n\n return { processing, checkAgain };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/billing/checkout-processing.ts"],"sourcesContent":["import { useCallback, useEffect, useEffectEvent, useLayoutEffect, useRef, useSyncExternalStore } from \"react\";\n\nimport { toDate } from \"../lib/format\";\nimport type { DateInput } from \"../lib/format\";\nimport { readStored, removeStored, writeStored } from \"../lib/safe-storage\";\nimport type { PaidPrice } from \"./billing-overview\";\nimport type { BillingStanding } from \"./billing-standing\";\n\n/**\n * \"Payment processing\" after the way back from a checkout (docs/billing-harmonization.md\n * §12.21, §14.4; keksdose's `features/billing/checkout-return.ts`, made router-agnostic).\n *\n * The provider tells the app about a payment by WEBHOOK, and the webhook may land after\n * the buyer is back: Cloud Run scales to zero, and a cold start can outlast the\n * provider's redirect. Without this, someone who has just paid comes back to a page that\n * still says their trial has ended. So the return is recognised (`?checkout=done`,\n * `isCheckoutReturn`), the page says \"your payment is being processed\"\n * (`BillingBanner kind=\"processing\"`), and the overview is asked again every few seconds\n * until it has moved.\n *\n * **\"Landed\" is judged against the overview at departure** ({@link checkoutFingerprint},\n * noted by {@link noteCheckoutStarted}), not by \"is it `active` yet\": a trial and a beta\n * grant can both buy, and a running free grant keeps its status whatever the provider\n * says (§12.11) — only its period end moves — so \"active\" alone would wait for ever on\n * one of them. A return with no departure on record (another tab, another device) waits\n * for `active`. kastlan's 60-second poll for `source === \"provider\"` is the case this\n * replaces: a payer who bought under a grant never matched it.\n *\n * **Bounded:** after `timeoutMs` (10 minutes) the device stops asking, and the page says\n * what the overview says, whatever that is — a checkout abandoned at the provider must\n * not leave \"processing\" up for good.\n *\n * **Remembered across the whole-page return, per payer.** The departure goes into the\n * browser's storage through the kit's safe storage (a throwing storage — Safari's\n * private mode — keeps it in memory for this page instead), under `storageKey`; a mark\n * names its payer, so it never speaks for another account on a shared device. It is a\n * browser entry on the app's origin: an app whose privacy text lists its entries names\n * it (Kurvenschmiede passes its own key).\n */\n\n/** The overview fields a completed checkout changes (wire names, `GET /billing/overview`).\n * Pass the overview as it came; the other fields are ignored. */\nexport type CheckoutOverview = Pick<BillingStanding, \"status\" | \"in_good_standing\"> & {\n plan: string;\n source: string;\n current_period_end?: DateInput;\n /** 0.39 (§15.2.10): what the payer pays — a plan switch moves only this under a grant,\n * and keeps `active` without one. Left out or null: nothing paid. */\n paid?: Pick<PaidPrice, \"plan\" | \"interval\"> | null;\n};\n\n/** How long a return keeps the page in \"processing\" before it trusts the overview. */\nconst PROCESSING_FOR = 10 * 60_000;\n/** How often the overview is asked meanwhile. */\nconst POLL_EVERY = 4_000;\n/** The browser entry, unless the app names its own. */\nconst STORAGE_KEY = \"eifi1-billing-checkout\";\n\n/**\n * What of the overview a completed checkout — or plan switch — changes, as one string:\n * the plan (read without case, §12.16), the status, the source, the standing, the\n * period's end (read as an instant, so `…Z` and `…+00:00` agree) and, since 0.39, the\n * paid plan and period (`paid`, §15.2.10). A switch keeps `active`, and under a grant\n * the plan too, so only `paid` shows it landed: one storage key serves the checkout and\n * the switch.\n */\nexport function checkoutFingerprint(o: CheckoutOverview): string {\n const end = toDate(o.current_period_end ?? null);\n return [\n String(o.plan ?? \"\").toLowerCase(),\n o.status,\n o.source,\n String(o.in_good_standing),\n end ? end.toISOString() : \"\",\n String(o.paid?.plan ?? \"\").toLowerCase(),\n o.paid?.interval ?? \"\",\n ].join(\"|\");\n}\n\n/**\n * Whether the overview `now` shows the checkout: it differs from the fingerprint taken\n * at departure (`before`), or — with no departure on record — it is `active`.\n */\nexport function checkoutLanded(before: string | null, now: CheckoutOverview): boolean {\n return before === null ? now.status === \"active\" : checkoutFingerprint(now) !== before;\n}\n\n/* ── The store: one per storage key, shared by every mounted instance ─────── */\n\ninterface PendingCheckout {\n /** Whose checkout: a user's or a company's id, as a string. */\n payer: string;\n /** The overview's fingerprint at departure, or null when it was not known. */\n before: string | null;\n /** When the buyer came back; null while they are (or were) at the provider. */\n returnedAt: number | null;\n}\n\n/** One mounted hook: what the shared timers call, kept current on every render. */\ninterface Instance {\n payer: string | null;\n refetch: () => unknown;\n onLanded: (() => void) | undefined;\n timeoutMs: number;\n pollMs: number;\n}\n\ninterface CheckoutStore {\n key: string;\n pending: PendingCheckout | null;\n listeners: Set<() => void>;\n /** The mounted instances, in mount order: the last is the latest. */\n instances: Instance[];\n poll: ReturnType<typeof setInterval> | null;\n pollEvery: number;\n expiry: ReturnType<typeof setTimeout> | null;\n expiryAt: number | null;\n /** An instance consumed the marker in this commit; the others leave it be. */\n consuming: boolean;\n}\n\n/** Module-level, so the banner's instance and the subscription page's share one state,\n * one poll and one landing (keksdose mounts the hook twice, §14.4). */\nconst stores = new Map<string, CheckoutStore>();\n\nfunction readPending(key: string): PendingCheckout | null {\n const raw = readStored(key);\n if (raw === null) return null;\n try {\n const value = JSON.parse(raw) as Partial<PendingCheckout> | null;\n if (\n value &&\n typeof value.payer === \"string\" &&\n (value.before === null || typeof value.before === \"string\") &&\n (value.returnedAt === null || typeof value.returnedAt === \"number\")\n ) {\n return { payer: value.payer, before: value.before, returnedAt: value.returnedAt };\n }\n } catch {\n // Not ours, or damaged: as if nothing was noted.\n }\n return null;\n}\n\nfunction storeOf(key: string): CheckoutStore {\n let store = stores.get(key);\n if (!store) {\n store = {\n key,\n pending: readPending(key),\n listeners: new Set(),\n instances: [],\n poll: null,\n pollEvery: 0,\n expiry: null,\n expiryAt: null,\n consuming: false,\n };\n stores.set(key, store);\n }\n return store;\n}\n\nfunction setPending(store: CheckoutStore, next: PendingCheckout | null): void {\n store.pending = next;\n if (next) writeStored(store.key, JSON.stringify(next));\n else removeStored(store.key);\n for (const listener of store.listeners) listener();\n sync(store);\n}\n\n/** The latest mounted instance's refetch, failures swallowed: a poll that fails is\n * simply asked again at the next tick. */\nfunction refetchLatest(store: CheckoutStore): void {\n const latest = store.instances.at(-1);\n if (!latest) return;\n try {\n const result = latest.refetch() as PromiseLike<unknown> | undefined;\n if (result && typeof result.then === \"function\") result.then(undefined, () => {});\n } catch {\n // as above\n }\n}\n\n/** Arms or stops the store's two timers — the bound and the poll — for its state. */\nfunction sync(store: CheckoutStore): void {\n const pending = store.pending;\n const latest = store.instances.at(-1);\n const returnedAt = pending?.returnedAt ?? null;\n\n if (returnedAt === null) {\n if (store.expiry) clearTimeout(store.expiry);\n store.expiry = null;\n store.expiryAt = null;\n } else {\n const at = returnedAt + (latest?.timeoutMs ?? PROCESSING_FOR);\n if (at <= Date.now()) {\n setPending(store, null);\n return;\n }\n if (store.expiryAt !== at) {\n if (store.expiry) clearTimeout(store.expiry);\n store.expiryAt = at;\n store.expiry = setTimeout(() => {\n store.expiry = null;\n store.expiryAt = null;\n if (store.pending === pending) setPending(store, null);\n }, at - Date.now());\n }\n }\n\n // One poll for every instance, while a mounted one is the mark's payer.\n const watching = returnedAt !== null && store.instances.some((i) => i.payer === pending!.payer);\n const every = latest?.pollMs ?? POLL_EVERY;\n if (!watching) {\n if (store.poll) clearInterval(store.poll);\n store.poll = null;\n } else if (!store.poll || store.pollEvery !== every) {\n if (store.poll) clearInterval(store.poll);\n store.pollEvery = every;\n store.poll = setInterval(() => refetchLatest(store), every);\n }\n}\n\n/** A mounted instance joins the store's timers; the latest is the one they call. */\nfunction register(store: CheckoutStore, instance: Instance): () => void {\n store.instances.push(instance);\n return () => {\n store.instances = store.instances.filter((i) => i !== instance);\n sync(store);\n };\n}\n\n/**\n * Whether this instance is the one to consume a return marker: the first in a commit is.\n * Another instance in the same commit sees the same marker and leaves it be; the claim\n * lapses once the commit's effects have run.\n */\nfunction claimReturn(store: CheckoutStore): boolean {\n if (store.consuming) return false;\n store.consuming = true;\n void Promise.resolve().then(() => {\n store.consuming = false;\n });\n return true;\n}\n\n/**\n * Note the departure for a checkout — just before the page leaves for the provider (or\n * for the pay page, `payPageUrl(url, locale)`):\n *\n * noteCheckoutStarted(user.id, overview.data);\n * window.location.assign(payPageUrl(url, i18n.language));\n *\n * `payer` is the user's or the company's id: a mark never speaks for another payer.\n * `overview` is what the page shows now; left out, the return waits for `active`. Pass\n * the same `storageKey` as the hook's.\n *\n * A plan switch (0.39, §15.2.10) is waited for the same way, under the same key: note\n * the departure before `POST /billing/plan`, and once it answers put the return marker\n * on the page's URL yourself (`checkoutReturnUrl`'s `?checkout=done`) — the row follows\n * by webhook, and the fingerprint's `paid` shows it landed.\n */\nexport function noteCheckoutStarted(\n payer: string | number,\n overview?: CheckoutOverview,\n opts: { storageKey?: string } = {},\n): void {\n const store = storeOf(opts.storageKey ?? STORAGE_KEY);\n setPending(store, {\n payer: String(payer),\n before: overview ? checkoutFingerprint(overview) : null,\n returnedAt: null,\n });\n}\n\nexport interface CheckoutProcessingOptions {\n /** The signed-in payer — the user's or the company's id; null while unknown (the\n * marker then waits in the URL). */\n payer: string | number | null;\n /** The page was reached as the way back: `isCheckoutReturn(location.search)`. */\n returned: boolean;\n /** Drop the marker, with replace — `setSearchParams((p) => withoutCheckoutReturn(p),\n * { replace: true })`. Called once per return, by the first instance that sees it. */\n onConsumed: () => void;\n /** The overview query's data, judged here against the departure. */\n overview: CheckoutOverview | undefined;\n /** The overview query's refetch: the hook polls with it — one timer for every\n * instance, with the latest mounted one's — and \"Check again\" calls it. */\n refetch: () => unknown;\n /** Once per landing: refresh what a new standing changes — the plans, the items whose\n * `locked` may have lifted. The overview itself is already fresh. */\n onLanded?: () => void;\n /** How long a return keeps \"processing\". Default 600 000 (10 minutes). */\n timeoutMs?: number;\n /** How often the overview is asked meanwhile. Default 4 000. */\n pollMs?: number;\n /** The browser entry. Default `\"eifi1-billing-checkout\"`. */\n storageKey?: string;\n}\n\n/**\n * Whether this payer is back from a checkout whose payment the overview has not shown\n * yet — and the \"Check again\" for the banner that says so:\n *\n * const [params, setParams] = useSearchParams();\n * const overview = useQuery(billingOverviewQuery);\n * const { processing, checkAgain } = useCheckoutProcessing({\n * payer: user?.id ?? null,\n * returned: isCheckoutReturn(params),\n * onConsumed: () => setParams((p) => withoutCheckoutReturn(p), { replace: true }),\n * overview: overview.data,\n * refetch: overview.refetch,\n * onLanded: () => void qc.invalidateQueries({ queryKey: [\"budgets\"] }),\n * });\n * processing && <BillingBanner kind=\"processing\" onAction={checkAgain} />\n *\n * The kit has no router: the app says whether the URL returned and how to drop the\n * marker. Mount it wherever the return lands and the banner shows — every instance with\n * the same `storageKey` shares one state: the first that sees the marker consumes it,\n * one timer polls, and `onLanded` runs once. The app's `refetchOnWindowFocus` doesn't\n * matter.\n */\nexport function useCheckoutProcessing(options: CheckoutProcessingOptions): {\n processing: boolean;\n checkAgain: () => void;\n} {\n const {\n payer,\n returned,\n onConsumed,\n overview,\n refetch,\n onLanded,\n timeoutMs = PROCESSING_FOR,\n pollMs = POLL_EVERY,\n storageKey = STORAGE_KEY,\n } = options;\n const payerKey = payer === null || payer === undefined ? null : String(payer);\n const store = storeOf(storageKey);\n const subscribe = useCallback(\n (notify: () => void) => {\n store.listeners.add(notify);\n return () => {\n store.listeners.delete(notify);\n };\n },\n [store],\n );\n const snapshot = () => store.pending;\n const pending = useSyncExternalStore(subscribe, snapshot, snapshot);\n const processing = payerKey !== null && pending?.payer === payerKey && pending.returnedAt !== null;\n\n // This instance, as the shared timers see it. Registered before paint, so a mark that\n // ran out while the page was closed is dropped before \"processing\" can flash.\n const instanceRef = useRef<Instance>({ payer: payerKey, refetch, onLanded, timeoutMs, pollMs });\n useLayoutEffect(() => register(store, instanceRef.current), [store]);\n useLayoutEffect(() => {\n Object.assign(instanceRef.current, { payer: payerKey, refetch, onLanded, timeoutMs, pollMs });\n sync(store);\n });\n\n // The return: note it, then drop the marker so a reload does not count it twice.\n const consumed = useEffectEvent(() => onConsumed());\n useEffect(() => {\n if (!returned || payerKey === null || !claimReturn(store)) return;\n const before = store.pending?.payer === payerKey ? store.pending.before : null;\n setPending(store, { payer: payerKey, before, returnedAt: Date.now() });\n consumed();\n }, [returned, payerKey, store]);\n\n // Caught up: the overview has moved since the departure. Judged against the store as\n // it is NOW, so a second instance in the same commit finds the mark gone.\n useEffect(() => {\n if (!processing || !overview) return;\n const live = store.pending;\n if (!live || live.payer !== payerKey || live.returnedAt === null) return;\n if (!checkoutLanded(live.before, overview)) return;\n setPending(store, null);\n const latest = [...store.instances].reverse().find((i) => i.onLanded);\n latest?.onLanded?.();\n }, [processing, overview, payerKey, store]);\n\n const checkAgain = useCallback(() => {\n try {\n const result = instanceRef.current.refetch() as PromiseLike<unknown> | undefined;\n if (result && typeof result.then === \"function\") result.then(undefined, () => {});\n } catch {\n // The banner stays; the next poll asks again.\n }\n }, []);\n\n return { processing, checkAgain };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -154,9 +154,7 @@ function PlanPicker({
|
|
|
154
154
|
const legendId = `${generated}-legend`;
|
|
155
155
|
const [ownValue, setOwnValue] = useState(() => defaultValue ?? current ?? null);
|
|
156
156
|
const [ownInterval, setOwnInterval] = useState(defaultInterval);
|
|
157
|
-
const [ownCurrency, setOwnCurrency] = useState(
|
|
158
|
-
() => defaultCurrency ?? billingCurrencyFor(locale)
|
|
159
|
-
);
|
|
157
|
+
const [ownCurrency, setOwnCurrency] = useState(() => defaultCurrency ?? billingCurrencyFor(locale));
|
|
160
158
|
const value = valueProp !== void 0 ? valueProp : ownValue;
|
|
161
159
|
const interval = intervalProp ?? ownInterval;
|
|
162
160
|
const currency = currencyProp ?? ownCurrency;
|
|
@@ -179,90 +177,98 @@ function PlanPicker({
|
|
|
179
177
|
const pickedPrice = picked ? planPrice(picked.prices, currency, interval) : void 0;
|
|
180
178
|
const buttonText = !picked ? labels.choosePlan : relation === "upgrade" ? labels.upgrade(picked.name) : relation === "downgrade" ? labels.downgrade(picked.name) : labels.choose(picked.name);
|
|
181
179
|
const blocked = !picked ? labels.pickFirst : relation === "current" ? labels.isCurrent : pickedPrice?.kind === "not-offered" ? labels.notOffered : void 0;
|
|
182
|
-
return /* @__PURE__ */ jsxs(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
)
|
|
204
|
-
] }),
|
|
205
|
-
/* @__PURE__ */ jsxs(
|
|
206
|
-
"fieldset",
|
|
207
|
-
{
|
|
208
|
-
className: "min-w-0",
|
|
209
|
-
"aria-labelledby": legend !== void 0 ? legendId : void 0,
|
|
210
|
-
"aria-label": legend === void 0 ? labels.plans : void 0,
|
|
211
|
-
children: [
|
|
212
|
-
legend !== void 0 && /* @__PURE__ */ jsx("legend", { id: legendId, className: "mb-2 text-sm font-medium text-primary", children: legend }),
|
|
213
|
-
/* @__PURE__ */ jsx("div", { className: cn("grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3", gridClassName), children: plans.map((plan) => /* @__PURE__ */ jsx(
|
|
214
|
-
PlanCard,
|
|
180
|
+
return /* @__PURE__ */ jsxs(
|
|
181
|
+
"div",
|
|
182
|
+
{
|
|
183
|
+
className: cn("min-w-0 space-y-3", className),
|
|
184
|
+
"data-billing-interval": interval,
|
|
185
|
+
"data-billing-currency": currency,
|
|
186
|
+
children: [
|
|
187
|
+
(intervals.length > 1 || currencies.length > 1) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
188
|
+
intervals.length > 1 && /* @__PURE__ */ jsx(
|
|
189
|
+
ToggleGroup,
|
|
190
|
+
{
|
|
191
|
+
"aria-label": labels.interval,
|
|
192
|
+
className: "w-auto",
|
|
193
|
+
options: intervals.map((i) => ({ value: i, label: i === "year" ? labels.yearly : labels.monthly })),
|
|
194
|
+
value: interval,
|
|
195
|
+
onChange: setInterval
|
|
196
|
+
}
|
|
197
|
+
),
|
|
198
|
+
currencies.length > 1 && /* @__PURE__ */ jsx(
|
|
199
|
+
ToggleGroup,
|
|
215
200
|
{
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
201
|
+
"aria-label": labels.currency,
|
|
202
|
+
className: "w-auto",
|
|
203
|
+
options: currencies.map((c) => ({ value: c, label: c })),
|
|
204
|
+
value: currency,
|
|
205
|
+
onChange: setCurrency
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
] }),
|
|
209
|
+
/* @__PURE__ */ jsxs(
|
|
210
|
+
"fieldset",
|
|
211
|
+
{
|
|
212
|
+
className: "min-w-0",
|
|
213
|
+
"aria-labelledby": legend !== void 0 ? legendId : void 0,
|
|
214
|
+
"aria-label": legend === void 0 ? labels.plans : void 0,
|
|
215
|
+
children: [
|
|
216
|
+
legend !== void 0 && /* @__PURE__ */ jsx("legend", { id: legendId, className: "mb-2 text-sm font-medium text-primary", children: legend }),
|
|
217
|
+
/* @__PURE__ */ jsx("div", { className: cn("grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3", gridClassName), children: plans.map((plan) => /* @__PURE__ */ jsx(
|
|
218
|
+
PlanCard,
|
|
219
|
+
{
|
|
220
|
+
plan,
|
|
221
|
+
name: groupName,
|
|
222
|
+
currency,
|
|
223
|
+
interval,
|
|
224
|
+
relation: planRelation(plans, plan.code, current),
|
|
225
|
+
pricePreview: pricePreview?.(plan, { interval, currency }),
|
|
226
|
+
checked: picked?.code === plan.code,
|
|
227
|
+
onCheckedChange: (on) => on && pick(plan.code),
|
|
228
|
+
dimensionLabels,
|
|
229
|
+
formatLimit,
|
|
230
|
+
locale,
|
|
231
|
+
labels: labelsProp,
|
|
232
|
+
className: cardClassName
|
|
233
|
+
},
|
|
234
|
+
plan.code
|
|
235
|
+
)) })
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
),
|
|
239
|
+
/* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
|
|
240
|
+
Button,
|
|
241
|
+
{
|
|
242
|
+
type: "button",
|
|
243
|
+
variant: "default",
|
|
244
|
+
className: "max-sm:w-full",
|
|
245
|
+
disabledReason: blocked,
|
|
246
|
+
pending,
|
|
247
|
+
onClick: () => {
|
|
248
|
+
if (!picked) return;
|
|
249
|
+
const choice = { plan: picked.code, interval, currency };
|
|
250
|
+
if (permanentGrant && !switching) setWarned(choice);
|
|
251
|
+
else onChoose(choice);
|
|
229
252
|
},
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
children: buttonText
|
|
250
|
-
}
|
|
251
|
-
) }),
|
|
252
|
-
permanentGrant && !switching && /* @__PURE__ */ jsx(
|
|
253
|
-
GrantWarningConfirm,
|
|
254
|
-
{
|
|
255
|
-
open: warned !== null,
|
|
256
|
-
onOpenChange: (open) => {
|
|
257
|
-
if (!open) setWarned(null);
|
|
258
|
-
},
|
|
259
|
-
onConfirm: () => {
|
|
260
|
-
if (warned) onChoose(warned);
|
|
261
|
-
},
|
|
262
|
-
labels: labelsProp
|
|
263
|
-
}
|
|
264
|
-
)
|
|
265
|
-
] });
|
|
253
|
+
children: buttonText
|
|
254
|
+
}
|
|
255
|
+
) }),
|
|
256
|
+
permanentGrant && !switching && /* @__PURE__ */ jsx(
|
|
257
|
+
GrantWarningConfirm,
|
|
258
|
+
{
|
|
259
|
+
open: warned !== null,
|
|
260
|
+
onOpenChange: (open) => {
|
|
261
|
+
if (!open) setWarned(null);
|
|
262
|
+
},
|
|
263
|
+
onConfirm: () => {
|
|
264
|
+
if (warned) onChoose(warned);
|
|
265
|
+
},
|
|
266
|
+
labels: labelsProp
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
]
|
|
270
|
+
}
|
|
271
|
+
);
|
|
266
272
|
}
|
|
267
273
|
function GrantWarningConfirm({ open, onOpenChange, onConfirm, labels: labelsProp }) {
|
|
268
274
|
const labels = useBillingLabels(labelsProp);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/billing/plan-card.tsx"],"sourcesContent":["import { useId, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Check, Gauge } from \"lucide-react\";\n\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { formatNumber } from \"../lib/format\";\nimport { cn } from \"../lib/cn\";\nimport { ChoiceCard } from \"../components/choice-card\";\nimport type { ChoiceCardProps } from \"../components/choice-card\";\nimport { Chip } from \"../components/chip\";\nimport { ToggleGroup } from \"../components/toggle-group\";\nimport { Button } from \"../components/button\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport { BILLING_INTERVALS, billingCurrencyFor, formatPlanPrice, planPrice } from \"./plan-price\";\nimport type { BillingCurrency, BillingInterval, PlanPrices } from \"./plan-price\";\n\n/**\n * One plan of the app's catalogue (docs/billing-harmonization.md §3.1) as a page shows it:\n * server-kit's `PlanSpec` from `GET /billing/plans`, plus the words, which are the app's\n * (display names and feature lines come from its i18n, never from the provider).\n */\nexport interface BillingPlan {\n /** The plan code, lowercase on the wire (§12.16); compared without case here. */\n code: string;\n /** The app's display name, in the reader's language: \"Pro\". */\n name: string;\n /** A line under the price. */\n description?: ReactNode;\n /** Gross prices in minor units, per currency and period (§3.1, §12.17). None at all —\n * or 0 — is a free plan. */\n prices?: PlanPrices | null;\n /** Dimension → limit; `null` is unlimited (§3.1). Shown in the object's order. */\n limits?: Readonly<Record<string, number | null>>;\n /** The app's feature lines. */\n features?: readonly ReactNode[];\n /** Not choosable — a plan being retired, say. */\n disabled?: boolean;\n}\n\n/**\n * How a plan stands to the one the payer pays for now: `current` is it, `upgrade` comes\n * after it in the catalogue's order (`PlanSpec.sort`), `downgrade` before it.\n */\nexport type PlanRelation = \"current\" | \"upgrade\" | \"downgrade\";\n\n/** The plan's place against `current`, by the order of `plans`; `undefined` without a\n * current plan (a trial, an ended plan) or for a code that is not in the list. */\nexport function planRelation(\n plans: readonly Pick<BillingPlan, \"code\">[],\n code: string,\n current: string | null | undefined,\n): PlanRelation | undefined {\n if (!current) return undefined;\n const index = (c: string) => plans.findIndex((p) => p.code.toLowerCase() === c.toLowerCase());\n const at = index(code);\n const now = index(current);\n if (at < 0 || now < 0) return undefined;\n return at === now ? \"current\" : at > now ? \"upgrade\" : \"downgrade\";\n}\n\n/** What both parts need to draw a plan. */\ninterface PlanDisplayProps {\n /** The currency the prices are shown in (§2.6). */\n currency: BillingCurrency;\n /** The period the prices are shown for. */\n interval: BillingInterval;\n /** The app's name of each limit's dimension (\"Budgets\"). A dimension without one is\n * shown by its key. */\n dimensionLabels?: Readonly<Record<string, string>>;\n /** How a limit's figure is written — kastlan's storage is bytes (`formatFileSize`).\n * Default: a number in the kit's locale. */\n formatLimit?: (dimension: string, value: number) => string;\n /** Over the provider's locale. */\n locale?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\nexport interface PlanCardProps\n extends Omit<ChoiceCardProps, \"type\" | \"title\" | \"description\" | \"icon\" | \"indeterminate\" | \"value\" | \"labels\">,\n PlanDisplayProps {\n plan: BillingPlan;\n /** See {@link PlanRelation}. `current` draws the \"Current plan\" chip — a word in the\n * card's name, never a colour alone. */\n relation?: PlanRelation;\n /**\n * The provider's own localised price in place of the catalogue's figure (§12.17) — a\n * Merchant of Record's price preview for the buyer's country, tax included. The period\n * and \"VAT included\" stay: the law asks for a gross price, and the card says so.\n */\n pricePreview?: ReactNode;\n}\n\n/** A list from spans (`role=\"list\"`): the card is a `<label>`, which may hold phrasing\n * content only, so a `<ul>` there would be invalid HTML. */\nfunction SpanList({ name, items, icon }: { name: string; items: ReactNode[]; icon: ReactNode }) {\n if (items.length === 0) return null;\n return (\n <span role=\"list\" aria-label={name} className=\"block space-y-1\">\n {items.map((item, i) => (\n <span role=\"listitem\" key={i} className=\"flex items-start gap-1.5 text-secondary\">\n <span aria-hidden className=\"mt-[0.0625rem] flex shrink-0 text-muted [&_svg]:size-3.5\">\n {icon}\n </span>\n <span className=\"min-w-0\">\n {item}\n {/* A space between the items' texts in the flattened description. */}{\" \"}\n </span>\n </span>\n ))}\n </span>\n );\n}\n\n/**\n * One plan as a {@link ChoiceCard} radio (§7): the name (and \"Current plan\"), the GROSS\n * price in the chosen currency and period through `formatMoney` (\"CHF 49 per year, VAT\n * included\", §12.17), the limits (\"Budgets: 3\", \"Seats: Unlimited\") and the app's feature\n * lines. A plan with prices but none for this currency and period is shown, \"Not offered\n * for this billing period\", and cannot be picked.\n *\n * A radio of a set: give the cards one `name`, or use {@link PlanPicker}, which does that\n * and owns the choice. Everything else a `ChoiceCard` takes goes to it — `checked`,\n * `onCheckedChange`, `name`, `className`. Carries `data-plan` and `data-relation`.\n */\nexport function PlanCard({\n // The radio `<input>` (ChoiceCard's): a React 19 ref prop since 0.34 (MOD-4, refactor\n // audit 2026-10), where this was `forwardRef`.\n ref,\n plan,\n relation,\n pricePreview,\n currency,\n interval,\n dimensionLabels,\n formatLimit,\n locale: localeProp,\n labels: labelsProp,\n disabled,\n ...rest\n}: PlanCardProps) {\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale(localeProp);\n const price = planPrice(plan.prices, currency, interval);\n\n const priced = price.kind === \"price\";\n const figure = priced\n ? pricePreview !== undefined && pricePreview !== null\n ? pricePreview\n : formatPlanPrice(price.minor, currency, { locale })\n : price.kind === \"free\"\n ? labels.free\n : labels.notOffered;\n\n const limitLines = Object.entries(plan.limits ?? {}).map(([dimension, value]) => {\n const name = dimensionLabels?.[dimension] ?? dimension;\n const shown =\n value === null ? labels.unlimited : formatLimit ? formatLimit(dimension, value) : formatNumber(value, { locale });\n return common.fieldValue(name, shown);\n });\n\n const body = (\n <span className=\"mt-1 block space-y-2\">\n <span className=\"block\">\n <span\n data-part=\"price\"\n className={cn(\n \"tabular-nums\",\n price.kind === \"not-offered\"\n ? \"text-xs text-muted\"\n : \"text-lg font-semibold leading-tight text-primary\",\n )}\n >\n {figure}\n </span>\n {priced && (\n <>\n {\" \"}\n <span className=\"text-xs text-secondary\">\n {interval === \"year\" ? labels.perYear : labels.perMonth}\n </span>{\" \"}\n <span className=\"block text-xs text-muted\">{labels.vatIncluded}</span>\n </>\n )}{\" \"}\n </span>\n {plan.description !== undefined && plan.description !== null && (\n <span className=\"block text-secondary\">{plan.description} </span>\n )}\n <SpanList name={labels.limits} items={limitLines} icon={<Gauge />} />\n <SpanList name={labels.features} items={[...(plan.features ?? [])]} icon={<Check />} />\n </span>\n );\n\n return (\n <ChoiceCard\n ref={ref}\n {...rest}\n type=\"radio\"\n value={plan.code}\n disabled={disabled || plan.disabled || price.kind === \"not-offered\"}\n data-plan={plan.code}\n data-relation={relation}\n title={\n <>\n {plan.name}\n {relation === \"current\" && (\n <>\n {\" \"}\n <Chip size=\"xs\" tone=\"brand\" shape=\"square\" caps className=\"ms-1 align-[0.0625rem]\">\n {labels.current}\n </Chip>\n </>\n )}\n </>\n }\n description={body}\n />\n );\n}\n\n/** What {@link PlanPickerProps.onChoose} is handed: everything `POST /billing/checkout`\n * takes (§4). */\nexport interface PlanChoice {\n plan: string;\n interval: BillingInterval;\n currency: BillingCurrency;\n}\n\nexport interface PlanPickerProps extends Omit<PlanDisplayProps, \"currency\" | \"interval\"> {\n /** The catalogue, in its order (`PlanSpec.sort`): cheapest first. The order decides\n * upgrade and downgrade. */\n plans: readonly BillingPlan[];\n /**\n * The plan the payer pays for now (`plan` of the overview while `active` or\n * `past_due`). Leave it out during a trial and after a plan ended: then every plan is\n * \"Choose …\", the trial's plan included, since none is paid for yet.\n */\n current?: string | null;\n /** The picked plan, controlled. `null` while none is. */\n value?: string | null;\n /** The picked plan, uncontrolled. Default: `current`, else nothing. */\n defaultValue?: string | null;\n onValueChange?: (plan: string) => void;\n /** The period, controlled. */\n interval?: BillingInterval;\n /** The period, uncontrolled. Default `year` — lead with yearly (§12.17). */\n defaultInterval?: BillingInterval;\n onIntervalChange?: (interval: BillingInterval) => void;\n /** The currency, controlled. */\n currency?: BillingCurrency;\n /** The currency, uncontrolled. Default: the account's stored currency is the app's to\n * pass here (§12.18); without it, {@link billingCurrencyFor} the kit's locale. */\n defaultCurrency?: BillingCurrency;\n onCurrencyChange?: (currency: BillingCurrency) => void;\n /** The currencies the payer may switch between. Default: every currency a plan has a\n * price in. The switch shows only for more than one. */\n currencies?: readonly BillingCurrency[];\n /** The button: the app starts its checkout (`POST /billing/checkout`, then off to the\n * provider's page). The kit sends no request. */\n onChoose: (choice: PlanChoice) => void;\n /** The checkout is being opened: the button's spinner, no second press. */\n pending?: boolean;\n /**\n * 0.42 (§16.2 decision 5, §16.8 B): the payer's free grant runs without an end —\n * `isPermanentGrant(overview)`. A purchase under it changes nothing the payer gets\n * while the grant runs (§12.11), so the button first opens a confirm —\n * `grantWarningTitle`, `grantWarningBody`, \"Buy anyway\" (`grantWarningAction`) and\n * \"Back\" — and {@link onChoose} runs only on \"Buy anyway\". Warned, not refused.\n *\n * Not while {@link switching}: then the pick opens `PlanSwitchConfirm`, which takes\n * the same `permanentGrant` and shows the warning above its figures — one warning,\n * in the dialog where the payer agrees, rather than a confirm before a confirm.\n */\n permanentGrant?: boolean;\n /**\n * 0.42: the overview's `switching` — the app sends this pick to `PlanSwitchConfirm`\n * (§15.2.10), not the checkout. Only decides where {@link permanentGrant}'s warning\n * shows; the button and {@link onChoose} are the same either way.\n */\n switching?: boolean;\n /** The provider's localised price for a plan, or `undefined` for the catalogue's —\n * see {@link PlanCardProps.pricePreview}. */\n pricePreview?: (plan: BillingPlan, choice: Omit<PlanChoice, \"plan\">) => ReactNode | undefined;\n /** A visible question over the cards. Without one the set is named \"Plans\". */\n legend?: ReactNode;\n /** Shared by the radios. Generated if omitted. */\n name?: string;\n /** Classes for the whole picker. */\n className?: string;\n /** Classes for the card grid. Default: one column, two from `sm`, three from `lg`. */\n gridClassName?: string;\n /** Classes for every card. */\n cardClassName?: string;\n}\n\n/** Every currency some plan has a price in, CHF first. */\nfunction pricedCurrencies(plans: readonly BillingPlan[]): BillingCurrency[] {\n const found = new Set<BillingCurrency>();\n for (const p of plans) for (const c of Object.keys(p.prices ?? {})) found.add(c as BillingCurrency);\n return ([\"CHF\", \"EUR\"] as const).filter((c) => found.has(c));\n}\n\n/** Every period some plan has a price for, yearly first. */\nfunction pricedIntervals(plans: readonly BillingPlan[]): BillingInterval[] {\n const found = new Set<BillingInterval>();\n for (const p of plans)\n for (const byInterval of Object.values(p.prices ?? {}))\n for (const [k, v] of Object.entries(byInterval ?? {})) if (typeof v === \"number\") found.add(k as BillingInterval);\n return BILLING_INTERVALS.filter((i) => found.has(i));\n}\n\n/**\n * The plans to choose from (§7): the period switch — yearly first (§12.17) — and, where\n * the payer may pay in either, the currency switch (§2.6); a {@link PlanCard} per plan in\n * a radio set; and one button that says what pressing it does: \"Upgrade to Pro\", \"Switch\n * to Free\", or \"Choose Pro\" with no plan paid for yet.\n *\n * One button under the set rather than one per card: each card IS a radio — a `<label>`\n * round an `<input>` — and a button inside a label is interactive content inside\n * interactive content. Picking and committing are two steps anyway: the price under the\n * picked card is what checkout will charge.\n *\n * The button never takes the write lock: billing itself always works, a read-only payer\n * must be able to pay (§3.3). It is off, with the reason on hover and focus, while\n * nothing is picked or the current plan is.\n *\n * 0.42 (§16.8 B): under a grant without an end (`permanentGrant`) the payer is warned\n * before paying for a plan that won't apply — here, in a confirm before `onChoose`, on\n * the way to the checkout; on the way to a switch (`switching`) in `PlanSwitchConfirm`\n * instead, so the warning shows once on either path.\n */\nexport function PlanPicker({\n plans,\n current,\n value: valueProp,\n defaultValue,\n onValueChange,\n interval: intervalProp,\n defaultInterval = \"year\",\n onIntervalChange,\n currency: currencyProp,\n defaultCurrency,\n onCurrencyChange,\n currencies: currenciesProp,\n onChoose,\n pending,\n permanentGrant = false,\n switching = false,\n pricePreview,\n legend,\n name,\n className,\n gridClassName,\n cardClassName,\n dimensionLabels,\n formatLimit,\n locale: localeProp,\n labels: labelsProp,\n}: PlanPickerProps) {\n const labels = useBillingLabels(labelsProp);\n const locale = useKitLocale(localeProp);\n const generated = useId();\n /** The choice the grant warning holds back until \"Buy anyway\". */\n const [warned, setWarned] = useState<PlanChoice | null>(null);\n const groupName = name ?? generated;\n const legendId = `${generated}-legend`;\n\n const [ownValue, setOwnValue] = useState<string | null>(() => defaultValue ?? current ?? null);\n const [ownInterval, setOwnInterval] = useState<BillingInterval>(defaultInterval);\n const [ownCurrency, setOwnCurrency] = useState<BillingCurrency>(\n () => defaultCurrency ?? billingCurrencyFor(locale),\n );\n const value = valueProp !== undefined ? valueProp : ownValue;\n const interval = intervalProp ?? ownInterval;\n const currency = currencyProp ?? ownCurrency;\n\n const pick = (code: string) => {\n if (valueProp === undefined) setOwnValue(code);\n onValueChange?.(code);\n };\n const setInterval = (next: BillingInterval) => {\n if (intervalProp === undefined) setOwnInterval(next);\n onIntervalChange?.(next);\n };\n const setCurrency = (next: BillingCurrency) => {\n if (currencyProp === undefined) setOwnCurrency(next);\n onCurrencyChange?.(next);\n };\n\n const currencies = currenciesProp ?? pricedCurrencies(plans);\n const intervals = pricedIntervals(plans);\n const picked = value ? plans.find((p) => p.code.toLowerCase() === value.toLowerCase()) : undefined;\n const relation = picked ? planRelation(plans, picked.code, current) : undefined;\n const pickedPrice = picked ? planPrice(picked.prices, currency, interval) : undefined;\n\n const buttonText = !picked\n ? labels.choosePlan\n : relation === \"upgrade\"\n ? labels.upgrade(picked.name)\n : relation === \"downgrade\"\n ? labels.downgrade(picked.name)\n : labels.choose(picked.name);\n const blocked = !picked\n ? labels.pickFirst\n : relation === \"current\"\n ? labels.isCurrent\n : pickedPrice?.kind === \"not-offered\"\n ? labels.notOffered\n : undefined;\n\n return (\n <div className={cn(\"min-w-0 space-y-3\", className)} data-billing-interval={interval} data-billing-currency={currency}>\n {(intervals.length > 1 || currencies.length > 1) && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {intervals.length > 1 && (\n <ToggleGroup\n aria-label={labels.interval}\n // Content-wide, so the two switches share a row and wrap only when narrow.\n className=\"w-auto\"\n options={intervals.map((i) => ({ value: i, label: i === \"year\" ? labels.yearly : labels.monthly }))}\n value={interval}\n onChange={setInterval}\n />\n )}\n {currencies.length > 1 && (\n <ToggleGroup\n aria-label={labels.currency}\n className=\"w-auto\"\n options={currencies.map((c) => ({ value: c, label: c }))}\n value={currency}\n onChange={setCurrency}\n />\n )}\n </div>\n )}\n <fieldset\n className=\"min-w-0\"\n aria-labelledby={legend !== undefined ? legendId : undefined}\n aria-label={legend === undefined ? labels.plans : undefined}\n >\n {legend !== undefined && (\n <legend id={legendId} className=\"mb-2 text-sm font-medium text-primary\">\n {legend}\n </legend>\n )}\n <div className={cn(\"grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3\", gridClassName)}>\n {plans.map((plan) => (\n <PlanCard\n key={plan.code}\n plan={plan}\n name={groupName}\n currency={currency}\n interval={interval}\n relation={planRelation(plans, plan.code, current)}\n pricePreview={pricePreview?.(plan, { interval, currency })}\n checked={picked?.code === plan.code}\n onCheckedChange={(on) => on && pick(plan.code)}\n dimensionLabels={dimensionLabels}\n formatLimit={formatLimit}\n locale={locale}\n labels={labelsProp}\n className={cardClassName}\n />\n ))}\n </div>\n </fieldset>\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n variant=\"default\"\n className=\"max-sm:w-full\"\n disabledReason={blocked}\n pending={pending}\n onClick={() => {\n if (!picked) return;\n const choice = { plan: picked.code, interval, currency };\n if (permanentGrant && !switching) setWarned(choice);\n else onChoose(choice);\n }}\n >\n {buttonText}\n </Button>\n </div>\n {permanentGrant && !switching && (\n <GrantWarningConfirm\n open={warned !== null}\n onOpenChange={(open) => {\n if (!open) setWarned(null);\n }}\n onConfirm={() => {\n if (warned) onChoose(warned);\n }}\n labels={labelsProp}\n />\n )}\n </div>\n );\n}\n\nexport interface GrantWarningConfirmProps {\n /** Shown, controlled. */\n open: boolean;\n /** `false` on \"Back\", Escape, the backdrop — and after \"Buy anyway\". */\n onOpenChange: (open: boolean) => void;\n /** \"Buy anyway\": the app opens its checkout. The dialog closes itself after it. */\n onConfirm: () => void;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English: `grantWarningTitle`,\n * `grantWarningBody`, `grantWarningAction`. \"Back\" is `common.back`. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/**\n * 0.42 (§16.8 B): the permanent-grant warning before a checkout — `grantWarningTitle`,\n * `grantWarningBody`, \"Back\" and \"Buy anyway\" (`grantWarningAction`). {@link PlanPicker}\n * shows it itself under `permanentGrant` when not `switching`. Exported for the path the\n * picker cannot see: an app that meant to switch, learned there is no subscription to\n * switch (keksdose: `/billing/plan/preview` → 409 `billing_not_subscribed`) and falls back\n * to a checkout — show this before that checkout, so a permanent-grant holder is still\n * warned. Carries `data-grant-warning`.\n */\nexport function GrantWarningConfirm({ open, onOpenChange, onConfirm, labels: labelsProp }: GrantWarningConfirmProps) {\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n return (\n <DialogFrame\n open={open}\n title={labels.grantWarningTitle}\n description={labels.grantWarningBody}\n onClose={() => onOpenChange(false)}\n data-grant-warning=\"\"\n actions={(close) => (\n <>\n <Button variant=\"secondary\" onClick={close}>\n {common.back}\n </Button>\n <Button\n variant=\"default\"\n onClick={() => {\n onConfirm();\n onOpenChange(false);\n }}\n >\n {labels.grantWarningAction}\n </Button>\n </>\n )}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/billing/plan-card.tsx"],"sourcesContent":["import { useId, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Check, Gauge } from \"lucide-react\";\n\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { formatNumber } from \"../lib/format\";\nimport { cn } from \"../lib/cn\";\nimport { ChoiceCard } from \"../components/choice-card\";\nimport type { ChoiceCardProps } from \"../components/choice-card\";\nimport { Chip } from \"../components/chip\";\nimport { ToggleGroup } from \"../components/toggle-group\";\nimport { Button } from \"../components/button\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport { BILLING_INTERVALS, billingCurrencyFor, formatPlanPrice, planPrice } from \"./plan-price\";\nimport type { BillingCurrency, BillingInterval, PlanPrices } from \"./plan-price\";\n\n/**\n * One plan of the app's catalogue (docs/billing-harmonization.md §3.1) as a page shows it:\n * server-kit's `PlanSpec` from `GET /billing/plans`, plus the words, which are the app's\n * (display names and feature lines come from its i18n, never from the provider).\n */\nexport interface BillingPlan {\n /** The plan code, lowercase on the wire (§12.16); compared without case here. */\n code: string;\n /** The app's display name, in the reader's language: \"Pro\". */\n name: string;\n /** A line under the price. */\n description?: ReactNode;\n /** Gross prices in minor units, per currency and period (§3.1, §12.17). None at all —\n * or 0 — is a free plan. */\n prices?: PlanPrices | null;\n /** Dimension → limit; `null` is unlimited (§3.1). Shown in the object's order. */\n limits?: Readonly<Record<string, number | null>>;\n /** The app's feature lines. */\n features?: readonly ReactNode[];\n /** Not choosable — a plan being retired, say. */\n disabled?: boolean;\n}\n\n/**\n * How a plan stands to the one the payer pays for now: `current` is it, `upgrade` comes\n * after it in the catalogue's order (`PlanSpec.sort`), `downgrade` before it.\n */\nexport type PlanRelation = \"current\" | \"upgrade\" | \"downgrade\";\n\n/** The plan's place against `current`, by the order of `plans`; `undefined` without a\n * current plan (a trial, an ended plan) or for a code that is not in the list. */\nexport function planRelation(\n plans: readonly Pick<BillingPlan, \"code\">[],\n code: string,\n current: string | null | undefined,\n): PlanRelation | undefined {\n if (!current) return undefined;\n const index = (c: string) => plans.findIndex((p) => p.code.toLowerCase() === c.toLowerCase());\n const at = index(code);\n const now = index(current);\n if (at < 0 || now < 0) return undefined;\n return at === now ? \"current\" : at > now ? \"upgrade\" : \"downgrade\";\n}\n\n/** What both parts need to draw a plan. */\ninterface PlanDisplayProps {\n /** The currency the prices are shown in (§2.6). */\n currency: BillingCurrency;\n /** The period the prices are shown for. */\n interval: BillingInterval;\n /** The app's name of each limit's dimension (\"Budgets\"). A dimension without one is\n * shown by its key. */\n dimensionLabels?: Readonly<Record<string, string>>;\n /** How a limit's figure is written — kastlan's storage is bytes (`formatFileSize`).\n * Default: a number in the kit's locale. */\n formatLimit?: (dimension: string, value: number) => string;\n /** Over the provider's locale. */\n locale?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\nexport interface PlanCardProps\n extends\n Omit<ChoiceCardProps, \"type\" | \"title\" | \"description\" | \"icon\" | \"indeterminate\" | \"value\" | \"labels\">,\n PlanDisplayProps {\n plan: BillingPlan;\n /** See {@link PlanRelation}. `current` draws the \"Current plan\" chip — a word in the\n * card's name, never a colour alone. */\n relation?: PlanRelation;\n /**\n * The provider's own localised price in place of the catalogue's figure (§12.17) — a\n * Merchant of Record's price preview for the buyer's country, tax included. The period\n * and \"VAT included\" stay: the law asks for a gross price, and the card says so.\n */\n pricePreview?: ReactNode;\n}\n\n/** A list from spans (`role=\"list\"`): the card is a `<label>`, which may hold phrasing\n * content only, so a `<ul>` there would be invalid HTML. */\nfunction SpanList({ name, items, icon }: { name: string; items: ReactNode[]; icon: ReactNode }) {\n if (items.length === 0) return null;\n return (\n <span role=\"list\" aria-label={name} className=\"block space-y-1\">\n {items.map((item, i) => (\n <span role=\"listitem\" key={i} className=\"flex items-start gap-1.5 text-secondary\">\n <span aria-hidden className=\"mt-[0.0625rem] flex shrink-0 text-muted [&_svg]:size-3.5\">\n {icon}\n </span>\n <span className=\"min-w-0\">\n {item}\n {/* A space between the items' texts in the flattened description. */}{\" \"}\n </span>\n </span>\n ))}\n </span>\n );\n}\n\n/**\n * One plan as a {@link ChoiceCard} radio (§7): the name (and \"Current plan\"), the GROSS\n * price in the chosen currency and period through `formatMoney` (\"CHF 49 per year, VAT\n * included\", §12.17), the limits (\"Budgets: 3\", \"Seats: Unlimited\") and the app's feature\n * lines. A plan with prices but none for this currency and period is shown, \"Not offered\n * for this billing period\", and cannot be picked.\n *\n * A radio of a set: give the cards one `name`, or use {@link PlanPicker}, which does that\n * and owns the choice. Everything else a `ChoiceCard` takes goes to it — `checked`,\n * `onCheckedChange`, `name`, `className`. Carries `data-plan` and `data-relation`.\n */\nexport function PlanCard({\n // The radio `<input>` (ChoiceCard's): a React 19 ref prop since 0.34 (MOD-4, refactor\n // audit 2026-10), where this was `forwardRef`.\n ref,\n plan,\n relation,\n pricePreview,\n currency,\n interval,\n dimensionLabels,\n formatLimit,\n locale: localeProp,\n labels: labelsProp,\n disabled,\n ...rest\n}: PlanCardProps) {\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale(localeProp);\n const price = planPrice(plan.prices, currency, interval);\n\n const priced = price.kind === \"price\";\n const figure = priced\n ? pricePreview !== undefined && pricePreview !== null\n ? pricePreview\n : formatPlanPrice(price.minor, currency, { locale })\n : price.kind === \"free\"\n ? labels.free\n : labels.notOffered;\n\n const limitLines = Object.entries(plan.limits ?? {}).map(([dimension, value]) => {\n const name = dimensionLabels?.[dimension] ?? dimension;\n const shown =\n value === null ? labels.unlimited : formatLimit ? formatLimit(dimension, value) : formatNumber(value, { locale });\n return common.fieldValue(name, shown);\n });\n\n const body = (\n <span className=\"mt-1 block space-y-2\">\n <span className=\"block\">\n <span\n data-part=\"price\"\n className={cn(\n \"tabular-nums\",\n price.kind === \"not-offered\" ? \"text-xs text-muted\" : \"text-lg font-semibold leading-tight text-primary\",\n )}\n >\n {figure}\n </span>\n {priced && (\n <>\n {\" \"}\n <span className=\"text-xs text-secondary\">\n {interval === \"year\" ? labels.perYear : labels.perMonth}\n </span>{\" \"}\n <span className=\"block text-xs text-muted\">{labels.vatIncluded}</span>\n </>\n )}{\" \"}\n </span>\n {plan.description !== undefined && plan.description !== null && (\n <span className=\"block text-secondary\">{plan.description} </span>\n )}\n <SpanList name={labels.limits} items={limitLines} icon={<Gauge />} />\n <SpanList name={labels.features} items={[...(plan.features ?? [])]} icon={<Check />} />\n </span>\n );\n\n return (\n <ChoiceCard\n ref={ref}\n {...rest}\n type=\"radio\"\n value={plan.code}\n disabled={disabled || plan.disabled || price.kind === \"not-offered\"}\n data-plan={plan.code}\n data-relation={relation}\n title={\n <>\n {plan.name}\n {relation === \"current\" && (\n <>\n {\" \"}\n <Chip size=\"xs\" tone=\"brand\" shape=\"square\" caps className=\"ms-1 align-[0.0625rem]\">\n {labels.current}\n </Chip>\n </>\n )}\n </>\n }\n description={body}\n />\n );\n}\n\n/** What {@link PlanPickerProps.onChoose} is handed: everything `POST /billing/checkout`\n * takes (§4). */\nexport interface PlanChoice {\n plan: string;\n interval: BillingInterval;\n currency: BillingCurrency;\n}\n\nexport interface PlanPickerProps extends Omit<PlanDisplayProps, \"currency\" | \"interval\"> {\n /** The catalogue, in its order (`PlanSpec.sort`): cheapest first. The order decides\n * upgrade and downgrade. */\n plans: readonly BillingPlan[];\n /**\n * The plan the payer pays for now (`plan` of the overview while `active` or\n * `past_due`). Leave it out during a trial and after a plan ended: then every plan is\n * \"Choose …\", the trial's plan included, since none is paid for yet.\n */\n current?: string | null;\n /** The picked plan, controlled. `null` while none is. */\n value?: string | null;\n /** The picked plan, uncontrolled. Default: `current`, else nothing. */\n defaultValue?: string | null;\n onValueChange?: (plan: string) => void;\n /** The period, controlled. */\n interval?: BillingInterval;\n /** The period, uncontrolled. Default `year` — lead with yearly (§12.17). */\n defaultInterval?: BillingInterval;\n onIntervalChange?: (interval: BillingInterval) => void;\n /** The currency, controlled. */\n currency?: BillingCurrency;\n /** The currency, uncontrolled. Default: the account's stored currency is the app's to\n * pass here (§12.18); without it, {@link billingCurrencyFor} the kit's locale. */\n defaultCurrency?: BillingCurrency;\n onCurrencyChange?: (currency: BillingCurrency) => void;\n /** The currencies the payer may switch between. Default: every currency a plan has a\n * price in. The switch shows only for more than one. */\n currencies?: readonly BillingCurrency[];\n /** The button: the app starts its checkout (`POST /billing/checkout`, then off to the\n * provider's page). The kit sends no request. */\n onChoose: (choice: PlanChoice) => void;\n /** The checkout is being opened: the button's spinner, no second press. */\n pending?: boolean;\n /**\n * 0.42 (§16.2 decision 5, §16.8 B): the payer's free grant runs without an end —\n * `isPermanentGrant(overview)`. A purchase under it changes nothing the payer gets\n * while the grant runs (§12.11), so the button first opens a confirm —\n * `grantWarningTitle`, `grantWarningBody`, \"Buy anyway\" (`grantWarningAction`) and\n * \"Back\" — and {@link onChoose} runs only on \"Buy anyway\". Warned, not refused.\n *\n * Not while {@link switching}: then the pick opens `PlanSwitchConfirm`, which takes\n * the same `permanentGrant` and shows the warning above its figures — one warning,\n * in the dialog where the payer agrees, rather than a confirm before a confirm.\n */\n permanentGrant?: boolean;\n /**\n * 0.42: the overview's `switching` — the app sends this pick to `PlanSwitchConfirm`\n * (§15.2.10), not the checkout. Only decides where {@link permanentGrant}'s warning\n * shows; the button and {@link onChoose} are the same either way.\n */\n switching?: boolean;\n /** The provider's localised price for a plan, or `undefined` for the catalogue's —\n * see {@link PlanCardProps.pricePreview}. */\n pricePreview?: (plan: BillingPlan, choice: Omit<PlanChoice, \"plan\">) => ReactNode | undefined;\n /** A visible question over the cards. Without one the set is named \"Plans\". */\n legend?: ReactNode;\n /** Shared by the radios. Generated if omitted. */\n name?: string;\n /** Classes for the whole picker. */\n className?: string;\n /** Classes for the card grid. Default: one column, two from `sm`, three from `lg`. */\n gridClassName?: string;\n /** Classes for every card. */\n cardClassName?: string;\n}\n\n/** Every currency some plan has a price in, CHF first. */\nfunction pricedCurrencies(plans: readonly BillingPlan[]): BillingCurrency[] {\n const found = new Set<BillingCurrency>();\n for (const p of plans) for (const c of Object.keys(p.prices ?? {})) found.add(c as BillingCurrency);\n return ([\"CHF\", \"EUR\"] as const).filter((c) => found.has(c));\n}\n\n/** Every period some plan has a price for, yearly first. */\nfunction pricedIntervals(plans: readonly BillingPlan[]): BillingInterval[] {\n const found = new Set<BillingInterval>();\n for (const p of plans)\n for (const byInterval of Object.values(p.prices ?? {}))\n for (const [k, v] of Object.entries(byInterval ?? {})) if (typeof v === \"number\") found.add(k as BillingInterval);\n return BILLING_INTERVALS.filter((i) => found.has(i));\n}\n\n/**\n * The plans to choose from (§7): the period switch — yearly first (§12.17) — and, where\n * the payer may pay in either, the currency switch (§2.6); a {@link PlanCard} per plan in\n * a radio set; and one button that says what pressing it does: \"Upgrade to Pro\", \"Switch\n * to Free\", or \"Choose Pro\" with no plan paid for yet.\n *\n * One button under the set rather than one per card: each card IS a radio — a `<label>`\n * round an `<input>` — and a button inside a label is interactive content inside\n * interactive content. Picking and committing are two steps anyway: the price under the\n * picked card is what checkout will charge.\n *\n * The button never takes the write lock: billing itself always works, a read-only payer\n * must be able to pay (§3.3). It is off, with the reason on hover and focus, while\n * nothing is picked or the current plan is.\n *\n * 0.42 (§16.8 B): under a grant without an end (`permanentGrant`) the payer is warned\n * before paying for a plan that won't apply — here, in a confirm before `onChoose`, on\n * the way to the checkout; on the way to a switch (`switching`) in `PlanSwitchConfirm`\n * instead, so the warning shows once on either path.\n */\nexport function PlanPicker({\n plans,\n current,\n value: valueProp,\n defaultValue,\n onValueChange,\n interval: intervalProp,\n defaultInterval = \"year\",\n onIntervalChange,\n currency: currencyProp,\n defaultCurrency,\n onCurrencyChange,\n currencies: currenciesProp,\n onChoose,\n pending,\n permanentGrant = false,\n switching = false,\n pricePreview,\n legend,\n name,\n className,\n gridClassName,\n cardClassName,\n dimensionLabels,\n formatLimit,\n locale: localeProp,\n labels: labelsProp,\n}: PlanPickerProps) {\n const labels = useBillingLabels(labelsProp);\n const locale = useKitLocale(localeProp);\n const generated = useId();\n /** The choice the grant warning holds back until \"Buy anyway\". */\n const [warned, setWarned] = useState<PlanChoice | null>(null);\n const groupName = name ?? generated;\n const legendId = `${generated}-legend`;\n\n const [ownValue, setOwnValue] = useState<string | null>(() => defaultValue ?? current ?? null);\n const [ownInterval, setOwnInterval] = useState<BillingInterval>(defaultInterval);\n const [ownCurrency, setOwnCurrency] = useState<BillingCurrency>(() => defaultCurrency ?? billingCurrencyFor(locale));\n const value = valueProp !== undefined ? valueProp : ownValue;\n const interval = intervalProp ?? ownInterval;\n const currency = currencyProp ?? ownCurrency;\n\n const pick = (code: string) => {\n if (valueProp === undefined) setOwnValue(code);\n onValueChange?.(code);\n };\n const setInterval = (next: BillingInterval) => {\n if (intervalProp === undefined) setOwnInterval(next);\n onIntervalChange?.(next);\n };\n const setCurrency = (next: BillingCurrency) => {\n if (currencyProp === undefined) setOwnCurrency(next);\n onCurrencyChange?.(next);\n };\n\n const currencies = currenciesProp ?? pricedCurrencies(plans);\n const intervals = pricedIntervals(plans);\n const picked = value ? plans.find((p) => p.code.toLowerCase() === value.toLowerCase()) : undefined;\n const relation = picked ? planRelation(plans, picked.code, current) : undefined;\n const pickedPrice = picked ? planPrice(picked.prices, currency, interval) : undefined;\n\n const buttonText = !picked\n ? labels.choosePlan\n : relation === \"upgrade\"\n ? labels.upgrade(picked.name)\n : relation === \"downgrade\"\n ? labels.downgrade(picked.name)\n : labels.choose(picked.name);\n const blocked = !picked\n ? labels.pickFirst\n : relation === \"current\"\n ? labels.isCurrent\n : pickedPrice?.kind === \"not-offered\"\n ? labels.notOffered\n : undefined;\n\n return (\n <div\n className={cn(\"min-w-0 space-y-3\", className)}\n data-billing-interval={interval}\n data-billing-currency={currency}\n >\n {(intervals.length > 1 || currencies.length > 1) && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {intervals.length > 1 && (\n <ToggleGroup\n aria-label={labels.interval}\n // Content-wide, so the two switches share a row and wrap only when narrow.\n className=\"w-auto\"\n options={intervals.map((i) => ({ value: i, label: i === \"year\" ? labels.yearly : labels.monthly }))}\n value={interval}\n onChange={setInterval}\n />\n )}\n {currencies.length > 1 && (\n <ToggleGroup\n aria-label={labels.currency}\n className=\"w-auto\"\n options={currencies.map((c) => ({ value: c, label: c }))}\n value={currency}\n onChange={setCurrency}\n />\n )}\n </div>\n )}\n <fieldset\n className=\"min-w-0\"\n aria-labelledby={legend !== undefined ? legendId : undefined}\n aria-label={legend === undefined ? labels.plans : undefined}\n >\n {legend !== undefined && (\n <legend id={legendId} className=\"mb-2 text-sm font-medium text-primary\">\n {legend}\n </legend>\n )}\n <div className={cn(\"grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3\", gridClassName)}>\n {plans.map((plan) => (\n <PlanCard\n key={plan.code}\n plan={plan}\n name={groupName}\n currency={currency}\n interval={interval}\n relation={planRelation(plans, plan.code, current)}\n pricePreview={pricePreview?.(plan, { interval, currency })}\n checked={picked?.code === plan.code}\n onCheckedChange={(on) => on && pick(plan.code)}\n dimensionLabels={dimensionLabels}\n formatLimit={formatLimit}\n locale={locale}\n labels={labelsProp}\n className={cardClassName}\n />\n ))}\n </div>\n </fieldset>\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n variant=\"default\"\n className=\"max-sm:w-full\"\n disabledReason={blocked}\n pending={pending}\n onClick={() => {\n if (!picked) return;\n const choice = { plan: picked.code, interval, currency };\n if (permanentGrant && !switching) setWarned(choice);\n else onChoose(choice);\n }}\n >\n {buttonText}\n </Button>\n </div>\n {permanentGrant && !switching && (\n <GrantWarningConfirm\n open={warned !== null}\n onOpenChange={(open) => {\n if (!open) setWarned(null);\n }}\n onConfirm={() => {\n if (warned) onChoose(warned);\n }}\n labels={labelsProp}\n />\n )}\n </div>\n );\n}\n\nexport interface GrantWarningConfirmProps {\n /** Shown, controlled. */\n open: boolean;\n /** `false` on \"Back\", Escape, the backdrop — and after \"Buy anyway\". */\n onOpenChange: (open: boolean) => void;\n /** \"Buy anyway\": the app opens its checkout. The dialog closes itself after it. */\n onConfirm: () => void;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English: `grantWarningTitle`,\n * `grantWarningBody`, `grantWarningAction`. \"Back\" is `common.back`. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/**\n * 0.42 (§16.8 B): the permanent-grant warning before a checkout — `grantWarningTitle`,\n * `grantWarningBody`, \"Back\" and \"Buy anyway\" (`grantWarningAction`). {@link PlanPicker}\n * shows it itself under `permanentGrant` when not `switching`. Exported for the path the\n * picker cannot see: an app that meant to switch, learned there is no subscription to\n * switch (keksdose: `/billing/plan/preview` → 409 `billing_not_subscribed`) and falls back\n * to a checkout — show this before that checkout, so a permanent-grant holder is still\n * warned. Carries `data-grant-warning`.\n */\nexport function GrantWarningConfirm({ open, onOpenChange, onConfirm, labels: labelsProp }: GrantWarningConfirmProps) {\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n return (\n <DialogFrame\n open={open}\n title={labels.grantWarningTitle}\n description={labels.grantWarningBody}\n onClose={() => onOpenChange(false)}\n data-grant-warning=\"\"\n actions={(close) => (\n <>\n <Button variant=\"secondary\" onClick={close}>\n {common.back}\n </Button>\n <Button\n variant=\"default\"\n onClick={() => {\n onConfirm();\n onOpenChange(false);\n }}\n >\n {labels.grantWarningAction}\n </Button>\n </>\n )}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/billing/plan-limit-toast.tsx"],"sourcesContent":["import { useCallback } from \"react\";\n\nimport { useKitLocale } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { documentNavigation } from \"../lib/document-navigation\";\nimport { formatNumber } from \"../lib/format\";\nimport { toast } from \"../components/toast\";\nimport type { ToastAction, ToastId } from \"../components/toast\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport type { PlanLimitRefusal } from \"./plan-limit\";\nimport { planLimitMailto } from \"./plan-limit-notice\";\n\nexport interface PlanLimitToastOptions {\n /** `upgrade`: plans are bought — \"Choose a plan\". `contact`: the operator grants them\n * by hand — \"Ask for more\", a mail to support. */\n mode: \"upgrade\" | \"contact\";\n /** `upgrade`: the app's navigation to its subscription page (the kit has no router).\n * Left out, the toast has no action. */\n onChoosePlan?: () => void;\n /** `contact`: support@ the app's domain, opened as `planLimitMailto(email,\n * contactSubject(name))`. Left out, the toast has no action. */\n email?: string;\n /** The app's word per dimension: `{ budgets: \"Budgets\", scans: \"Scans this month\" }`.\n * A dimension without one is named by its key. */\n dimensionLabels?: Readonly<Record<string, string>>;\n /** How a figure is written — kastlan's storage limit is bytes. Default: a number in\n * the kit's locale. */\n formatValue?: (dimension: string, value: number) => string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/**\n * `PlanLimitNotice`'s words as a toast (docs/billing-harmonization.md §14.11), for a\n * refused create with no room for the notice — a row's button, a share target: \"Plan\n * limit reached\", what to do, the figure when the refusal has one (\"Budgets: 3 of 3\"),\n * and the mode's action.\n *\n * const showLimit = usePlanLimitToast({\n * mode: \"upgrade\",\n * onChoosePlan: () => navigate(\"/settings/subscription\"),\n * dimensionLabels: { budgets: t(\"billing.dim_budgets\") },\n * });\n * catch (err) { const hit = isPlanLimit(err); if (hit) showLimit(hit); }\n *\n * One toast per dimension: its id is `plan-limit:<dimension>`, so a repeat replaces it\n * in place instead of stacking a second (keksdose's own hook stacked). Called with no\n * hit, it says the words without a figure, under `plan-limit`. No `limit` in the\n * refusal, no figure line; keksdose's legacy `plan_budget_limit` reads as `budgets`\n * (`isPlanLimit`, §12.16). Returns the toast's id.\n */\nexport function usePlanLimitToast(options: PlanLimitToastOptions): (hit?: PlanLimitRefusal) => ToastId {\n const { mode, onChoosePlan, email, dimensionLabels, formatValue, labels: labelsProp } = options;\n const labels = useBillingLabels(labelsProp);\n const locale = useKitLocale();\n return useCallback(\n (hit?: PlanLimitRefusal) => {\n const dimension = hit?.dimension ?? \"\";\n const name = dimensionLabels?.[dimension] ?? dimension;\n const figure = (value: number)
|
|
1
|
+
{"version":3,"sources":["../../src/billing/plan-limit-toast.tsx"],"sourcesContent":["import { useCallback } from \"react\";\n\nimport { useKitLocale } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { documentNavigation } from \"../lib/document-navigation\";\nimport { formatNumber } from \"../lib/format\";\nimport { toast } from \"../components/toast\";\nimport type { ToastAction, ToastId } from \"../components/toast\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport type { PlanLimitRefusal } from \"./plan-limit\";\nimport { planLimitMailto } from \"./plan-limit-notice\";\n\nexport interface PlanLimitToastOptions {\n /** `upgrade`: plans are bought — \"Choose a plan\". `contact`: the operator grants them\n * by hand — \"Ask for more\", a mail to support. */\n mode: \"upgrade\" | \"contact\";\n /** `upgrade`: the app's navigation to its subscription page (the kit has no router).\n * Left out, the toast has no action. */\n onChoosePlan?: () => void;\n /** `contact`: support@ the app's domain, opened as `planLimitMailto(email,\n * contactSubject(name))`. Left out, the toast has no action. */\n email?: string;\n /** The app's word per dimension: `{ budgets: \"Budgets\", scans: \"Scans this month\" }`.\n * A dimension without one is named by its key. */\n dimensionLabels?: Readonly<Record<string, string>>;\n /** How a figure is written — kastlan's storage limit is bytes. Default: a number in\n * the kit's locale. */\n formatValue?: (dimension: string, value: number) => string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/**\n * `PlanLimitNotice`'s words as a toast (docs/billing-harmonization.md §14.11), for a\n * refused create with no room for the notice — a row's button, a share target: \"Plan\n * limit reached\", what to do, the figure when the refusal has one (\"Budgets: 3 of 3\"),\n * and the mode's action.\n *\n * const showLimit = usePlanLimitToast({\n * mode: \"upgrade\",\n * onChoosePlan: () => navigate(\"/settings/subscription\"),\n * dimensionLabels: { budgets: t(\"billing.dim_budgets\") },\n * });\n * catch (err) { const hit = isPlanLimit(err); if (hit) showLimit(hit); }\n *\n * One toast per dimension: its id is `plan-limit:<dimension>`, so a repeat replaces it\n * in place instead of stacking a second (keksdose's own hook stacked). Called with no\n * hit, it says the words without a figure, under `plan-limit`. No `limit` in the\n * refusal, no figure line; keksdose's legacy `plan_budget_limit` reads as `budgets`\n * (`isPlanLimit`, §12.16). Returns the toast's id.\n */\nexport function usePlanLimitToast(options: PlanLimitToastOptions): (hit?: PlanLimitRefusal) => ToastId {\n const { mode, onChoosePlan, email, dimensionLabels, formatValue, labels: labelsProp } = options;\n const labels = useBillingLabels(labelsProp);\n const locale = useKitLocale();\n return useCallback(\n (hit?: PlanLimitRefusal) => {\n const dimension = hit?.dimension ?? \"\";\n const name = dimensionLabels?.[dimension] ?? dimension;\n const figure = (value: number) => (formatValue ? formatValue(dimension, value) : formatNumber(value, { locale }));\n // A body without `used` was refused for being AT the limit (PlanLimitNotice's rule).\n const usage =\n hit?.limit !== undefined\n ? labels.limitUsageLine(name, figure(hit.used ?? hit.limit), figure(hit.limit))\n : undefined;\n const action: ToastAction | undefined =\n mode === \"upgrade\"\n ? onChoosePlan && { label: labels.choosePlan, onClick: () => onChoosePlan() }\n : email\n ? {\n label: labels.askForMore,\n // A mailto: hands the address to the reader's mail client; the page stays.\n onClick: () => documentNavigation.replace(planLimitMailto(email, labels.contactSubject(name))),\n }\n : undefined;\n return toast.warning(labels.limitReached, {\n id: dimension ? `plan-limit:${dimension}` : \"plan-limit\",\n description: (\n <span data-plan-limit-mode={mode} data-dimension={dimension || undefined}>\n <span className=\"block\">{mode === \"upgrade\" ? labels.limitUpgrade : labels.limitContact}</span>\n {usage !== undefined && <span className=\"block tabular-nums\">{usage}</span>}\n </span>\n ),\n action,\n });\n },\n [mode, onChoosePlan, email, dimensionLabels, formatValue, labels, locale],\n );\n}\n"],"mappings":";AA+EU,SACE,KADF;AA/EV,SAAS,mBAAmB;AAE5B,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAC7B,SAAS,aAAa;AAEtB,SAAS,wBAAwB;AAGjC,SAAS,uBAAuB;AAyCzB,SAAS,kBAAkB,SAAqE;AACrG,QAAM,EAAE,MAAM,cAAc,OAAO,iBAAiB,aAAa,QAAQ,WAAW,IAAI;AACxF,QAAM,SAAS,iBAAiB,UAAU;AAC1C,QAAM,SAAS,aAAa;AAC5B,SAAO;AAAA,IACL,CAAC,QAA2B;AAC1B,YAAM,YAAY,KAAK,aAAa;AACpC,YAAM,OAAO,kBAAkB,SAAS,KAAK;AAC7C,YAAM,SAAS,CAAC,UAAmB,cAAc,YAAY,WAAW,KAAK,IAAI,aAAa,OAAO,EAAE,OAAO,CAAC;AAE/G,YAAM,QACJ,KAAK,UAAU,SACX,OAAO,eAAe,MAAM,OAAO,IAAI,QAAQ,IAAI,KAAK,GAAG,OAAO,IAAI,KAAK,CAAC,IAC5E;AACN,YAAM,SACJ,SAAS,YACL,gBAAgB,EAAE,OAAO,OAAO,YAAY,SAAS,MAAM,aAAa,EAAE,IAC1E,QACE;AAAA,QACE,OAAO,OAAO;AAAA;AAAA,QAEd,SAAS,MAAM,mBAAmB,QAAQ,gBAAgB,OAAO,OAAO,eAAe,IAAI,CAAC,CAAC;AAAA,MAC/F,IACA;AACR,aAAO,MAAM,QAAQ,OAAO,cAAc;AAAA,QACxC,IAAI,YAAY,cAAc,SAAS,KAAK;AAAA,QAC5C,aACE,qBAAC,UAAK,wBAAsB,MAAM,kBAAgB,aAAa,QAC7D;AAAA,8BAAC,UAAK,WAAU,SAAS,mBAAS,YAAY,OAAO,eAAe,OAAO,cAAa;AAAA,UACvF,UAAU,UAAa,oBAAC,UAAK,WAAU,sBAAsB,iBAAM;AAAA,WACtE;AAAA,QAEF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,CAAC,MAAM,cAAc,OAAO,iBAAiB,aAAa,QAAQ,MAAM;AAAA,EAC1E;AACF;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/billing/plan-limit.ts"],"sourcesContent":["import { errorBodyWithCode } from \"../auth/auth-errors\";\n\n/** What {@link isPlanLimit} reads off a `plan_limit` refusal (docs/billing-harmonization.md\n * §3.4): `402 {detail, code: \"plan_limit\", dimension, plan, limit, used}`. */\nexport interface PlanLimitRefusal {\n /** The limited dimension, as the app's catalogue names it: `\"budgets\"`, `\"seats\"`. */\n dimension: string;\n /** The payer's plan code, lowercased (§12.16: reads compare without case, and\n * keksdose's legacy body still says `\"FREE\"`). `undefined` when the body has none. */\n plan: string | undefined;\n /** The plan's limit for the dimension. `undefined` when the body has none. */\n limit: number | undefined;\n /** How many there are. `undefined` when the body has none — keksdose's legacy\n * `plan_budget_limit` body never had it. */\n used: number | undefined;\n}\n\n/** keksdose's code before §3.4: `402 {detail, code: \"plan_budget_limit\", plan, limit}`. */\nconst LEGACY_BUDGET_LIMIT = \"plan_budget_limit\";\n\nconst text = (value: unknown): string | undefined
|
|
1
|
+
{"version":3,"sources":["../../src/billing/plan-limit.ts"],"sourcesContent":["import { errorBodyWithCode } from \"../auth/auth-errors\";\n\n/** What {@link isPlanLimit} reads off a `plan_limit` refusal (docs/billing-harmonization.md\n * §3.4): `402 {detail, code: \"plan_limit\", dimension, plan, limit, used}`. */\nexport interface PlanLimitRefusal {\n /** The limited dimension, as the app's catalogue names it: `\"budgets\"`, `\"seats\"`. */\n dimension: string;\n /** The payer's plan code, lowercased (§12.16: reads compare without case, and\n * keksdose's legacy body still says `\"FREE\"`). `undefined` when the body has none. */\n plan: string | undefined;\n /** The plan's limit for the dimension. `undefined` when the body has none. */\n limit: number | undefined;\n /** How many there are. `undefined` when the body has none — keksdose's legacy\n * `plan_budget_limit` body never had it. */\n used: number | undefined;\n}\n\n/** keksdose's code before §3.4: `402 {detail, code: \"plan_budget_limit\", plan, limit}`. */\nconst LEGACY_BUDGET_LIMIT = \"plan_budget_limit\";\n\nconst text = (value: unknown): string | undefined => (typeof value === \"string\" && value !== \"\" ? value : undefined);\nconst count = (value: unknown): number | undefined =>\n typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n\n/**\n * The plan-limit refusal `err` carries — `{dimension, plan, limit, used}` — or\n * `undefined` when it is not one (docs/billing-harmonization.md §3.4):\n *\n * catch (err) {\n * const hit = isPlanLimit(err);\n * if (hit) return setLimit(hit); // → <PlanLimitNotice {...hit} mode=\"upgrade\" href=\"/settings/subscription\" />\n * }\n *\n * The body is found where `authErrorCode` finds a code — axios' `err.response.data`,\n * ofetch's `err.data`, a generated client's `err.body`, or `err` itself — and FastAPI's\n * nested `{detail: {code, …}}` too; the extra fields are read beside the code.\n *\n * keksdose's old code, `plan_budget_limit`, is read as `dimension: \"budgets\"` (§12.16):\n * its frontend accepts both codes for a release, because installed PWAs run the previous\n * bundle while the server switches.\n */\nexport function isPlanLimit(err: unknown): PlanLimitRefusal | undefined {\n const body = errorBodyWithCode(err, (code) => code === \"plan_limit\" || code === LEGACY_BUDGET_LIMIT);\n if (!body) return undefined;\n const legacy = body.code === LEGACY_BUDGET_LIMIT;\n return {\n dimension: text(body.dimension) ?? (legacy ? \"budgets\" : \"\"),\n plan: text(body.plan)?.toLowerCase(),\n limit: count(body.limit),\n used: count(body.used),\n };\n}\n"],"mappings":";AAAA,SAAS,yBAAyB;AAkBlC,MAAM,sBAAsB;AAE5B,MAAM,OAAO,CAAC,UAAwC,OAAO,UAAU,YAAY,UAAU,KAAK,QAAQ;AAC1G,MAAM,QAAQ,CAAC,UACb,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAAI,QAAQ;AAmBzD,SAAS,YAAY,KAA4C;AACtE,QAAM,OAAO,kBAAkB,KAAK,CAAC,SAAS,SAAS,gBAAgB,SAAS,mBAAmB;AACnG,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,SAAS,KAAK,SAAS;AAC7B,SAAO;AAAA,IACL,WAAW,KAAK,KAAK,SAAS,MAAM,SAAS,YAAY;AAAA,IACzD,MAAM,KAAK,KAAK,IAAI,GAAG,YAAY;AAAA,IACnC,OAAO,MAAM,KAAK,KAAK;AAAA,IACvB,MAAM,MAAM,KAAK,IAAI;AAAA,EACvB;AACF;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/billing/plan-price.ts"],"sourcesContent":["import { currencyMinorDigits, formatMoney } from \"../lib/format\";\n\n/**\n * Money and periods of the plan catalogue (docs/billing-harmonization.md §3.1, §4, §12.17).\n *\n * Money crosses the wire as INTEGER MINOR UNITS plus an ISO currency, never as a float\n * (§4): 4900 CHF is CHF 49.00. The kit turns it into the reader's notation with\n * `formatMoney`, so no app divides by 100 by hand.\n */\n\n/** A billing period: the provider charges per month or per year. */\nexport type BillingInterval = \"month\" | \"year\";\n\n/** The two currencies every app sells in (§2.6). */\nexport type BillingCurrency = \"CHF\" | \"EUR\";\n\n/**\n * The periods in the order the picker offers them — YEARLY FIRST (§12.17): a Merchant of\n * Record takes about 5 % + 0.50 per transaction, so a small monthly price loses a fifth\n * of itself (CHF 3 a month loses about 22 %). The page leads with the yearly price.\n */\nexport const BILLING_INTERVALS: readonly BillingInterval[] = Object.freeze([\"year\", \"month\"]);\n\n/** One currency's prices, in minor units, per period. A period left out is not offered. */\nexport type PlanIntervalPrices = Partial<Record<BillingInterval, number>>;\n\n/** A plan's prices as server-kit's `PlanSpec.prices` sends them: currency → period →\n * minor units, GROSS (§12.17). */\nexport type PlanPrices = Partial<Record<BillingCurrency, PlanIntervalPrices>>;\n\n/**\n * What one plan costs in one currency and period:\n * - `price` — a gross amount in minor units;\n * - `free` — a plan with no prices at all (keksdose's free plan), or a price of 0;\n * - `not-offered` — a plan with prices, but none for this currency and period.\n */\nexport type PlanPriceAnswer
|
|
1
|
+
{"version":3,"sources":["../../src/billing/plan-price.ts"],"sourcesContent":["import { currencyMinorDigits, formatMoney } from \"../lib/format\";\n\n/**\n * Money and periods of the plan catalogue (docs/billing-harmonization.md §3.1, §4, §12.17).\n *\n * Money crosses the wire as INTEGER MINOR UNITS plus an ISO currency, never as a float\n * (§4): 4900 CHF is CHF 49.00. The kit turns it into the reader's notation with\n * `formatMoney`, so no app divides by 100 by hand.\n */\n\n/** A billing period: the provider charges per month or per year. */\nexport type BillingInterval = \"month\" | \"year\";\n\n/** The two currencies every app sells in (§2.6). */\nexport type BillingCurrency = \"CHF\" | \"EUR\";\n\n/**\n * The periods in the order the picker offers them — YEARLY FIRST (§12.17): a Merchant of\n * Record takes about 5 % + 0.50 per transaction, so a small monthly price loses a fifth\n * of itself (CHF 3 a month loses about 22 %). The page leads with the yearly price.\n */\nexport const BILLING_INTERVALS: readonly BillingInterval[] = Object.freeze([\"year\", \"month\"]);\n\n/** One currency's prices, in minor units, per period. A period left out is not offered. */\nexport type PlanIntervalPrices = Partial<Record<BillingInterval, number>>;\n\n/** A plan's prices as server-kit's `PlanSpec.prices` sends them: currency → period →\n * minor units, GROSS (§12.17). */\nexport type PlanPrices = Partial<Record<BillingCurrency, PlanIntervalPrices>>;\n\n/**\n * What one plan costs in one currency and period:\n * - `price` — a gross amount in minor units;\n * - `free` — a plan with no prices at all (keksdose's free plan), or a price of 0;\n * - `not-offered` — a plan with prices, but none for this currency and period.\n */\nexport type PlanPriceAnswer = { kind: \"price\"; minor: number } | { kind: \"free\" } | { kind: \"not-offered\" };\n\n/** Whether a price table holds any figure at all. */\nfunction hasAnyPrice(prices: PlanPrices | null | undefined): boolean {\n if (!prices) return false;\n return Object.values(prices).some(\n (byInterval) => byInterval && Object.values(byInterval).some((v) => typeof v === \"number\"),\n );\n}\n\n/** What `prices` says for `currency` and `interval` — see {@link PlanPriceAnswer}. */\nexport function planPrice(\n prices: PlanPrices | null | undefined,\n currency: BillingCurrency,\n interval: BillingInterval,\n): PlanPriceAnswer {\n const minor = prices?.[currency]?.[interval];\n if (typeof minor === \"number\" && Number.isFinite(minor)) {\n return minor === 0 ? { kind: \"free\" } : { kind: \"price\", minor };\n }\n return hasAnyPrice(prices) ? { kind: \"not-offered\" } : { kind: \"free\" };\n}\n\n/** Minor units as the amount: 4900 CHF → 49, 1250 EUR → 12.5. The currency's minor unit\n * comes from `Intl` (CHF and EUR have 2); a code `Intl` cannot read counts as 2. */\nexport function minorToMajor(minor: number, currency: string): number {\n const digits = currencyMinorDigits(currency) ?? 2;\n return minor / 10 ** digits;\n}\n\n/**\n * A price in minor units, in the reader's notation: \"CHF 49\", \"CHF 4.90\", \"49 €\",\n * \"4,90 €\". A whole amount drops its decimals — a price list reads \"CHF 49\" — and any\n * other keeps all of them (\"CHF 4.90\", never \"CHF 4.9\").\n */\nexport function formatPlanPrice(minor: number, currency: string, options: { locale?: string } = {}): string {\n const digits = currencyMinorDigits(currency) ?? 2;\n const whole = Number.isInteger(minor) && minor % 10 ** digits === 0;\n return formatMoney(minorToMajor(minor, currency), currency, {\n locale: options.locale,\n digits: whole ? 0 : digits,\n });\n}\n\n/**\n * The currency a payer starts with when the app stores none (§12.18): francs where the\n * locale's region is Switzerland or Liechtenstein, euros everywhere else — \"de-CH\" and\n * \"fr-CH\" CHF, \"de\" (Germany, once `Intl` fills the region in) and \"it\" EUR.\n *\n * Only the fallback: an account's stored currency comes first (keksdose's\n * `reporting_currency` — a de-CH speaker living in Germany pays in euros), and the payer\n * may switch in the picker either way; checkout takes the one they leave it on (§4).\n */\nexport function billingCurrencyFor(locale?: string): BillingCurrency {\n if (!locale) return \"EUR\";\n try {\n const region = new Intl.Locale(locale).maximize().region;\n return region === \"CH\" || region === \"LI\" ? \"CHF\" : \"EUR\";\n } catch {\n return \"EUR\";\n }\n}\n"],"mappings":";AAAA,SAAS,qBAAqB,mBAAmB;AAqB1C,MAAM,oBAAgD,OAAO,OAAO,CAAC,QAAQ,OAAO,CAAC;AAkB5F,SAAS,YAAY,QAAgD;AACnE,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,OAAO,OAAO,MAAM,EAAE;AAAA,IAC3B,CAAC,eAAe,cAAc,OAAO,OAAO,UAAU,EAAE,KAAK,CAAC,MAAM,OAAO,MAAM,QAAQ;AAAA,EAC3F;AACF;AAGO,SAAS,UACd,QACA,UACA,UACiB;AACjB,QAAM,QAAQ,SAAS,QAAQ,IAAI,QAAQ;AAC3C,MAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GAAG;AACvD,WAAO,UAAU,IAAI,EAAE,MAAM,OAAO,IAAI,EAAE,MAAM,SAAS,MAAM;AAAA,EACjE;AACA,SAAO,YAAY,MAAM,IAAI,EAAE,MAAM,cAAc,IAAI,EAAE,MAAM,OAAO;AACxE;AAIO,SAAS,aAAa,OAAe,UAA0B;AACpE,QAAM,SAAS,oBAAoB,QAAQ,KAAK;AAChD,SAAO,QAAQ,MAAM;AACvB;AAOO,SAAS,gBAAgB,OAAe,UAAkB,UAA+B,CAAC,GAAW;AAC1G,QAAM,SAAS,oBAAoB,QAAQ,KAAK;AAChD,QAAM,QAAQ,OAAO,UAAU,KAAK,KAAK,QAAQ,MAAM,WAAW;AAClE,SAAO,YAAY,aAAa,OAAO,QAAQ,GAAG,UAAU;AAAA,IAC1D,QAAQ,QAAQ;AAAA,IAChB,QAAQ,QAAQ,IAAI;AAAA,EACtB,CAAC;AACH;AAWO,SAAS,mBAAmB,QAAkC;AACnE,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI;AACF,UAAM,SAAS,IAAI,KAAK,OAAO,MAAM,EAAE,SAAS,EAAE;AAClD,WAAO,WAAW,QAAQ,WAAW,OAAO,QAAQ;AAAA,EACtD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/billing/plan-switch.tsx"],"sourcesContent":["import type { KeyboardEvent } from \"react\";\n\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_COMMON_LABELS } from \"../i18n/shared-labels\";\nimport { pluralForm } from \"../i18n/plurals\";\nimport type { PluralForms } from \"../i18n/plurals\";\nimport { formatDate, formatMoney, formatNumber } from \"../lib/format\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { Spinner } from \"../components/spinner\";\nimport { COMMIT_EXCEPT_BILLING } from \"../components/write-lock\";\nimport { DEFAULT_PLAN_CHANGE_LABELS } from \"../admin/plan-change-labels\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport type { PlanSwitchPreview } from \"./billing-overview\";\nimport { minorToMajor } from \"./plan-price\";\nimport type { BillingInterval } from \"./plan-price\";\n\n/**\n * The payer's plan switch (docs/billing-harmonization.md §15.2.10, ui-kit 0.39): built\n * once in the kits and used by all three apps, instead of Paddle's portal. While a\n * provider subscription runs, the app sends a `PlanPicker` pick to\n * `POST /billing/plan/preview` and opens this dialog with the answer; Confirm is the\n * app's `POST /billing/plan`. Otherwise the pick goes to the checkout, as before.\n *\n * The switch is immediate and prorated: an upgrade charges the difference now, a\n * downgrade credits the unused part to the customer's balance, which Paddle takes off\n * the next charges. So the payer agrees to a figure — Paddle's preview — and sees the\n * dimensions over the new plan's limit before agreeing (§3.4: a limit gates creation\n * only, so the switch is allowed).\n */\n\n/** One side of a switch: a plan code and its billing period. */\nexport interface PlanSwitchSide {\n plan: string;\n interval: BillingInterval;\n}\n\nexport interface PlanSwitchConfirmProps {\n open: boolean;\n /** `false` when the payer dismisses it — Cancel, Escape, the backdrop. Confirm leaves\n * it open: close it once the switch answers. */\n onOpenChange: (open: boolean) => void;\n /** What the payer pays now: the overview's `paid`. */\n from: PlanSwitchSide;\n /** The pick. */\n to: PlanSwitchSide;\n /** `POST /billing/plan/preview`'s answer. Undefined while it loads: a spinner, and\n * Confirm is held. */\n preview?: PlanSwitchPreview;\n /** `POST /billing/plan` is under way: Confirm's spinner, no second press, and Escape\n * doesn't close. */\n pending?: boolean;\n /** The plan's display name, the app's (§3.1). Default: the code. */\n planName?: (code: string) => string;\n /**\n * The app's word per dimension, for the over-limit lines. A heading's word (\"Budgets\")\n * reads \"Budgets: 7 of 5\" (`billing.limitUsageLine`); its plural forms\n * (`{ one: \"budget\", other: \"budgets\" }`) read \"7 of 5 budgets\" (`planChange.usageOf`,\n * the form the limit takes). A dimension without one is named by its key.\n */\n dimensionLabels?: Readonly<Record<string, string | PluralForms>>;\n /** How a usage or limit figure is written. Default: a number in the locale. */\n formatLimit?: (dimension: string, value: number) => string;\n /** Confirm: the app's `POST /billing/plan` with `{plan, interval}` of `to`. */\n onConfirm: () => void;\n /** The figures' and the date's locale. Default: the provider's. */\n locale?: string;\n /**\n * 0.42 (§16.2 decision 5, §16.8 B): the payer's free grant runs without an end —\n * `isPermanentGrant(overview)`. The switch changes the PAID plan, not the applied one\n * (§12.11), so the dialog says so above its figures (`grantWarningTitle`,\n * `grantWarningBody`) before the payer agrees. Confirm stays Confirm: the warning is\n * this dialog's own, and `PlanPicker` — told `switching` — shows none of its own on\n * the way here, so the payer reads it once.\n */\n permanentGrant?: boolean;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/**\n * The confirm for a plan switch (§15.2.10): from → to, each with its billing period;\n * Paddle's preview — \"Charged now\" (an upgrade) or \"Credited towards your next charges\"\n * (a downgrade), and the next charge with its date — in the subscription's currency;\n * the dimensions over the new plan's limit; and Confirm.\n *\n * const preview = useMutation({ mutationFn: api.previewPlanSwitch });\n * <PlanSwitchConfirm\n * open={pick !== null}\n * onOpenChange={(open) => !open && setPick(null)}\n * from={overview.paid}\n * to={pick}\n * preview={preview.data}\n * pending={switching.isPending}\n * planName={planName}\n * onConfirm={() => switching.mutate(pick)}\n * />\n *\n * It takes no billing lock (`COMMIT_EXCEPT_BILLING` built in): a read-only payer may\n * change plan, as they may pay. A demo lock still holds it, as the server's demo guard\n * does. The kit sends no request.\n */\nexport function PlanSwitchConfirm({\n open,\n onOpenChange,\n from,\n to,\n preview,\n pending = false,\n planName = (code) => code,\n dimensionLabels,\n formatLimit,\n onConfirm,\n locale: localeProp,\n permanentGrant = false,\n labels: labelsProp,\n}: PlanSwitchConfirmProps) {\n const labels = useBillingLabels(labelsProp);\n const planChange = useKitLabels(\"planChange\", DEFAULT_PLAN_CHANGE_LABELS);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale(localeProp);\n\n const period = (interval: BillingInterval) => (interval === \"year\" ? labels.yearly : labels.monthly);\n const side = (s: PlanSwitchSide) => `${planName(s.plan.toLowerCase())} (${period(s.interval)})`;\n const money = (minor: number, currency: string) =>\n formatMoney(minorToMajor(minor, currency), currency, { locale });\n const figure = (dimension: string, value: number) =>\n formatLimit ? formatLimit(dimension, value) : formatNumber(value, { locale });\n\n /** One line per dimension over the new limit, in the word's own sentence. */\n const overLimit = (p: PlanSwitchPreview): string[] =>\n (p.over_limit ?? []).flatMap((dimension) => {\n const limit = p.limits?.[dimension];\n const used = p.usage?.[dimension];\n if (limit === null || limit === undefined || used === undefined) return [];\n const word = dimensionLabels?.[dimension] ?? dimension;\n return [\n typeof word === \"string\"\n ? labels.limitUsageLine(word, figure(dimension, used), figure(dimension, limit))\n : planChange.usageOf(figure(dimension, used), figure(dimension, limit), pluralForm(limit, word, locale)),\n ];\n });\n\n const credit = preview !== undefined && preview.now_minor < 0;\n const lines = preview ? overLimit(preview) : [];\n\n return (\n <DialogFrame\n open={open}\n title={labels.switchTitle}\n description={labels.switchFromTo(side(from), side(to))}\n onClose={() => onOpenChange(false)}\n onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {\n if (pending && e.key === \"Escape\") e.preventDefault();\n }}\n aria-busy={preview === undefined || pending || undefined}\n actions={(close) => (\n <>\n <Button variant=\"secondary\" onClick={close} disabled={pending}>\n {common.cancel}\n </Button>\n <Button\n commit={COMMIT_EXCEPT_BILLING}\n pending={pending}\n disabled={preview === undefined}\n onClick={() => {\n if (preview !== undefined && !pending) onConfirm();\n }}\n >\n {labels.switchConfirm}\n </Button>\n </>\n )}\n >\n {permanentGrant && (\n <AlertBanner tone=\"warning\" size=\"sm\" data-grant-warning=\"\">\n <strong className=\"block font-medium\">{labels.grantWarningTitle}</strong> {labels.grantWarningBody}\n </AlertBanner>\n )}\n {preview === undefined ? (\n <div className=\"flex justify-center py-4\">\n <Spinner showLabel />\n </div>\n ) : (\n <>\n <dl className=\"grid grid-cols-[minmax(0,1fr)_auto] gap-x-4 gap-y-1 text-sm\" data-plan-switch-preview>\n <dt className=\"text-secondary\">{credit ? labels.switchCredited : labels.switchChargedNow}</dt>\n <dd className=\"text-right font-medium tabular-nums\" data-now={credit ? \"credit\" : \"charge\"}>\n {money(Math.abs(preview.now_minor), preview.currency)}\n </dd>\n {preview.next_billed_at && preview.next_minor != null && (\n <>\n <dt className=\"text-secondary\">\n {labels.switchNextCharge(formatDate(preview.next_billed_at, \"medium\", { locale }))}\n </dt>\n <dd className=\"text-right tabular-nums\">{money(preview.next_minor, preview.currency)}</dd>\n </>\n )}\n </dl>\n {lines.length > 0 && (\n <div className=\"space-y-1 text-sm\" data-plan-switch-over-limit>\n <p>{planChange.overLimit}</p>\n <ul className=\"list-disc space-y-0.5 ps-5 text-secondary\">\n {lines.map((line) => (\n <li key={line}>{line}</li>\n ))}\n </ul>\n </div>\n )}\n </>\n )}\n </DialogFrame>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/billing/plan-switch.tsx"],"sourcesContent":["import type { KeyboardEvent } from \"react\";\n\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_COMMON_LABELS } from \"../i18n/shared-labels\";\nimport { pluralForm } from \"../i18n/plurals\";\nimport type { PluralForms } from \"../i18n/plurals\";\nimport { formatDate, formatMoney, formatNumber } from \"../lib/format\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { Spinner } from \"../components/spinner\";\nimport { COMMIT_EXCEPT_BILLING } from \"../components/write-lock\";\nimport { DEFAULT_PLAN_CHANGE_LABELS } from \"../admin/plan-change-labels\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport type { PlanSwitchPreview } from \"./billing-overview\";\nimport { minorToMajor } from \"./plan-price\";\nimport type { BillingInterval } from \"./plan-price\";\n\n/**\n * The payer's plan switch (docs/billing-harmonization.md §15.2.10, ui-kit 0.39): built\n * once in the kits and used by all three apps, instead of Paddle's portal. While a\n * provider subscription runs, the app sends a `PlanPicker` pick to\n * `POST /billing/plan/preview` and opens this dialog with the answer; Confirm is the\n * app's `POST /billing/plan`. Otherwise the pick goes to the checkout, as before.\n *\n * The switch is immediate and prorated: an upgrade charges the difference now, a\n * downgrade credits the unused part to the customer's balance, which Paddle takes off\n * the next charges. So the payer agrees to a figure — Paddle's preview — and sees the\n * dimensions over the new plan's limit before agreeing (§3.4: a limit gates creation\n * only, so the switch is allowed).\n */\n\n/** One side of a switch: a plan code and its billing period. */\nexport interface PlanSwitchSide {\n plan: string;\n interval: BillingInterval;\n}\n\nexport interface PlanSwitchConfirmProps {\n open: boolean;\n /** `false` when the payer dismisses it — Cancel, Escape, the backdrop. Confirm leaves\n * it open: close it once the switch answers. */\n onOpenChange: (open: boolean) => void;\n /** What the payer pays now: the overview's `paid`. */\n from: PlanSwitchSide;\n /** The pick. */\n to: PlanSwitchSide;\n /** `POST /billing/plan/preview`'s answer. Undefined while it loads: a spinner, and\n * Confirm is held. */\n preview?: PlanSwitchPreview;\n /** `POST /billing/plan` is under way: Confirm's spinner, no second press, and Escape\n * doesn't close. */\n pending?: boolean;\n /** The plan's display name, the app's (§3.1). Default: the code. */\n planName?: (code: string) => string;\n /**\n * The app's word per dimension, for the over-limit lines. A heading's word (\"Budgets\")\n * reads \"Budgets: 7 of 5\" (`billing.limitUsageLine`); its plural forms\n * (`{ one: \"budget\", other: \"budgets\" }`) read \"7 of 5 budgets\" (`planChange.usageOf`,\n * the form the limit takes). A dimension without one is named by its key.\n */\n dimensionLabels?: Readonly<Record<string, string | PluralForms>>;\n /** How a usage or limit figure is written. Default: a number in the locale. */\n formatLimit?: (dimension: string, value: number) => string;\n /** Confirm: the app's `POST /billing/plan` with `{plan, interval}` of `to`. */\n onConfirm: () => void;\n /** The figures' and the date's locale. Default: the provider's. */\n locale?: string;\n /**\n * 0.42 (§16.2 decision 5, §16.8 B): the payer's free grant runs without an end —\n * `isPermanentGrant(overview)`. The switch changes the PAID plan, not the applied one\n * (§12.11), so the dialog says so above its figures (`grantWarningTitle`,\n * `grantWarningBody`) before the payer agrees. Confirm stays Confirm: the warning is\n * this dialog's own, and `PlanPicker` — told `switching` — shows none of its own on\n * the way here, so the payer reads it once.\n */\n permanentGrant?: boolean;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/**\n * The confirm for a plan switch (§15.2.10): from → to, each with its billing period;\n * Paddle's preview — \"Charged now\" (an upgrade) or \"Credited towards your next charges\"\n * (a downgrade), and the next charge with its date — in the subscription's currency;\n * the dimensions over the new plan's limit; and Confirm.\n *\n * const preview = useMutation({ mutationFn: api.previewPlanSwitch });\n * <PlanSwitchConfirm\n * open={pick !== null}\n * onOpenChange={(open) => !open && setPick(null)}\n * from={overview.paid}\n * to={pick}\n * preview={preview.data}\n * pending={switching.isPending}\n * planName={planName}\n * onConfirm={() => switching.mutate(pick)}\n * />\n *\n * It takes no billing lock (`COMMIT_EXCEPT_BILLING` built in): a read-only payer may\n * change plan, as they may pay. A demo lock still holds it, as the server's demo guard\n * does. The kit sends no request.\n */\nexport function PlanSwitchConfirm({\n open,\n onOpenChange,\n from,\n to,\n preview,\n pending = false,\n planName = (code) => code,\n dimensionLabels,\n formatLimit,\n onConfirm,\n locale: localeProp,\n permanentGrant = false,\n labels: labelsProp,\n}: PlanSwitchConfirmProps) {\n const labels = useBillingLabels(labelsProp);\n const planChange = useKitLabels(\"planChange\", DEFAULT_PLAN_CHANGE_LABELS);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale(localeProp);\n\n const period = (interval: BillingInterval) => (interval === \"year\" ? labels.yearly : labels.monthly);\n const side = (s: PlanSwitchSide) => `${planName(s.plan.toLowerCase())} (${period(s.interval)})`;\n const money = (minor: number, currency: string) => formatMoney(minorToMajor(minor, currency), currency, { locale });\n const figure = (dimension: string, value: number) =>\n formatLimit ? formatLimit(dimension, value) : formatNumber(value, { locale });\n\n /** One line per dimension over the new limit, in the word's own sentence. */\n const overLimit = (p: PlanSwitchPreview): string[] =>\n (p.over_limit ?? []).flatMap((dimension) => {\n const limit = p.limits?.[dimension];\n const used = p.usage?.[dimension];\n if (limit === null || limit === undefined || used === undefined) return [];\n const word = dimensionLabels?.[dimension] ?? dimension;\n return [\n typeof word === \"string\"\n ? labels.limitUsageLine(word, figure(dimension, used), figure(dimension, limit))\n : planChange.usageOf(figure(dimension, used), figure(dimension, limit), pluralForm(limit, word, locale)),\n ];\n });\n\n const credit = preview !== undefined && preview.now_minor < 0;\n const lines = preview ? overLimit(preview) : [];\n\n return (\n <DialogFrame\n open={open}\n title={labels.switchTitle}\n description={labels.switchFromTo(side(from), side(to))}\n onClose={() => onOpenChange(false)}\n onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {\n if (pending && e.key === \"Escape\") e.preventDefault();\n }}\n aria-busy={preview === undefined || pending || undefined}\n actions={(close) => (\n <>\n <Button variant=\"secondary\" onClick={close} disabled={pending}>\n {common.cancel}\n </Button>\n <Button\n commit={COMMIT_EXCEPT_BILLING}\n pending={pending}\n disabled={preview === undefined}\n onClick={() => {\n if (preview !== undefined && !pending) onConfirm();\n }}\n >\n {labels.switchConfirm}\n </Button>\n </>\n )}\n >\n {permanentGrant && (\n <AlertBanner tone=\"warning\" size=\"sm\" data-grant-warning=\"\">\n <strong className=\"block font-medium\">{labels.grantWarningTitle}</strong> {labels.grantWarningBody}\n </AlertBanner>\n )}\n {preview === undefined ? (\n <div className=\"flex justify-center py-4\">\n <Spinner showLabel />\n </div>\n ) : (\n <>\n <dl className=\"grid grid-cols-[minmax(0,1fr)_auto] gap-x-4 gap-y-1 text-sm\" data-plan-switch-preview>\n <dt className=\"text-secondary\">{credit ? labels.switchCredited : labels.switchChargedNow}</dt>\n <dd className=\"text-right font-medium tabular-nums\" data-now={credit ? \"credit\" : \"charge\"}>\n {money(Math.abs(preview.now_minor), preview.currency)}\n </dd>\n {preview.next_billed_at && preview.next_minor != null && (\n <>\n <dt className=\"text-secondary\">\n {labels.switchNextCharge(formatDate(preview.next_billed_at, \"medium\", { locale }))}\n </dt>\n <dd className=\"text-right tabular-nums\">{money(preview.next_minor, preview.currency)}</dd>\n </>\n )}\n </dl>\n {lines.length > 0 && (\n <div className=\"space-y-1 text-sm\" data-plan-switch-over-limit>\n <p>{planChange.overLimit}</p>\n <ul className=\"list-disc space-y-0.5 ps-5 text-secondary\">\n {lines.map((line) => (\n <li key={line}>{line}</li>\n ))}\n </ul>\n </div>\n )}\n </>\n )}\n </DialogFrame>\n );\n}\n"],"mappings":"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","names":[]}
|