@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/auth/sign-in-form.tsx"],"sourcesContent":["import { useEffect, useEffectEvent, useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, FormEvent, ReactNode } from \"react\";\nimport { KeyRound } from \"lucide-react\";\n\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { hasContent } from \"../lib/presence\";\nimport { CurrentPasswordInput } from \"../components/danger-confirm\";\nimport { OneTimeCodeInput } from \"../components/one-time-code-input\";\nimport { TextLink } from \"../components/text-link\";\nimport { Button } from \"../components/button\";\nimport { Input } from \"../components/input\";\nimport { accessAction } from \"../landing/access\";\nimport type { AccessChoice } from \"../landing/access\";\nimport { useLandingLabels } from \"../landing/landing-labels\";\nimport { authErrorCode, isRateLimited, retryAfterSeconds } from \"./auth-errors\";\nimport type { KitErrorCode } from \"./auth-errors\";\nimport { taggedEmail } from \"./email-tag\";\nimport { newPasswordProblem } from \"./form-rules\";\nimport { NewPasswordFields } from \"./new-password-fields\";\nimport { DEFAULT_SIGN_IN_LABELS } from \"./sign-in-form-labels\";\n\n/* ── The answers (§5.1) ──────────────────────────────────────────────────── */\n\n/**\n * What a sign-in call answers, in the kit's words — docs/auth-harmonization.md §5.1,\n * keksdose's three shapes. The app maps its own HTTP answer to one of these and resolves\n * with it; a refusal REJECTS (with whatever its client throws — {@link authErrorCode}\n * reads the code out of it).\n *\n * | the server answers (keksdose) | resolve with |\n * |---|---|\n * | `TokenResponse` `{access_token, refresh_token, token_type, user}` | `{ kind: \"signed-in\" }`, after the app has stored the session |\n * | `{requires_2fa: true, challenge_token}` | `{ kind: \"2fa\", challengeToken: challenge_token }` |\n * | `{requires_password_change: true, challenge_token, encrypted}` | `{ kind: \"password-change\", challengeToken: challenge_token, extra: { encrypted } }` |\n * | `401 {code: \"invalid_credentials\"}` | reject — let the client's error through |\n *\n * ```ts\n * async function toAnswer(res: TokenResponse | TwoFactorChallenge | PasswordChangeChallenge): Promise<SignInAnswer> {\n * if (\"requires_2fa\" in res) return { kind: \"2fa\", challengeToken: res.challenge_token };\n * if (\"requires_password_change\" in res)\n * return { kind: \"password-change\", challengeToken: res.challenge_token, extra: { encrypted: res.encrypted } };\n * await onLoginSuccess(res); // store tokens, load /auth/me, switch language\n * return { kind: \"signed-in\" };\n * }\n * <SignInForm onSubmit={async (v) => toAnswer(await authApi.login(v))} … />\n * ```\n *\n * The same mapping serves `/auth/login/2fa`, `/auth/login/set-password` and the passkey\n * finish, which answer the same union.\n */\nexport type SignInAnswer =\n | { kind: \"signed-in\" }\n | { kind: \"2fa\"; challengeToken: string }\n | {\n kind: \"password-change\";\n challengeToken: string;\n /** The challenge's app fields, passed through untouched — keksdose's `encrypted`,\n * which its note on this step reads (`passwordChangeContent`). */\n extra?: Readonly<Record<string, unknown>>;\n };\n\n/** The step on screen: the credentials, the second factor, or a new password. */\nexport type SignInStep = \"credentials\" | \"2fa\" | \"password-change\";\n\n/** What the person did when a call failed — {@link SignInFormProps.describeError}'s\n * second argument. */\nexport type SignInAction = \"password\" | \"passkey\" | \"code\" | \"set-password\";\n\nexport interface SignInCredentials {\n /** Trimmed. Lower-casing is the server's (§3.1). */\n email: string;\n /** As typed, never trimmed. */\n password: string;\n}\n\nexport interface SignInCodeValues {\n challengeToken: string;\n /** Digits only — {@link OneTimeCodeInput} drops the spaces of \"123 456\". A backup code\n * (`kind: \"recovery\"`) is trimmed and otherwise as typed: its dashes and case are the\n * server's to normalise, as kastlan's `_normalise_backup_code` does. */\n code: string;\n /**\n * 0.30.0: `\"recovery\"` when the person typed one of their backup codes instead\n * (`recoveryCode`); absent for the authenticator's code, as before. kastlan's\n * `/auth/login/2fa` takes either in the same `code` and tells them apart itself; an\n * app whose server takes a backup code elsewhere routes on this.\n */\n kind?: \"recovery\";\n}\n\nexport interface SignInNewPasswordValues {\n challengeToken: string;\n password: string;\n}\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/**\n * Every string `SignInForm` renders — the `signIn` namespace of `<UiKitProvider\n * labels>`, overridable per instance through `labels`. keksdose's words where it has\n * them (`auth.*` in its catalogue).\n */\nexport interface SignInLabels {\n email: string;\n password: string;\n /** The submit button. */\n submit: string;\n /** The passkey button (`onPasskey`). */\n passkey: string;\n /** The link to `forgotHref`. */\n forgotPassword: string;\n /** Before the link to `registerHref`. */\n noAccount: string;\n /** The link to `registerHref`. */\n register: string;\n /** The ONE answer to an unknown address, a wrong password and a deactivated account\n * (`invalid_credentials`, §5.2). */\n invalidCredentials: string;\n /** Any other failed sign-in (the network, a 500). */\n failed: string;\n /** A failed or refused passkey sign-in. */\n passkeyFailed: string;\n /**\n * The hint under `invalidCredentials` (§2.10), shown when `deactivatedContact` is\n * given. A TEMPLATE: `{contact}` is replaced by the contact — a `mailto:` link for an\n * address — so a language puts it where its grammar needs it.\n */\n deactivatedHint: string;\n /** The hint under `invalidCredentials` when `emailTag` is given (§4.5), given the\n * typed address with the app's tag: \"Signed up with ada+kastlan@example.com?\". */\n tagHint: (taggedAddress: string) => string;\n /** The second-factor step's heading. */\n twoFactorTitle: string;\n twoFactorIntro: string;\n /** The code field's label. */\n code: string;\n /** The code step's submit button. */\n verify: string;\n /** The code was refused. */\n codeInvalid: string;\n /** The set-a-new-password step's heading. */\n setPasswordTitle: string;\n setPasswordIntro: string;\n newPassword: string;\n confirmPassword: string;\n passwordMismatch: string;\n /** The set-a-new-password step's submit button. */\n setPasswordSubmit: string;\n setPasswordFailed: string;\n /** A challenge was refused as `token_invalid` — it expired between the steps; the\n * form is back on the credentials. */\n expired: string;\n /** From the second and third step back to the credentials. */\n backToSignIn: string;\n /*\n * 0.30.0 — OPTIONAL, like `TwoFactorSettingLabels.qrAlt` and for its reason: a new\n * required key is a compile error at every call site that annotates this interface\n * (CONTRIBUTING's additive-API rule). Each falls back to the provider's `signIn`, then\n * English.\n */\n /** The second-factor step's switch to a backup code (`recoveryCode`). */\n useRecoveryCode?: string;\n /** The switch back, from the backup-code field to the authenticator's code. */\n useAuthenticatorCode?: string;\n /** The backup-code field's label. */\n recoveryCode?: string;\n /** The step's intro while a backup code is asked for, in place of `twoFactorIntro`. */\n recoveryIntro?: string;\n /** Under the backup-code field, given how many letters and digits a code has. */\n recoveryCodeHint?: (length: number) => string;\n /** A refused backup code — wrong, or spent already (each one works once). */\n recoveryCodeInvalid?: string;\n /** Any step's request was throttled — HTTP 429 ({@link isRateLimited}) — given the\n * wait its `Retry-After` asks for, in seconds ({@link retryAfterSeconds}), or\n * `undefined` (or `0`) without one. */\n rateLimited?: (seconds?: number) => string;\n /** 0.31.0: the link after `noAccount` for an invitation-only app (`access` of kind\n * `request`) — a mail to support, in place of `register`. */\n requestAccess?: string;\n}\n\n// Defined in ./sign-in-form-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_SIGN_IN_LABELS };\n\n/* ── Props ───────────────────────────────────────────────────────────────── */\n\nexport interface SignInFormProps extends Omit<ComponentPropsWithRef<\"div\">, \"onSubmit\" | \"children\"> {\n /**\n * The credentials step was submitted. Send them, and resolve with the answer\n * ({@link SignInAnswer}) or reject with the client's error. The form never sends a\n * request itself (§8): keksdose must see the password on its way, to refuse the same\n * string as an encryption passphrase later.\n *\n * On `signed-in` the form's job is over — it stays busy while the app navigates.\n */\n onSubmit: (credentials: SignInCredentials) => Promise<SignInAnswer>;\n /** The second factor was submitted (`/auth/login/2fa`). Resolves with the next answer\n * — keksdose may still require a new password after the code. */\n onCode: (values: SignInCodeValues) => Promise<SignInAnswer>;\n /** The new password was submitted (`/auth/login/set-password`). Resolves with the\n * answer, `signed-in` in practice; keksdose's `key_wrap_unchanged` note is the app's\n * to show from here. */\n onSetPassword: (values: SignInNewPasswordValues) => Promise<SignInAnswer>;\n /**\n * The passkey button was pressed: run the ceremony (`/auth/passkey/login/begin|finish`)\n * and resolve with its answer. It asks for NO email — a passkey is bound to the site,\n * and the browser offers every one it holds (keksdose #126). Left out, no button: pass\n * it only where `PublicKeyCredential` exists.\n */\n onPasskey?: () => Promise<SignInAnswer>;\n /**\n * Passkey autofill (conditional mediation). The email field is\n * `autoComplete=\"username webauthn\"`, so a pending `navigator.credentials.get({\n * mediation: \"conditional\" })` lists the site's passkeys in its dropdown. The form\n * calls this ONCE on mount; arm the request there and resolve with its answer when the\n * person picks a passkey, or with `null` when nothing happens. The signal aborts on\n * unmount — cancel the pending request then (keksdose: `armPasskeyAutofill(onDone,\n * signal)` + `cancelPasskeyAutofill()`). A rejection is silent: the button stays the\n * reliable path.\n */\n passkeyAutofill?: (signal: AbortSignal) => Promise<SignInAnswer | null | undefined>;\n /** \"Forgot your password?\" goes here — the app's `/forgot-password`. Left out, no\n * link. */\n forgotHref?: string;\n /** \"No account yet? Create account\" goes here — `/register`. Left out, no line. */\n registerHref?: string;\n /**\n * 0.31.0 (docs/landing-demo-harmonization.md §4.2): how a visitor gets an account —\n * the same value the landing's `PublicHeader` and `Hero` take. `{kind: \"request\",\n * email}` turns the line into \"No account yet? Request access\", a mail to support with\n * the subject and body of `accessAction` (every app is invitation-only); `{kind:\n * \"register\", href}` keeps \"Create account\", to `href`. Wins over `registerHref`; left\n * out, `registerHref` works as before.\n */\n access?: AccessChoice;\n /** The email field's start value — the reset page ends on \"Sign in\" with the address\n * filled in (§6.3). */\n defaultEmail?: string;\n /**\n * The app's address tag (`\"kastlan\"`, §4.5). After `invalid_credentials` the form also\n * says \"Signed up with you+kastlan@example.com? Use that address.\" — sign-in stays\n * exact, and the server looks nothing up.\n */\n emailTag?: string;\n /**\n * Who a deactivated user writes to (§2.10): an address (drawn as a `mailto:` link) or\n * the app's own node. Shown after `invalid_credentials` as \"Account deactivated? Write\n * to …\". Left out, no hint.\n */\n deactivatedContact?: ReactNode;\n /** Digits of the second factor. Default 6 (TOTP). */\n codeLength?: number;\n /**\n * 0.30.0: offer a backup code on the second-factor step (kastlan's 0.29 adoption: its\n * `/auth/login/2fa` takes one of the eight codes minted at 2FA setup, and the kit's\n * step had no way to type one). A \"Use a backup code\" switch under the code field\n * swaps the digits-only {@link OneTimeCodeInput} for a plain text field; the code goes\n * to `onCode` with the same challenge and `kind: \"recovery\"`.\n *\n * `true` for a 16-character code (kastlan's `ABCD-EFGH-JKLM-NPQR`), or `{ length }`.\n * The length counts letters and digits only — the dashes and spaces a printed code is\n * grouped with don't count — and \"Verify\" waits for it. Left out, no switch.\n */\n recoveryCode?: boolean | { length?: number };\n /** App content at the top of the credentials step — a notice, a demo button. */\n credentialsContent?: ReactNode;\n /**\n * App content on the second-factor step, under its intro. 0.30.0: pass a function and\n * it gets the challenge token — kastlan kept its own copy of the token for a \"lost\n * your device?\" link, which the step now hands over.\n */\n twoFactorContent?: ReactNode | ((challengeToken: string) => ReactNode);\n /**\n * App content on the set-a-new-password step, under its intro — keksdose's\n * private-mode note (\"Your encrypted data is untouched …\"), which depends on the\n * challenge: pass a function and it gets the challenge's `extra`.\n */\n passwordChangeContent?: ReactNode | ((extra: Readonly<Record<string, unknown>> | undefined) => ReactNode);\n /**\n * The app's own words for a failure, or `undefined` for the kit's: the device being\n * offline, a throttle with its wait (\"try again in an hour\"). Asked first, for every\n * failure; what it returns replaces the message, and the `invalid_credentials` hints\n * still follow. A bare `429` needs none since 0.30.0: the kit says `rateLimited`.\n */\n describeError?: (error: unknown, action: SignInAction) => ReactNode | undefined;\n /** The step changed — retitle the page (`AuthLayout`'s `title`) if the app wants. */\n onStepChange?: (step: SignInStep) => void;\n /** The level of the second and third step's heading. Default `h2`, under the page's\n * `h1`; one level under `AuthLayout`'s `headingAs` where that is embedded lower. */\n headingAs?: \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\";\n labels?: LabelOverride<SignInLabels>;\n}\n\n/* ── Parts ───────────────────────────────────────────────────────────────── */\n\ninterface Failure {\n action: SignInAction;\n code: KitErrorCode | undefined;\n message: ReactNode;\n /** The address the failed credentials carried — the tag hint names its tagged form. */\n email?: string;\n}\n\n/** `{contact}` in the template, replaced by the contact (a `mailto:` link for an\n * address). Split, not interpolated, so the link is an element. A template that lost\n * its placeholder still shows the contact, after it. */\nfunction withContact(template: string, contact: ReactNode): ReactNode[] {\n const node =\n typeof contact === \"string\" && contact.includes(\"@\") ? (\n <TextLink key=\"contact\" href={`mailto:${contact}`} tone=\"secondary\">\n {contact}\n </TextLink>\n ) : (\n <span key=\"contact\">{contact}</span>\n );\n if (!template.includes(\"{contact}\")) return [template, \" \", node];\n return template\n .split(/(\\{contact\\})/)\n .filter((part) => part !== \"\")\n .map((part) => (part === \"{contact}\" ? node : part));\n}\n\nconst HINT_CLASS = \"text-xs text-secondary\";\n\n/** A backup code's letters and digits — kastlan's: 16, printed `ABCD-EFGH-JKLM-NPQR`. */\nconst RECOVERY_CODE_LENGTH = 16;\n\n/** How many letters and digits a typed backup code has: the dashes and spaces it is\n * printed in groups with don't count, so \"abcd-efgh ijkl mnop\" is 16 — the count\n * kastlan's server normalises the code to before it compares. */\nfunction recoveryCharacters(value: string): number {\n return value.replace(/[^\\p{L}\\p{N}]/gu, \"\").length;\n}\nconst INTRO_CLASS = \"text-sm text-secondary\";\n\n/* ── The form ────────────────────────────────────────────────────────────── */\n\n/**\n * Sign-in for every app (docs/auth-harmonization.md §5, §8): email and password, or a\n * passkey; then the second factor when it is on; then a new password when an admin\n * required one. keksdose's `LoginPage` and `ForcedPasswordChangeStep`\n * (features/auth/login-page.tsx, forced-password-change.tsx), as one component.\n *\n * **It never sends a request.** Each step hands its input to a callback and renders the\n * {@link SignInAnswer} it resolves with; a rejection is shown in the step it came from.\n * The challenge token lives in this component's state only, as in keksdose, so a reload\n * costs a fresh sign-in — no route, no storage.\n *\n * **One error for every wrong credential** (§2.10, §5.2): `invalid_credentials` shows one\n * sentence, whatever was wrong, plus two hints UNDER it — \"Account deactivated? Write to\n * …\" (`deactivatedContact`) and \"Signed up with you+kastlan@…? Use that address.\"\n * (`emailTag`). Any other failure says \"Sign-in failed\" and no hint: the hints answer a\n * refused credential, not a network that is down. `describeError` puts the app's words\n * on a failure the kit cannot know (a throttled attempt).\n *\n * **The steps.**\n * - Credentials: the email field is `username webauthn` (passkey autofill, see\n * `passkeyAutofill`), the password a `CurrentPasswordInput` (a password manager fills\n * it and never offers a new one). The submit is not disabled while the fields are\n * empty: a browser that autofills on load withholds the values until the first\n * interaction, and a disabled button would swallow that first click. The fields'\n * own `required` answers an empty submit instead. \"Forgot your password?\" sits under\n * the buttons, where someone whose password just failed is already looking\n * (keksdose #198).\n * - Second factor: {@link OneTimeCodeInput}, focused on arrival. \"Verify\" waits for the\n * whole code. A refused code stays here with its error; `token_invalid` (the\n * challenge expired) goes back to the credentials and says why. Kurvenschmiede has\n * no 2FA yet, and the step is here from the start, so adding it is backend work only\n * (§10.5). With `recoveryCode` (0.30.0) a switch under the field takes a backup code\n * instead, through the same `onCode` with `kind: \"recovery\"`.\n *\n * **A throttled request** (HTTP 429, {@link isRateLimited}) says \"Too many attempts. Try\n * again in 30 s.\" on any step (0.30.0) — the wait from its `Retry-After`, else \"Wait a\n * moment and try again.\" — after `describeError`, whose words still come first, and\n * instead of \"wrong password\" or \"invalid code\", which would send the person to try\n * another one.\n * - New password: new + confirm with the strength meter, the 8-character / 72-byte\n * rules and nothing else (§8). It reads as explained, not blocked (keksdose): the\n * person did nothing wrong.\n *\n * The password is dropped from state once the credentials are answered; \"Back to sign in\"\n * returns to an empty password field.\n */\nexport function SignInForm({\n onSubmit,\n onCode,\n onSetPassword,\n onPasskey,\n passkeyAutofill,\n forgotHref,\n registerHref,\n access,\n defaultEmail = \"\",\n emailTag,\n deactivatedContact,\n codeLength = 6,\n recoveryCode,\n credentialsContent,\n twoFactorContent,\n passwordChangeContent,\n describeError,\n onStepChange,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n className,\n ...rest\n}: SignInFormProps) {\n const labels = useKitLabels(\"signIn\", DEFAULT_SIGN_IN_LABELS, labelsProp);\n // The mail's subject and body are the landing's words (`landing` namespace), so the\n // request reads the same from every page that offers it.\n const landing = useLandingLabels();\n const accountLink =\n access === undefined\n ? registerHref === undefined\n ? null\n : { href: registerHref, label: labels.register }\n : access.kind === \"register\"\n ? { href: access.href, label: labels.register }\n : {\n href: accessAction(access, landing).href,\n label: labels.requestAccess ?? DEFAULT_SIGN_IN_LABELS.requestAccess,\n };\n const [step, setStep] = useState<SignInStep>(\"credentials\");\n const [email, setEmail] = useState(defaultEmail);\n const [password, setPassword] = useState(\"\");\n const [code, setCode] = useState(\"\");\n // 0.30.0: the second factor as a backup code instead of the authenticator's digits.\n const [useRecovery, setUseRecovery] = useState(false);\n const [recovery, setRecovery] = useState(\"\");\n const [newPassword, setNewPassword] = useState(\"\");\n const [confirm, setConfirm] = useState(\"\");\n const [challenge, setChallenge] = useState<{ token: string; extra?: Readonly<Record<string, unknown>> } | null>(\n null,\n );\n // What is running: one call at a time. \"done\" after `signed-in` — the app has the\n // session and is leaving; the buttons stay busy so nothing is sent twice meanwhile.\n const [busy, setBusy] = useState<SignInAction | \"done\" | null>(null);\n const [failure, setFailure] = useState<Failure | null>(null);\n const headingId = useId();\n\n const emailRef = useRef<HTMLInputElement>(null);\n const passwordRef = useRef<HTMLInputElement>(null);\n const codeRef = useRef<HTMLInputElement>(null);\n const recoveryRef = useRef<HTMLInputElement>(null);\n const newPasswordRef = useRef<HTMLInputElement>(null);\n\n // A late answer after unmount (the app navigated away) must not set state.\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n const goTo = (next: SignInStep) => {\n setStep(next);\n onStepChange?.(next);\n };\n\n const present = (answer: SignInAnswer) => {\n setFailure(null);\n if (answer.kind === \"signed-in\") {\n setBusy(\"done\");\n return;\n }\n setBusy(null);\n // The password has done its work; it does not stay in state for the next steps.\n setPassword(\"\");\n setCode(\"\");\n setRecovery(\"\");\n setUseRecovery(false);\n setNewPassword(\"\");\n setConfirm(\"\");\n if (answer.kind === \"2fa\") {\n setChallenge({ token: answer.challengeToken });\n goTo(\"2fa\");\n } else if (answer.kind === \"password-change\") {\n setChallenge({ token: answer.challengeToken, extra: answer.extra });\n goTo(\"password-change\");\n }\n };\n\n const messageFor = (\n error: unknown,\n action: SignInAction,\n code: KitErrorCode | undefined,\n recoveryUsed: boolean,\n ): ReactNode => {\n const own = describeError?.(error, action);\n if (hasContent(own)) return own;\n // After the app's words, before the kit's generic ones (0.30.0): a throttle is\n // neither a wrong password nor a wrong code, and saying so sends the person to try\n // another one — which the limiter counts too.\n if (isRateLimited(error)) return (labels.rateLimited ?? DEFAULT_SIGN_IN_LABELS.rateLimited)(retryAfterSeconds(error));\n if (action === \"password\") return code === \"invalid_credentials\" ? labels.invalidCredentials : labels.failed;\n if (action === \"passkey\") return labels.passkeyFailed;\n if (action === \"code\") {\n if (code === \"token_invalid\") return labels.expired;\n return recoveryUsed\n ? (labels.recoveryCodeInvalid ?? DEFAULT_SIGN_IN_LABELS.recoveryCodeInvalid)\n : labels.codeInvalid;\n }\n return code === \"token_invalid\" ? labels.expired : labels.setPasswordFailed;\n };\n\n const fail = (error: unknown, action: SignInAction, sentEmail?: string, recoveryUsed = false) => {\n setBusy(null);\n // Our own abort (a passkey ceremony cancelled by the unmount) is no failure.\n if ((error as { name?: unknown } | null)?.name === \"AbortError\") return;\n const code = authErrorCode(error);\n const message = messageFor(error, action, code, recoveryUsed);\n // The challenge expired between the steps: only a fresh sign-in helps.\n if ((action === \"code\" || action === \"set-password\") && code === \"token_invalid\") {\n setChallenge(null);\n setCode(\"\");\n setRecovery(\"\");\n setUseRecovery(false);\n setNewPassword(\"\");\n setConfirm(\"\");\n goTo(\"credentials\");\n }\n setFailure({ action, code, message, email: sentEmail });\n };\n\n const attempt = (\n action: SignInAction,\n call: () => Promise<SignInAnswer>,\n sentEmail?: string,\n recoveryUsed = false,\n ) => {\n if (busy) return;\n setBusy(action);\n setFailure(null);\n let result: Promise<SignInAnswer>;\n try {\n result = Promise.resolve(call());\n } catch (error) {\n result = Promise.reject(error);\n }\n result.then(\n (answer) => {\n if (mounted.current) present(answer);\n },\n (error: unknown) => {\n if (mounted.current) fail(error, action, sentEmail, recoveryUsed);\n },\n );\n };\n\n // Passkey autofill: armed once, on mount; the signal aborts on unmount. Its answer\n // arrives whenever the person picks a passkey from the email field's dropdown — and\n // is dropped if they have meanwhile started a sign-in of their own.\n const armAutofill = useEffectEvent((signal: AbortSignal) => passkeyAutofill?.(signal));\n const autofillAnswered = useEffectEvent((answer: SignInAnswer) => {\n if (busy === null) present(answer);\n });\n useEffect(() => {\n const controller = new AbortController();\n let pending: Promise<SignInAnswer | null | undefined>;\n try {\n pending = Promise.resolve(armAutofill(controller.signal));\n } catch (error) {\n pending = Promise.reject(error);\n }\n pending.then(\n (answer) => {\n if (answer && !controller.signal.aborted && mounted.current) autofillAnswered(answer);\n },\n () => {\n /* silent — unsupported, declined or aborted; the button stays the way in */\n },\n );\n return () => controller.abort();\n }, []);\n\n // Focus follows the step — not on mount (where it would pop a phone's keyboard over\n // the page), only once the person moved to another step.\n const shownStep = useRef(step);\n useEffect(() => {\n if (shownStep.current === step) return;\n shownStep.current = step;\n if (step === \"2fa\") codeRef.current?.focus();\n else if (step === \"password-change\") newPasswordRef.current?.focus();\n else if (emailRef.current?.value) passwordRef.current?.focus();\n else emailRef.current?.focus();\n }, [step]);\n // The backup-code switch swaps one field for the other under the focus that pressed\n // it; the field that came in is where the person is going. Only after a switch on\n // this step — arriving on it is the effect above.\n const shownRecovery = useRef(useRecovery);\n useEffect(() => {\n if (shownRecovery.current === useRecovery) return;\n shownRecovery.current = useRecovery;\n if (step !== \"2fa\") return;\n (useRecovery ? recoveryRef : codeRef).current?.focus();\n }, [useRecovery, step]);\n\n const backToCredentials = () => {\n if (busy) return;\n setChallenge(null);\n setFailure(null);\n setCode(\"\");\n setRecovery(\"\");\n setUseRecovery(false);\n setNewPassword(\"\");\n setConfirm(\"\");\n goTo(\"credentials\");\n };\n\n const working = busy !== null;\n const rootClass = cn(\"space-y-3\", className);\n\n /* ── Step 2: the second factor ── */\n if (step === \"2fa\" && challenge) {\n const recoveryOn = recoveryCode !== undefined && recoveryCode !== false;\n const recoveryLength = (typeof recoveryCode === \"object\" ? recoveryCode.length : undefined) ?? RECOVERY_CODE_LENGTH;\n const asRecovery = recoveryOn && useRecovery;\n const recoveryReady = recoveryCharacters(recovery) === recoveryLength;\n const ready = asRecovery ? recoveryReady : code.length === codeLength;\n const submitCode = (e: FormEvent) => {\n e.preventDefault();\n if (!ready) return;\n if (asRecovery) {\n attempt(\n \"code\",\n () => onCode({ challengeToken: challenge.token, code: recovery.trim(), kind: \"recovery\" }),\n undefined,\n true,\n );\n return;\n }\n attempt(\"code\", () => onCode({ challengeToken: challenge.token, code }));\n };\n const switchCode = () => {\n if (working) return;\n setFailure(null);\n setCode(\"\");\n setRecovery(\"\");\n setUseRecovery((on) => !on);\n };\n const codeError = failure?.action === \"code\" ? failure.message : undefined;\n const content =\n typeof twoFactorContent === \"function\" ? twoFactorContent(challenge.token) : twoFactorContent;\n return (\n <div {...rest} data-step=\"2fa\" className={rootClass}>\n <form className=\"space-y-3\" aria-labelledby={headingId} onSubmit={submitCode}>\n <Heading id={headingId} className=\"text-base font-semibold text-primary\">\n {labels.twoFactorTitle}\n </Heading>\n <p className={INTRO_CLASS}>\n {asRecovery ? (labels.recoveryIntro ?? DEFAULT_SIGN_IN_LABELS.recoveryIntro) : labels.twoFactorIntro}\n </p>\n {content}\n {asRecovery ? (\n // A plain text field, not the digits-only one: a backup code is letters and\n // digits in printed groups. Nothing a phone keyboard would \"correct\", and no\n // autofill — the browser's suggestion for an unknown field is the username.\n <Input\n ref={recoveryRef}\n label={labels.recoveryCode ?? DEFAULT_SIGN_IN_LABELS.recoveryCode}\n name=\"recovery-code\"\n value={recovery}\n onChange={(e) => {\n setRecovery(e.target.value);\n if (failure) setFailure(null);\n }}\n autoComplete=\"off\"\n autoCapitalize=\"characters\"\n autoCorrect=\"off\"\n spellCheck={false}\n inputClassName=\"font-mono\"\n hint={(labels.recoveryCodeHint ?? DEFAULT_SIGN_IN_LABELS.recoveryCodeHint)(recoveryLength)}\n readOnly={working}\n error={codeError}\n required\n />\n ) : (\n <OneTimeCodeInput\n ref={codeRef}\n label={labels.code}\n name=\"code\"\n value={code}\n length={codeLength}\n onChange={(next) => {\n setCode(next);\n if (failure) setFailure(null);\n }}\n readOnly={working}\n error={codeError}\n required\n />\n )}\n {recoveryOn && (\n <p className=\"text-sm\">\n <Button type=\"button\" variant=\"link\" size=\"sm\" disabled={working} onClick={switchCode}>\n {asRecovery\n ? (labels.useAuthenticatorCode ?? DEFAULT_SIGN_IN_LABELS.useAuthenticatorCode)\n : (labels.useRecoveryCode ?? DEFAULT_SIGN_IN_LABELS.useRecoveryCode)}\n </Button>\n </p>\n )}\n <Button type=\"submit\" className=\"w-full\" disabled={!working && !ready} pending={working}>\n {labels.verify}\n </Button>\n </form>\n <BackLink label={labels.backToSignIn} onClick={backToCredentials} disabled={working} />\n </div>\n );\n }\n\n /* ── Step 3: a new password ── */\n if (step === \"password-change\" && challenge) {\n const problem = newPasswordProblem(newPassword, confirm);\n const submitPassword = (e: FormEvent) => {\n e.preventDefault();\n if (problem) return;\n attempt(\"set-password\", () => onSetPassword({ challengeToken: challenge.token, password: newPassword }));\n };\n const content =\n typeof passwordChangeContent === \"function\" ? passwordChangeContent(challenge.extra) : passwordChangeContent;\n return (\n <div {...rest} data-step=\"password-change\" className={rootClass}>\n <form className=\"space-y-3\" aria-labelledby={headingId} onSubmit={submitPassword}>\n <Heading id={headingId} className=\"text-base font-semibold text-primary\">\n {labels.setPasswordTitle}\n </Heading>\n <p className={INTRO_CLASS}>{labels.setPasswordIntro}</p>\n {content}\n {/* Which account the new password is for, for the password manager: it files\n the new password under this username instead of guessing. */}\n {email !== \"\" && (\n <input type=\"email\" name=\"username\" autoComplete=\"username\" value={email} readOnly hidden />\n )}\n <NewPasswordFields\n password={newPassword}\n confirm={confirm}\n onPasswordChange={setNewPassword}\n onConfirmChange={setConfirm}\n busy={working}\n passwordRef={newPasswordRef}\n text={{ password: labels.newPassword, confirm: labels.confirmPassword, mismatch: labels.passwordMismatch }}\n />\n {failure?.action === \"set-password\" && (\n <AlertBanner tone=\"danger\" size=\"sm\" role=\"alert\">\n {failure.message}\n </AlertBanner>\n )}\n <Button type=\"submit\" className=\"w-full\" disabled={!working && problem !== null} pending={working}>\n {labels.setPasswordSubmit}\n </Button>\n </form>\n <BackLink label={labels.backToSignIn} onClick={backToCredentials} disabled={working} />\n </div>\n );\n }\n\n /* ── Step 1: the credentials ── */\n const submitCredentials = (e: FormEvent) => {\n e.preventDefault();\n const sent = email.trim();\n if (sent === \"\" || password === \"\") return;\n attempt(\"password\", () => onSubmit({ email: sent, password }), sent);\n };\n\n const refused = failure?.code === \"invalid_credentials\";\n const tagged =\n refused && failure?.action === \"password\" && emailTag && failure.email ? taggedEmail(failure.email, emailTag) : null;\n const showDeactivated = refused && hasContent(deactivatedContact);\n\n return (\n <div {...rest} data-step=\"credentials\" className={rootClass}>\n <form className=\"space-y-3\" onSubmit={submitCredentials}>\n {credentialsContent}\n <Input\n ref={emailRef}\n type=\"email\"\n name=\"email\"\n // \"webauthn\" lets the browser list passkeys in this field's dropdown, paired\n // with the conditional request `passkeyAutofill` arms.\n autoComplete=\"username webauthn\"\n label={labels.email}\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n readOnly={working}\n required\n />\n <CurrentPasswordInput\n ref={passwordRef}\n name=\"password\"\n label={labels.password}\n value={password}\n onValueChange={setPassword}\n busy={working}\n required\n />\n {failure && (\n // One live region for the sentence and its hints: they are one answer.\n <div role=\"alert\" className=\"space-y-1.5\">\n <AlertBanner tone=\"danger\" size=\"sm\">\n {failure.message}\n </AlertBanner>\n {showDeactivated && (\n <p className={HINT_CLASS}>{withContact(labels.deactivatedHint, deactivatedContact)}</p>\n )}\n {tagged && <p className={HINT_CLASS}>{labels.tagHint(tagged)}</p>}\n </div>\n )}\n <Button type=\"submit\" className=\"w-full\" pending={busy === \"password\" || busy === \"done\"} disabled={busy === \"passkey\"}>\n {labels.submit}\n </Button>\n {onPasskey && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n className=\"w-full\"\n pending={busy === \"passkey\"}\n disabled={busy === \"password\" || busy === \"done\"}\n onClick={() => attempt(\"passkey\", onPasskey)}\n >\n <KeyRound aria-hidden className=\"size-4\" />\n {labels.passkey}\n </Button>\n )}\n </form>\n {forgotHref !== undefined && (\n <p className=\"text-center text-sm\">\n <TextLink href={forgotHref} tone=\"secondary\">\n {labels.forgotPassword}\n </TextLink>\n </p>\n )}\n {accountLink !== null && (\n <p className=\"text-center text-sm text-secondary\">\n {labels.noAccount}{\" \"}\n <TextLink href={accountLink.href} tone=\"primary\">\n {accountLink.label}\n </TextLink>\n </p>\n )}\n </div>\n );\n}\n\n/** \"Back to sign in\" under the second and third step: a button, since it changes the\n * step in place rather than going anywhere. */\nfunction BackLink({ label, onClick, disabled }: { label: string; onClick: () => void; disabled: boolean }) {\n return (\n <p className=\"text-center text-sm\">\n <Button type=\"button\" variant=\"link\" tone=\"muted\" size=\"sm\" disabled={disabled} onClick={onClick}>\n {label}\n </Button>\n </p>\n );\n}\n"],"mappings":"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","names":["code"]}
|
|
1
|
+
{"version":3,"sources":["../../src/auth/sign-in-form.tsx"],"sourcesContent":["import { useEffect, useEffectEvent, useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, FormEvent, ReactNode } from \"react\";\nimport { KeyRound } from \"lucide-react\";\n\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { hasContent } from \"../lib/presence\";\nimport { CurrentPasswordInput } from \"../components/danger-confirm\";\nimport { OneTimeCodeInput } from \"../components/one-time-code-input\";\nimport { TextLink } from \"../components/text-link\";\nimport { Button } from \"../components/button\";\nimport { Input } from \"../components/input\";\nimport { accessAction } from \"../landing/access\";\nimport type { AccessChoice } from \"../landing/access\";\nimport { useLandingLabels } from \"../landing/landing-labels\";\nimport { authErrorCode, isRateLimited, retryAfterSeconds } from \"./auth-errors\";\nimport type { KitErrorCode } from \"./auth-errors\";\nimport { taggedEmail } from \"./email-tag\";\nimport { newPasswordProblem } from \"./form-rules\";\nimport { NewPasswordFields } from \"./new-password-fields\";\nimport { DEFAULT_SIGN_IN_LABELS } from \"./sign-in-form-labels\";\n\n/* ── The answers (§5.1) ──────────────────────────────────────────────────── */\n\n/**\n * What a sign-in call answers, in the kit's words — docs/auth-harmonization.md §5.1,\n * keksdose's three shapes. The app maps its own HTTP answer to one of these and resolves\n * with it; a refusal REJECTS (with whatever its client throws — {@link authErrorCode}\n * reads the code out of it).\n *\n * | the server answers (keksdose) | resolve with |\n * |---|---|\n * | `TokenResponse` `{access_token, refresh_token, token_type, user}` | `{ kind: \"signed-in\" }`, after the app has stored the session |\n * | `{requires_2fa: true, challenge_token}` | `{ kind: \"2fa\", challengeToken: challenge_token }` |\n * | `{requires_password_change: true, challenge_token, encrypted}` | `{ kind: \"password-change\", challengeToken: challenge_token, extra: { encrypted } }` |\n * | `401 {code: \"invalid_credentials\"}` | reject — let the client's error through |\n *\n * ```ts\n * async function toAnswer(res: TokenResponse | TwoFactorChallenge | PasswordChangeChallenge): Promise<SignInAnswer> {\n * if (\"requires_2fa\" in res) return { kind: \"2fa\", challengeToken: res.challenge_token };\n * if (\"requires_password_change\" in res)\n * return { kind: \"password-change\", challengeToken: res.challenge_token, extra: { encrypted: res.encrypted } };\n * await onLoginSuccess(res); // store tokens, load /auth/me, switch language\n * return { kind: \"signed-in\" };\n * }\n * <SignInForm onSubmit={async (v) => toAnswer(await authApi.login(v))} … />\n * ```\n *\n * The same mapping serves `/auth/login/2fa`, `/auth/login/set-password` and the passkey\n * finish, which answer the same union.\n */\nexport type SignInAnswer =\n | { kind: \"signed-in\" }\n | { kind: \"2fa\"; challengeToken: string }\n | {\n kind: \"password-change\";\n challengeToken: string;\n /** The challenge's app fields, passed through untouched — keksdose's `encrypted`,\n * which its note on this step reads (`passwordChangeContent`). */\n extra?: Readonly<Record<string, unknown>>;\n };\n\n/** The step on screen: the credentials, the second factor, or a new password. */\nexport type SignInStep = \"credentials\" | \"2fa\" | \"password-change\";\n\n/** What the person did when a call failed — {@link SignInFormProps.describeError}'s\n * second argument. */\nexport type SignInAction = \"password\" | \"passkey\" | \"code\" | \"set-password\";\n\nexport interface SignInCredentials {\n /** Trimmed. Lower-casing is the server's (§3.1). */\n email: string;\n /** As typed, never trimmed. */\n password: string;\n}\n\nexport interface SignInCodeValues {\n challengeToken: string;\n /** Digits only — {@link OneTimeCodeInput} drops the spaces of \"123 456\". A backup code\n * (`kind: \"recovery\"`) is trimmed and otherwise as typed: its dashes and case are the\n * server's to normalise, as kastlan's `_normalise_backup_code` does. */\n code: string;\n /**\n * 0.30.0: `\"recovery\"` when the person typed one of their backup codes instead\n * (`recoveryCode`); absent for the authenticator's code, as before. kastlan's\n * `/auth/login/2fa` takes either in the same `code` and tells them apart itself; an\n * app whose server takes a backup code elsewhere routes on this.\n */\n kind?: \"recovery\";\n}\n\nexport interface SignInNewPasswordValues {\n challengeToken: string;\n password: string;\n}\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/**\n * Every string `SignInForm` renders — the `signIn` namespace of `<UiKitProvider\n * labels>`, overridable per instance through `labels`. keksdose's words where it has\n * them (`auth.*` in its catalogue).\n */\nexport interface SignInLabels {\n email: string;\n password: string;\n /** The submit button. */\n submit: string;\n /** The passkey button (`onPasskey`). */\n passkey: string;\n /** The link to `forgotHref`. */\n forgotPassword: string;\n /** Before the link to `registerHref`. */\n noAccount: string;\n /** The link to `registerHref`. */\n register: string;\n /** The ONE answer to an unknown address, a wrong password and a deactivated account\n * (`invalid_credentials`, §5.2). */\n invalidCredentials: string;\n /** Any other failed sign-in (the network, a 500). */\n failed: string;\n /** A failed or refused passkey sign-in. */\n passkeyFailed: string;\n /**\n * The hint under `invalidCredentials` (§2.10), shown when `deactivatedContact` is\n * given. A TEMPLATE: `{contact}` is replaced by the contact — a `mailto:` link for an\n * address — so a language puts it where its grammar needs it.\n */\n deactivatedHint: string;\n /** The hint under `invalidCredentials` when `emailTag` is given (§4.5), given the\n * typed address with the app's tag: \"Signed up with ada+kastlan@example.com?\". */\n tagHint: (taggedAddress: string) => string;\n /** The second-factor step's heading. */\n twoFactorTitle: string;\n twoFactorIntro: string;\n /** The code field's label. */\n code: string;\n /** The code step's submit button. */\n verify: string;\n /** The code was refused. */\n codeInvalid: string;\n /** The set-a-new-password step's heading. */\n setPasswordTitle: string;\n setPasswordIntro: string;\n newPassword: string;\n confirmPassword: string;\n passwordMismatch: string;\n /** The set-a-new-password step's submit button. */\n setPasswordSubmit: string;\n setPasswordFailed: string;\n /** A challenge was refused as `token_invalid` — it expired between the steps; the\n * form is back on the credentials. */\n expired: string;\n /** From the second and third step back to the credentials. */\n backToSignIn: string;\n /*\n * 0.30.0 — OPTIONAL, like `TwoFactorSettingLabels.qrAlt` and for its reason: a new\n * required key is a compile error at every call site that annotates this interface\n * (CONTRIBUTING's additive-API rule). Each falls back to the provider's `signIn`, then\n * English.\n */\n /** The second-factor step's switch to a backup code (`recoveryCode`). */\n useRecoveryCode?: string;\n /** The switch back, from the backup-code field to the authenticator's code. */\n useAuthenticatorCode?: string;\n /** The backup-code field's label. */\n recoveryCode?: string;\n /** The step's intro while a backup code is asked for, in place of `twoFactorIntro`. */\n recoveryIntro?: string;\n /** Under the backup-code field, given how many letters and digits a code has. */\n recoveryCodeHint?: (length: number) => string;\n /** A refused backup code — wrong, or spent already (each one works once). */\n recoveryCodeInvalid?: string;\n /** Any step's request was throttled — HTTP 429 ({@link isRateLimited}) — given the\n * wait its `Retry-After` asks for, in seconds ({@link retryAfterSeconds}), or\n * `undefined` (or `0`) without one. */\n rateLimited?: (seconds?: number) => string;\n /** 0.31.0: the link after `noAccount` for an invitation-only app (`access` of kind\n * `request`) — a mail to support, in place of `register`. */\n requestAccess?: string;\n}\n\n// Defined in ./sign-in-form-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_SIGN_IN_LABELS };\n\n/* ── Props ───────────────────────────────────────────────────────────────── */\n\nexport interface SignInFormProps extends Omit<ComponentPropsWithRef<\"div\">, \"onSubmit\" | \"children\"> {\n /**\n * The credentials step was submitted. Send them, and resolve with the answer\n * ({@link SignInAnswer}) or reject with the client's error. The form never sends a\n * request itself (§8): keksdose must see the password on its way, to refuse the same\n * string as an encryption passphrase later.\n *\n * On `signed-in` the form's job is over — it stays busy while the app navigates.\n */\n onSubmit: (credentials: SignInCredentials) => Promise<SignInAnswer>;\n /** The second factor was submitted (`/auth/login/2fa`). Resolves with the next answer\n * — keksdose may still require a new password after the code. */\n onCode: (values: SignInCodeValues) => Promise<SignInAnswer>;\n /** The new password was submitted (`/auth/login/set-password`). Resolves with the\n * answer, `signed-in` in practice; keksdose's `key_wrap_unchanged` note is the app's\n * to show from here. */\n onSetPassword: (values: SignInNewPasswordValues) => Promise<SignInAnswer>;\n /**\n * The passkey button was pressed: run the ceremony (`/auth/passkey/login/begin|finish`)\n * and resolve with its answer. It asks for NO email — a passkey is bound to the site,\n * and the browser offers every one it holds (keksdose #126). Left out, no button: pass\n * it only where `PublicKeyCredential` exists.\n */\n onPasskey?: () => Promise<SignInAnswer>;\n /**\n * Passkey autofill (conditional mediation). The email field is\n * `autoComplete=\"username webauthn\"`, so a pending `navigator.credentials.get({\n * mediation: \"conditional\" })` lists the site's passkeys in its dropdown. The form\n * calls this ONCE on mount; arm the request there and resolve with its answer when the\n * person picks a passkey, or with `null` when nothing happens. The signal aborts on\n * unmount — cancel the pending request then (keksdose: `armPasskeyAutofill(onDone,\n * signal)` + `cancelPasskeyAutofill()`). A rejection is silent: the button stays the\n * reliable path.\n */\n passkeyAutofill?: (signal: AbortSignal) => Promise<SignInAnswer | null | undefined>;\n /** \"Forgot your password?\" goes here — the app's `/forgot-password`. Left out, no\n * link. */\n forgotHref?: string;\n /** \"No account yet? Create account\" goes here — `/register`. Left out, no line. */\n registerHref?: string;\n /**\n * 0.31.0 (docs/landing-demo-harmonization.md §4.2): how a visitor gets an account —\n * the same value the landing's `PublicHeader` and `Hero` take. `{kind: \"request\",\n * email}` turns the line into \"No account yet? Request access\", a mail to support with\n * the subject and body of `accessAction` (every app is invitation-only); `{kind:\n * \"register\", href}` keeps \"Create account\", to `href`. Wins over `registerHref`; left\n * out, `registerHref` works as before.\n */\n access?: AccessChoice;\n /** The email field's start value — the reset page ends on \"Sign in\" with the address\n * filled in (§6.3). */\n defaultEmail?: string;\n /**\n * The app's address tag (`\"kastlan\"`, §4.5). After `invalid_credentials` the form also\n * says \"Signed up with you+kastlan@example.com? Use that address.\" — sign-in stays\n * exact, and the server looks nothing up.\n */\n emailTag?: string;\n /**\n * Who a deactivated user writes to (§2.10): an address (drawn as a `mailto:` link) or\n * the app's own node. Shown after `invalid_credentials` as \"Account deactivated? Write\n * to …\". Left out, no hint.\n */\n deactivatedContact?: ReactNode;\n /** Digits of the second factor. Default 6 (TOTP). */\n codeLength?: number;\n /**\n * 0.30.0: offer a backup code on the second-factor step (kastlan's 0.29 adoption: its\n * `/auth/login/2fa` takes one of the eight codes minted at 2FA setup, and the kit's\n * step had no way to type one). A \"Use a backup code\" switch under the code field\n * swaps the digits-only {@link OneTimeCodeInput} for a plain text field; the code goes\n * to `onCode` with the same challenge and `kind: \"recovery\"`.\n *\n * `true` for a 16-character code (kastlan's `ABCD-EFGH-JKLM-NPQR`), or `{ length }`.\n * The length counts letters and digits only — the dashes and spaces a printed code is\n * grouped with don't count — and \"Verify\" waits for it. Left out, no switch.\n */\n recoveryCode?: boolean | { length?: number };\n /** App content at the top of the credentials step — a notice, a demo button. */\n credentialsContent?: ReactNode;\n /**\n * App content on the second-factor step, under its intro. 0.30.0: pass a function and\n * it gets the challenge token — kastlan kept its own copy of the token for a \"lost\n * your device?\" link, which the step now hands over.\n */\n twoFactorContent?: ReactNode | ((challengeToken: string) => ReactNode);\n /**\n * App content on the set-a-new-password step, under its intro — keksdose's\n * private-mode note (\"Your encrypted data is untouched …\"), which depends on the\n * challenge: pass a function and it gets the challenge's `extra`.\n */\n passwordChangeContent?: ReactNode | ((extra: Readonly<Record<string, unknown>> | undefined) => ReactNode);\n /**\n * The app's own words for a failure, or `undefined` for the kit's: the device being\n * offline, a throttle with its wait (\"try again in an hour\"). Asked first, for every\n * failure; what it returns replaces the message, and the `invalid_credentials` hints\n * still follow. A bare `429` needs none since 0.30.0: the kit says `rateLimited`.\n */\n describeError?: (error: unknown, action: SignInAction) => ReactNode | undefined;\n /** The step changed — retitle the page (`AuthLayout`'s `title`) if the app wants. */\n onStepChange?: (step: SignInStep) => void;\n /** The level of the second and third step's heading. Default `h2`, under the page's\n * `h1`; one level under `AuthLayout`'s `headingAs` where that is embedded lower. */\n headingAs?: \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\";\n labels?: LabelOverride<SignInLabels>;\n}\n\n/* ── Parts ───────────────────────────────────────────────────────────────── */\n\ninterface Failure {\n action: SignInAction;\n code: KitErrorCode | undefined;\n message: ReactNode;\n /** The address the failed credentials carried — the tag hint names its tagged form. */\n email?: string;\n}\n\n/** `{contact}` in the template, replaced by the contact (a `mailto:` link for an\n * address). Split, not interpolated, so the link is an element. A template that lost\n * its placeholder still shows the contact, after it. */\nfunction withContact(template: string, contact: ReactNode): ReactNode[] {\n const node =\n typeof contact === \"string\" && contact.includes(\"@\") ? (\n <TextLink key=\"contact\" href={`mailto:${contact}`} tone=\"secondary\">\n {contact}\n </TextLink>\n ) : (\n <span key=\"contact\">{contact}</span>\n );\n if (!template.includes(\"{contact}\")) return [template, \" \", node];\n return template\n .split(/(\\{contact\\})/)\n .filter((part) => part !== \"\")\n .map((part) => (part === \"{contact}\" ? node : part));\n}\n\nconst HINT_CLASS = \"text-xs text-secondary\";\n\n/** A backup code's letters and digits — kastlan's: 16, printed `ABCD-EFGH-JKLM-NPQR`. */\nconst RECOVERY_CODE_LENGTH = 16;\n\n/** How many letters and digits a typed backup code has: the dashes and spaces it is\n * printed in groups with don't count, so \"abcd-efgh ijkl mnop\" is 16 — the count\n * kastlan's server normalises the code to before it compares. */\nfunction recoveryCharacters(value: string): number {\n return value.replace(/[^\\p{L}\\p{N}]/gu, \"\").length;\n}\nconst INTRO_CLASS = \"text-sm text-secondary\";\n\n/* ── The form ────────────────────────────────────────────────────────────── */\n\n/**\n * Sign-in for every app (docs/auth-harmonization.md §5, §8): email and password, or a\n * passkey; then the second factor when it is on; then a new password when an admin\n * required one. keksdose's `LoginPage` and `ForcedPasswordChangeStep`\n * (features/auth/login-page.tsx, forced-password-change.tsx), as one component.\n *\n * **It never sends a request.** Each step hands its input to a callback and renders the\n * {@link SignInAnswer} it resolves with; a rejection is shown in the step it came from.\n * The challenge token lives in this component's state only, as in keksdose, so a reload\n * costs a fresh sign-in — no route, no storage.\n *\n * **One error for every wrong credential** (§2.10, §5.2): `invalid_credentials` shows one\n * sentence, whatever was wrong, plus two hints UNDER it — \"Account deactivated? Write to\n * …\" (`deactivatedContact`) and \"Signed up with you+kastlan@…? Use that address.\"\n * (`emailTag`). Any other failure says \"Sign-in failed\" and no hint: the hints answer a\n * refused credential, not a network that is down. `describeError` puts the app's words\n * on a failure the kit cannot know (a throttled attempt).\n *\n * **The steps.**\n * - Credentials: the email field is `username webauthn` (passkey autofill, see\n * `passkeyAutofill`), the password a `CurrentPasswordInput` (a password manager fills\n * it and never offers a new one). The submit is not disabled while the fields are\n * empty: a browser that autofills on load withholds the values until the first\n * interaction, and a disabled button would swallow that first click. The fields'\n * own `required` answers an empty submit instead. \"Forgot your password?\" sits under\n * the buttons, where someone whose password just failed is already looking\n * (keksdose #198).\n * - Second factor: {@link OneTimeCodeInput}, focused on arrival. \"Verify\" waits for the\n * whole code. A refused code stays here with its error; `token_invalid` (the\n * challenge expired) goes back to the credentials and says why. Kurvenschmiede has\n * no 2FA yet, and the step is here from the start, so adding it is backend work only\n * (§10.5). With `recoveryCode` (0.30.0) a switch under the field takes a backup code\n * instead, through the same `onCode` with `kind: \"recovery\"`.\n *\n * **A throttled request** (HTTP 429, {@link isRateLimited}) says \"Too many attempts. Try\n * again in 30 s.\" on any step (0.30.0) — the wait from its `Retry-After`, else \"Wait a\n * moment and try again.\" — after `describeError`, whose words still come first, and\n * instead of \"wrong password\" or \"invalid code\", which would send the person to try\n * another one.\n * - New password: new + confirm with the strength meter, the 8-character / 72-byte\n * rules and nothing else (§8). It reads as explained, not blocked (keksdose): the\n * person did nothing wrong.\n *\n * The password is dropped from state once the credentials are answered; \"Back to sign in\"\n * returns to an empty password field.\n */\nexport function SignInForm({\n onSubmit,\n onCode,\n onSetPassword,\n onPasskey,\n passkeyAutofill,\n forgotHref,\n registerHref,\n access,\n defaultEmail = \"\",\n emailTag,\n deactivatedContact,\n codeLength = 6,\n recoveryCode,\n credentialsContent,\n twoFactorContent,\n passwordChangeContent,\n describeError,\n onStepChange,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n className,\n ...rest\n}: SignInFormProps) {\n const labels = useKitLabels(\"signIn\", DEFAULT_SIGN_IN_LABELS, labelsProp);\n // The mail's subject and body are the landing's words (`landing` namespace), so the\n // request reads the same from every page that offers it.\n const landing = useLandingLabels();\n const accountLink =\n access === undefined\n ? registerHref === undefined\n ? null\n : { href: registerHref, label: labels.register }\n : access.kind === \"register\"\n ? { href: access.href, label: labels.register }\n : {\n href: accessAction(access, landing).href,\n label: labels.requestAccess ?? DEFAULT_SIGN_IN_LABELS.requestAccess,\n };\n const [step, setStep] = useState<SignInStep>(\"credentials\");\n const [email, setEmail] = useState(defaultEmail);\n const [password, setPassword] = useState(\"\");\n const [code, setCode] = useState(\"\");\n // 0.30.0: the second factor as a backup code instead of the authenticator's digits.\n const [useRecovery, setUseRecovery] = useState(false);\n const [recovery, setRecovery] = useState(\"\");\n const [newPassword, setNewPassword] = useState(\"\");\n const [confirm, setConfirm] = useState(\"\");\n const [challenge, setChallenge] = useState<{ token: string; extra?: Readonly<Record<string, unknown>> } | null>(null);\n // What is running: one call at a time. \"done\" after `signed-in` — the app has the\n // session and is leaving; the buttons stay busy so nothing is sent twice meanwhile.\n const [busy, setBusy] = useState<SignInAction | \"done\" | null>(null);\n const [failure, setFailure] = useState<Failure | null>(null);\n const headingId = useId();\n\n const emailRef = useRef<HTMLInputElement>(null);\n const passwordRef = useRef<HTMLInputElement>(null);\n const codeRef = useRef<HTMLInputElement>(null);\n const recoveryRef = useRef<HTMLInputElement>(null);\n const newPasswordRef = useRef<HTMLInputElement>(null);\n\n // A late answer after unmount (the app navigated away) must not set state.\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n const goTo = (next: SignInStep) => {\n setStep(next);\n onStepChange?.(next);\n };\n\n const present = (answer: SignInAnswer) => {\n setFailure(null);\n if (answer.kind === \"signed-in\") {\n setBusy(\"done\");\n return;\n }\n setBusy(null);\n // The password has done its work; it does not stay in state for the next steps.\n setPassword(\"\");\n setCode(\"\");\n setRecovery(\"\");\n setUseRecovery(false);\n setNewPassword(\"\");\n setConfirm(\"\");\n if (answer.kind === \"2fa\") {\n setChallenge({ token: answer.challengeToken });\n goTo(\"2fa\");\n } else if (answer.kind === \"password-change\") {\n setChallenge({ token: answer.challengeToken, extra: answer.extra });\n goTo(\"password-change\");\n }\n };\n\n const messageFor = (\n error: unknown,\n action: SignInAction,\n code: KitErrorCode | undefined,\n recoveryUsed: boolean,\n ): ReactNode => {\n const own = describeError?.(error, action);\n if (hasContent(own)) return own;\n // After the app's words, before the kit's generic ones (0.30.0): a throttle is\n // neither a wrong password nor a wrong code, and saying so sends the person to try\n // another one — which the limiter counts too.\n if (isRateLimited(error))\n return (labels.rateLimited ?? DEFAULT_SIGN_IN_LABELS.rateLimited)(retryAfterSeconds(error));\n if (action === \"password\") return code === \"invalid_credentials\" ? labels.invalidCredentials : labels.failed;\n if (action === \"passkey\") return labels.passkeyFailed;\n if (action === \"code\") {\n if (code === \"token_invalid\") return labels.expired;\n return recoveryUsed\n ? (labels.recoveryCodeInvalid ?? DEFAULT_SIGN_IN_LABELS.recoveryCodeInvalid)\n : labels.codeInvalid;\n }\n return code === \"token_invalid\" ? labels.expired : labels.setPasswordFailed;\n };\n\n const fail = (error: unknown, action: SignInAction, sentEmail?: string, recoveryUsed = false) => {\n setBusy(null);\n // Our own abort (a passkey ceremony cancelled by the unmount) is no failure.\n if ((error as { name?: unknown } | null)?.name === \"AbortError\") return;\n const code = authErrorCode(error);\n const message = messageFor(error, action, code, recoveryUsed);\n // The challenge expired between the steps: only a fresh sign-in helps.\n if ((action === \"code\" || action === \"set-password\") && code === \"token_invalid\") {\n setChallenge(null);\n setCode(\"\");\n setRecovery(\"\");\n setUseRecovery(false);\n setNewPassword(\"\");\n setConfirm(\"\");\n goTo(\"credentials\");\n }\n setFailure({ action, code, message, email: sentEmail });\n };\n\n const attempt = (\n action: SignInAction,\n call: () => Promise<SignInAnswer>,\n sentEmail?: string,\n recoveryUsed = false,\n ) => {\n if (busy) return;\n setBusy(action);\n setFailure(null);\n let result: Promise<SignInAnswer>;\n try {\n result = Promise.resolve(call());\n } catch (error) {\n result = Promise.reject(error);\n }\n result.then(\n (answer) => {\n if (mounted.current) present(answer);\n },\n (error: unknown) => {\n if (mounted.current) fail(error, action, sentEmail, recoveryUsed);\n },\n );\n };\n\n // Passkey autofill: armed once, on mount; the signal aborts on unmount. Its answer\n // arrives whenever the person picks a passkey from the email field's dropdown — and\n // is dropped if they have meanwhile started a sign-in of their own.\n const armAutofill = useEffectEvent((signal: AbortSignal) => passkeyAutofill?.(signal));\n const autofillAnswered = useEffectEvent((answer: SignInAnswer) => {\n if (busy === null) present(answer);\n });\n useEffect(() => {\n const controller = new AbortController();\n let pending: Promise<SignInAnswer | null | undefined>;\n try {\n pending = Promise.resolve(armAutofill(controller.signal));\n } catch (error) {\n pending = Promise.reject(error);\n }\n pending.then(\n (answer) => {\n if (answer && !controller.signal.aborted && mounted.current) autofillAnswered(answer);\n },\n () => {\n /* silent — unsupported, declined or aborted; the button stays the way in */\n },\n );\n return () => controller.abort();\n }, []);\n\n // Focus follows the step — not on mount (where it would pop a phone's keyboard over\n // the page), only once the person moved to another step.\n const shownStep = useRef(step);\n useEffect(() => {\n if (shownStep.current === step) return;\n shownStep.current = step;\n if (step === \"2fa\") codeRef.current?.focus();\n else if (step === \"password-change\") newPasswordRef.current?.focus();\n else if (emailRef.current?.value) passwordRef.current?.focus();\n else emailRef.current?.focus();\n }, [step]);\n // The backup-code switch swaps one field for the other under the focus that pressed\n // it; the field that came in is where the person is going. Only after a switch on\n // this step — arriving on it is the effect above.\n const shownRecovery = useRef(useRecovery);\n useEffect(() => {\n if (shownRecovery.current === useRecovery) return;\n shownRecovery.current = useRecovery;\n if (step !== \"2fa\") return;\n (useRecovery ? recoveryRef : codeRef).current?.focus();\n }, [useRecovery, step]);\n\n const backToCredentials = () => {\n if (busy) return;\n setChallenge(null);\n setFailure(null);\n setCode(\"\");\n setRecovery(\"\");\n setUseRecovery(false);\n setNewPassword(\"\");\n setConfirm(\"\");\n goTo(\"credentials\");\n };\n\n const working = busy !== null;\n const rootClass = cn(\"space-y-3\", className);\n\n /* ── Step 2: the second factor ── */\n if (step === \"2fa\" && challenge) {\n const recoveryOn = recoveryCode !== undefined && recoveryCode !== false;\n const recoveryLength = (typeof recoveryCode === \"object\" ? recoveryCode.length : undefined) ?? RECOVERY_CODE_LENGTH;\n const asRecovery = recoveryOn && useRecovery;\n const recoveryReady = recoveryCharacters(recovery) === recoveryLength;\n const ready = asRecovery ? recoveryReady : code.length === codeLength;\n const submitCode = (e: FormEvent) => {\n e.preventDefault();\n if (!ready) return;\n if (asRecovery) {\n attempt(\n \"code\",\n () => onCode({ challengeToken: challenge.token, code: recovery.trim(), kind: \"recovery\" }),\n undefined,\n true,\n );\n return;\n }\n attempt(\"code\", () => onCode({ challengeToken: challenge.token, code }));\n };\n const switchCode = () => {\n if (working) return;\n setFailure(null);\n setCode(\"\");\n setRecovery(\"\");\n setUseRecovery((on) => !on);\n };\n const codeError = failure?.action === \"code\" ? failure.message : undefined;\n const content = typeof twoFactorContent === \"function\" ? twoFactorContent(challenge.token) : twoFactorContent;\n return (\n <div {...rest} data-step=\"2fa\" className={rootClass}>\n <form className=\"space-y-3\" aria-labelledby={headingId} onSubmit={submitCode}>\n <Heading id={headingId} className=\"text-base font-semibold text-primary\">\n {labels.twoFactorTitle}\n </Heading>\n <p className={INTRO_CLASS}>\n {asRecovery ? (labels.recoveryIntro ?? DEFAULT_SIGN_IN_LABELS.recoveryIntro) : labels.twoFactorIntro}\n </p>\n {content}\n {asRecovery ? (\n // A plain text field, not the digits-only one: a backup code is letters and\n // digits in printed groups. Nothing a phone keyboard would \"correct\", and no\n // autofill — the browser's suggestion for an unknown field is the username.\n <Input\n ref={recoveryRef}\n label={labels.recoveryCode ?? DEFAULT_SIGN_IN_LABELS.recoveryCode}\n name=\"recovery-code\"\n value={recovery}\n onChange={(e) => {\n setRecovery(e.target.value);\n if (failure) setFailure(null);\n }}\n autoComplete=\"off\"\n autoCapitalize=\"characters\"\n autoCorrect=\"off\"\n spellCheck={false}\n inputClassName=\"font-mono\"\n hint={(labels.recoveryCodeHint ?? DEFAULT_SIGN_IN_LABELS.recoveryCodeHint)(recoveryLength)}\n readOnly={working}\n error={codeError}\n required\n />\n ) : (\n <OneTimeCodeInput\n ref={codeRef}\n label={labels.code}\n name=\"code\"\n value={code}\n length={codeLength}\n onChange={(next) => {\n setCode(next);\n if (failure) setFailure(null);\n }}\n readOnly={working}\n error={codeError}\n required\n />\n )}\n {recoveryOn && (\n <p className=\"text-sm\">\n <Button type=\"button\" variant=\"link\" size=\"sm\" disabled={working} onClick={switchCode}>\n {asRecovery\n ? (labels.useAuthenticatorCode ?? DEFAULT_SIGN_IN_LABELS.useAuthenticatorCode)\n : (labels.useRecoveryCode ?? DEFAULT_SIGN_IN_LABELS.useRecoveryCode)}\n </Button>\n </p>\n )}\n <Button type=\"submit\" className=\"w-full\" disabled={!working && !ready} pending={working}>\n {labels.verify}\n </Button>\n </form>\n <BackLink label={labels.backToSignIn} onClick={backToCredentials} disabled={working} />\n </div>\n );\n }\n\n /* ── Step 3: a new password ── */\n if (step === \"password-change\" && challenge) {\n const problem = newPasswordProblem(newPassword, confirm);\n const submitPassword = (e: FormEvent) => {\n e.preventDefault();\n if (problem) return;\n attempt(\"set-password\", () => onSetPassword({ challengeToken: challenge.token, password: newPassword }));\n };\n const content =\n typeof passwordChangeContent === \"function\" ? passwordChangeContent(challenge.extra) : passwordChangeContent;\n return (\n <div {...rest} data-step=\"password-change\" className={rootClass}>\n <form className=\"space-y-3\" aria-labelledby={headingId} onSubmit={submitPassword}>\n <Heading id={headingId} className=\"text-base font-semibold text-primary\">\n {labels.setPasswordTitle}\n </Heading>\n <p className={INTRO_CLASS}>{labels.setPasswordIntro}</p>\n {content}\n {/* Which account the new password is for, for the password manager: it files\n the new password under this username instead of guessing. */}\n {email !== \"\" && <input type=\"email\" name=\"username\" autoComplete=\"username\" value={email} readOnly hidden />}\n <NewPasswordFields\n password={newPassword}\n confirm={confirm}\n onPasswordChange={setNewPassword}\n onConfirmChange={setConfirm}\n busy={working}\n passwordRef={newPasswordRef}\n text={{ password: labels.newPassword, confirm: labels.confirmPassword, mismatch: labels.passwordMismatch }}\n />\n {failure?.action === \"set-password\" && (\n <AlertBanner tone=\"danger\" size=\"sm\" role=\"alert\">\n {failure.message}\n </AlertBanner>\n )}\n <Button type=\"submit\" className=\"w-full\" disabled={!working && problem !== null} pending={working}>\n {labels.setPasswordSubmit}\n </Button>\n </form>\n <BackLink label={labels.backToSignIn} onClick={backToCredentials} disabled={working} />\n </div>\n );\n }\n\n /* ── Step 1: the credentials ── */\n const submitCredentials = (e: FormEvent) => {\n e.preventDefault();\n const sent = email.trim();\n if (sent === \"\" || password === \"\") return;\n attempt(\"password\", () => onSubmit({ email: sent, password }), sent);\n };\n\n const refused = failure?.code === \"invalid_credentials\";\n const tagged =\n refused && failure?.action === \"password\" && emailTag && failure.email\n ? taggedEmail(failure.email, emailTag)\n : null;\n const showDeactivated = refused && hasContent(deactivatedContact);\n\n return (\n <div {...rest} data-step=\"credentials\" className={rootClass}>\n <form className=\"space-y-3\" onSubmit={submitCredentials}>\n {credentialsContent}\n <Input\n ref={emailRef}\n type=\"email\"\n name=\"email\"\n // \"webauthn\" lets the browser list passkeys in this field's dropdown, paired\n // with the conditional request `passkeyAutofill` arms.\n autoComplete=\"username webauthn\"\n label={labels.email}\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n readOnly={working}\n required\n />\n <CurrentPasswordInput\n ref={passwordRef}\n name=\"password\"\n label={labels.password}\n value={password}\n onValueChange={setPassword}\n busy={working}\n required\n />\n {failure && (\n // One live region for the sentence and its hints: they are one answer.\n <div role=\"alert\" className=\"space-y-1.5\">\n <AlertBanner tone=\"danger\" size=\"sm\">\n {failure.message}\n </AlertBanner>\n {showDeactivated && <p className={HINT_CLASS}>{withContact(labels.deactivatedHint, deactivatedContact)}</p>}\n {tagged && <p className={HINT_CLASS}>{labels.tagHint(tagged)}</p>}\n </div>\n )}\n <Button\n type=\"submit\"\n className=\"w-full\"\n pending={busy === \"password\" || busy === \"done\"}\n disabled={busy === \"passkey\"}\n >\n {labels.submit}\n </Button>\n {onPasskey && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n className=\"w-full\"\n pending={busy === \"passkey\"}\n disabled={busy === \"password\" || busy === \"done\"}\n onClick={() => attempt(\"passkey\", onPasskey)}\n >\n <KeyRound aria-hidden className=\"size-4\" />\n {labels.passkey}\n </Button>\n )}\n </form>\n {forgotHref !== undefined && (\n <p className=\"text-center text-sm\">\n <TextLink href={forgotHref} tone=\"secondary\">\n {labels.forgotPassword}\n </TextLink>\n </p>\n )}\n {accountLink !== null && (\n <p className=\"text-center text-sm text-secondary\">\n {labels.noAccount}{\" \"}\n <TextLink href={accountLink.href} tone=\"primary\">\n {accountLink.label}\n </TextLink>\n </p>\n )}\n </div>\n );\n}\n\n/** \"Back to sign in\" under the second and third step: a button, since it changes the\n * step in place rather than going anywhere. */\nfunction BackLink({ label, onClick, disabled }: { label: string; onClick: () => void; disabled: boolean }) {\n return (\n <p className=\"text-center text-sm\">\n <Button type=\"button\" variant=\"link\" tone=\"muted\" size=\"sm\" disabled={disabled} onClick={onClick}>\n {label}\n </Button>\n </p>\n );\n}\n"],"mappings":";AAuTM,cA+UE,YA/UF;AAvTN,SAAS,WAAW,gBAAgB,OAAO,QAAQ,gBAAgB;AAEnE,SAAS,gBAAgB;AAEzB,SAAS,oBAAwC;AACjD,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,kBAAkB;AAC3B,SAAS,4BAA4B;AACrC,SAAS,wBAAwB;AACjC,SAAS,gBAAgB;AACzB,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB,SAAS,oBAAoB;AAE7B,SAAS,wBAAwB;AACjC,SAAS,eAAe,eAAe,yBAAyB;AAEhE,SAAS,mBAAmB;AAC5B,SAAS,0BAA0B;AACnC,SAAS,yBAAyB;AAClC,SAAS,8BAA8B;AA+RvC,SAAS,YAAY,UAAkB,SAAiC;AACtE,QAAM,OACJ,OAAO,YAAY,YAAY,QAAQ,SAAS,GAAG,IACjD,oBAAC,YAAuB,MAAM,UAAU,OAAO,IAAI,MAAK,aACrD,qBADW,SAEd,IAEA,oBAAC,UAAoB,qBAAX,SAAmB;AAEjC,MAAI,CAAC,SAAS,SAAS,WAAW,EAAG,QAAO,CAAC,UAAU,KAAK,IAAI;AAChE,SAAO,SACJ,MAAM,eAAe,EACrB,OAAO,CAAC,SAAS,SAAS,EAAE,EAC5B,IAAI,CAAC,SAAU,SAAS,cAAc,OAAO,IAAK;AACvD;AAEA,MAAM,aAAa;AAGnB,MAAM,uBAAuB;AAK7B,SAAS,mBAAmB,OAAuB;AACjD,SAAO,MAAM,QAAQ,mBAAmB,EAAE,EAAE;AAC9C;AACA,MAAM,cAAc;AAkDb,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW,UAAU;AAAA,EACrB,QAAQ;AAAA,EACR;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,SAAS,aAAa,UAAU,wBAAwB,UAAU;AAGxE,QAAM,UAAU,iBAAiB;AACjC,QAAM,cACJ,WAAW,SACP,iBAAiB,SACf,OACA,EAAE,MAAM,cAAc,OAAO,OAAO,SAAS,IAC/C,OAAO,SAAS,aACd,EAAE,MAAM,OAAO,MAAM,OAAO,OAAO,SAAS,IAC5C;AAAA,IACE,MAAM,aAAa,QAAQ,OAAO,EAAE;AAAA,IACpC,OAAO,OAAO,iBAAiB,uBAAuB;AAAA,EACxD;AACR,QAAM,CAAC,MAAM,OAAO,IAAI,SAAqB,aAAa;AAC1D,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,YAAY;AAC/C,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,EAAE;AAC3C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,EAAE;AAEnC,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AACpD,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,EAAE;AAC3C,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,EAAE;AACjD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,EAAE;AACzC,QAAM,CAAC,WAAW,YAAY,IAAI,SAA8E,IAAI;AAGpH,QAAM,CAAC,MAAM,OAAO,IAAI,SAAuC,IAAI;AACnE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAyB,IAAI;AAC3D,QAAM,YAAY,MAAM;AAExB,QAAM,WAAW,OAAyB,IAAI;AAC9C,QAAM,cAAc,OAAyB,IAAI;AACjD,QAAM,UAAU,OAAyB,IAAI;AAC7C,QAAM,cAAc,OAAyB,IAAI;AACjD,QAAM,iBAAiB,OAAyB,IAAI;AAGpD,QAAM,UAAU,OAAO,IAAI;AAC3B,YAAU,MAAM;AACd,YAAQ,UAAU;AAClB,WAAO,MAAM;AACX,cAAQ,UAAU;AAAA,IACpB;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,OAAO,CAAC,SAAqB;AACjC,YAAQ,IAAI;AACZ,mBAAe,IAAI;AAAA,EACrB;AAEA,QAAM,UAAU,CAAC,WAAyB;AACxC,eAAW,IAAI;AACf,QAAI,OAAO,SAAS,aAAa;AAC/B,cAAQ,MAAM;AACd;AAAA,IACF;AACA,YAAQ,IAAI;AAEZ,gBAAY,EAAE;AACd,YAAQ,EAAE;AACV,gBAAY,EAAE;AACd,mBAAe,KAAK;AACpB,mBAAe,EAAE;AACjB,eAAW,EAAE;AACb,QAAI,OAAO,SAAS,OAAO;AACzB,mBAAa,EAAE,OAAO,OAAO,eAAe,CAAC;AAC7C,WAAK,KAAK;AAAA,IACZ,WAAW,OAAO,SAAS,mBAAmB;AAC5C,mBAAa,EAAE,OAAO,OAAO,gBAAgB,OAAO,OAAO,MAAM,CAAC;AAClE,WAAK,iBAAiB;AAAA,IACxB;AAAA,EACF;AAEA,QAAM,aAAa,CACjB,OACA,QACAA,OACA,iBACc;AACd,UAAM,MAAM,gBAAgB,OAAO,MAAM;AACzC,QAAI,WAAW,GAAG,EAAG,QAAO;AAI5B,QAAI,cAAc,KAAK;AACrB,cAAQ,OAAO,eAAe,uBAAuB,aAAa,kBAAkB,KAAK,CAAC;AAC5F,QAAI,WAAW,WAAY,QAAOA,UAAS,wBAAwB,OAAO,qBAAqB,OAAO;AACtG,QAAI,WAAW,UAAW,QAAO,OAAO;AACxC,QAAI,WAAW,QAAQ;AACrB,UAAIA,UAAS,gBAAiB,QAAO,OAAO;AAC5C,aAAO,eACF,OAAO,uBAAuB,uBAAuB,sBACtD,OAAO;AAAA,IACb;AACA,WAAOA,UAAS,kBAAkB,OAAO,UAAU,OAAO;AAAA,EAC5D;AAEA,QAAM,OAAO,CAAC,OAAgB,QAAsB,WAAoB,eAAe,UAAU;AAC/F,YAAQ,IAAI;AAEZ,QAAK,OAAqC,SAAS,aAAc;AACjE,UAAMA,QAAO,cAAc,KAAK;AAChC,UAAM,UAAU,WAAW,OAAO,QAAQA,OAAM,YAAY;AAE5D,SAAK,WAAW,UAAU,WAAW,mBAAmBA,UAAS,iBAAiB;AAChF,mBAAa,IAAI;AACjB,cAAQ,EAAE;AACV,kBAAY,EAAE;AACd,qBAAe,KAAK;AACpB,qBAAe,EAAE;AACjB,iBAAW,EAAE;AACb,WAAK,aAAa;AAAA,IACpB;AACA,eAAW,EAAE,QAAQ,MAAAA,OAAM,SAAS,OAAO,UAAU,CAAC;AAAA,EACxD;AAEA,QAAM,UAAU,CACd,QACA,MACA,WACA,eAAe,UACZ;AACH,QAAI,KAAM;AACV,YAAQ,MAAM;AACd,eAAW,IAAI;AACf,QAAI;AACJ,QAAI;AACF,eAAS,QAAQ,QAAQ,KAAK,CAAC;AAAA,IACjC,SAAS,OAAO;AACd,eAAS,QAAQ,OAAO,KAAK;AAAA,IAC/B;AACA,WAAO;AAAA,MACL,CAAC,WAAW;AACV,YAAI,QAAQ,QAAS,SAAQ,MAAM;AAAA,MACrC;AAAA,MACA,CAAC,UAAmB;AAClB,YAAI,QAAQ,QAAS,MAAK,OAAO,QAAQ,WAAW,YAAY;AAAA,MAClE;AAAA,IACF;AAAA,EACF;AAKA,QAAM,cAAc,eAAe,CAAC,WAAwB,kBAAkB,MAAM,CAAC;AACrF,QAAM,mBAAmB,eAAe,CAAC,WAAyB;AAChE,QAAI,SAAS,KAAM,SAAQ,MAAM;AAAA,EACnC,CAAC;AACD,YAAU,MAAM;AACd,UAAM,aAAa,IAAI,gBAAgB;AACvC,QAAI;AACJ,QAAI;AACF,gBAAU,QAAQ,QAAQ,YAAY,WAAW,MAAM,CAAC;AAAA,IAC1D,SAAS,OAAO;AACd,gBAAU,QAAQ,OAAO,KAAK;AAAA,IAChC;AACA,YAAQ;AAAA,MACN,CAAC,WAAW;AACV,YAAI,UAAU,CAAC,WAAW,OAAO,WAAW,QAAQ,QAAS,kBAAiB,MAAM;AAAA,MACtF;AAAA,MACA,MAAM;AAAA,MAEN;AAAA,IACF;AACA,WAAO,MAAM,WAAW,MAAM;AAAA,EAChC,GAAG,CAAC,CAAC;AAIL,QAAM,YAAY,OAAO,IAAI;AAC7B,YAAU,MAAM;AACd,QAAI,UAAU,YAAY,KAAM;AAChC,cAAU,UAAU;AACpB,QAAI,SAAS,MAAO,SAAQ,SAAS,MAAM;AAAA,aAClC,SAAS,kBAAmB,gBAAe,SAAS,MAAM;AAAA,aAC1D,SAAS,SAAS,MAAO,aAAY,SAAS,MAAM;AAAA,QACxD,UAAS,SAAS,MAAM;AAAA,EAC/B,GAAG,CAAC,IAAI,CAAC;AAIT,QAAM,gBAAgB,OAAO,WAAW;AACxC,YAAU,MAAM;AACd,QAAI,cAAc,YAAY,YAAa;AAC3C,kBAAc,UAAU;AACxB,QAAI,SAAS,MAAO;AACpB,KAAC,cAAc,cAAc,SAAS,SAAS,MAAM;AAAA,EACvD,GAAG,CAAC,aAAa,IAAI,CAAC;AAEtB,QAAM,oBAAoB,MAAM;AAC9B,QAAI,KAAM;AACV,iBAAa,IAAI;AACjB,eAAW,IAAI;AACf,YAAQ,EAAE;AACV,gBAAY,EAAE;AACd,mBAAe,KAAK;AACpB,mBAAe,EAAE;AACjB,eAAW,EAAE;AACb,SAAK,aAAa;AAAA,EACpB;AAEA,QAAM,UAAU,SAAS;AACzB,QAAM,YAAY,GAAG,aAAa,SAAS;AAG3C,MAAI,SAAS,SAAS,WAAW;AAC/B,UAAM,aAAa,iBAAiB,UAAa,iBAAiB;AAClE,UAAM,kBAAkB,OAAO,iBAAiB,WAAW,aAAa,SAAS,WAAc;AAC/F,UAAM,aAAa,cAAc;AACjC,UAAM,gBAAgB,mBAAmB,QAAQ,MAAM;AACvD,UAAM,QAAQ,aAAa,gBAAgB,KAAK,WAAW;AAC3D,UAAM,aAAa,CAAC,MAAiB;AACnC,QAAE,eAAe;AACjB,UAAI,CAAC,MAAO;AACZ,UAAI,YAAY;AACd;AAAA,UACE;AAAA,UACA,MAAM,OAAO,EAAE,gBAAgB,UAAU,OAAO,MAAM,SAAS,KAAK,GAAG,MAAM,WAAW,CAAC;AAAA,UACzF;AAAA,UACA;AAAA,QACF;AACA;AAAA,MACF;AACA,cAAQ,QAAQ,MAAM,OAAO,EAAE,gBAAgB,UAAU,OAAO,KAAK,CAAC,CAAC;AAAA,IACzE;AACA,UAAM,aAAa,MAAM;AACvB,UAAI,QAAS;AACb,iBAAW,IAAI;AACf,cAAQ,EAAE;AACV,kBAAY,EAAE;AACd,qBAAe,CAAC,OAAO,CAAC,EAAE;AAAA,IAC5B;AACA,UAAM,YAAY,SAAS,WAAW,SAAS,QAAQ,UAAU;AACjE,UAAM,UAAU,OAAO,qBAAqB,aAAa,iBAAiB,UAAU,KAAK,IAAI;AAC7F,WACE,qBAAC,SAAK,GAAG,MAAM,aAAU,OAAM,WAAW,WACxC;AAAA,2BAAC,UAAK,WAAU,aAAY,mBAAiB,WAAW,UAAU,YAChE;AAAA,4BAAC,WAAQ,IAAI,WAAW,WAAU,wCAC/B,iBAAO,gBACV;AAAA,QACA,oBAAC,OAAE,WAAW,aACX,uBAAc,OAAO,iBAAiB,uBAAuB,gBAAiB,OAAO,gBACxF;AAAA,QACC;AAAA,QACA;AAAA;AAAA;AAAA;AAAA,UAIC;AAAA,YAAC;AAAA;AAAA,cACC,KAAK;AAAA,cACL,OAAO,OAAO,gBAAgB,uBAAuB;AAAA,cACrD,MAAK;AAAA,cACL,OAAO;AAAA,cACP,UAAU,CAAC,MAAM;AACf,4BAAY,EAAE,OAAO,KAAK;AAC1B,oBAAI,QAAS,YAAW,IAAI;AAAA,cAC9B;AAAA,cACA,cAAa;AAAA,cACb,gBAAe;AAAA,cACf,aAAY;AAAA,cACZ,YAAY;AAAA,cACZ,gBAAe;AAAA,cACf,OAAO,OAAO,oBAAoB,uBAAuB,kBAAkB,cAAc;AAAA,cACzF,UAAU;AAAA,cACV,OAAO;AAAA,cACP,UAAQ;AAAA;AAAA,UACV;AAAA,YAEA;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,OAAO,OAAO;AAAA,YACd,MAAK;AAAA,YACL,OAAO;AAAA,YACP,QAAQ;AAAA,YACR,UAAU,CAAC,SAAS;AAClB,sBAAQ,IAAI;AACZ,kBAAI,QAAS,YAAW,IAAI;AAAA,YAC9B;AAAA,YACA,UAAU;AAAA,YACV,OAAO;AAAA,YACP,UAAQ;AAAA;AAAA,QACV;AAAA,QAED,cACC,oBAAC,OAAE,WAAU,WACX,8BAAC,UAAO,MAAK,UAAS,SAAQ,QAAO,MAAK,MAAK,UAAU,SAAS,SAAS,YACxE,uBACI,OAAO,wBAAwB,uBAAuB,uBACtD,OAAO,mBAAmB,uBAAuB,iBACxD,GACF;AAAA,QAEF,oBAAC,UAAO,MAAK,UAAS,WAAU,UAAS,UAAU,CAAC,WAAW,CAAC,OAAO,SAAS,SAC7E,iBAAO,QACV;AAAA,SACF;AAAA,MACA,oBAAC,YAAS,OAAO,OAAO,cAAc,SAAS,mBAAmB,UAAU,SAAS;AAAA,OACvF;AAAA,EAEJ;AAGA,MAAI,SAAS,qBAAqB,WAAW;AAC3C,UAAM,UAAU,mBAAmB,aAAa,OAAO;AACvD,UAAM,iBAAiB,CAAC,MAAiB;AACvC,QAAE,eAAe;AACjB,UAAI,QAAS;AACb,cAAQ,gBAAgB,MAAM,cAAc,EAAE,gBAAgB,UAAU,OAAO,UAAU,YAAY,CAAC,CAAC;AAAA,IACzG;AACA,UAAM,UACJ,OAAO,0BAA0B,aAAa,sBAAsB,UAAU,KAAK,IAAI;AACzF,WACE,qBAAC,SAAK,GAAG,MAAM,aAAU,mBAAkB,WAAW,WACpD;AAAA,2BAAC,UAAK,WAAU,aAAY,mBAAiB,WAAW,UAAU,gBAChE;AAAA,4BAAC,WAAQ,IAAI,WAAW,WAAU,wCAC/B,iBAAO,kBACV;AAAA,QACA,oBAAC,OAAE,WAAW,aAAc,iBAAO,kBAAiB;AAAA,QACnD;AAAA,QAGA,UAAU,MAAM,oBAAC,WAAM,MAAK,SAAQ,MAAK,YAAW,cAAa,YAAW,OAAO,OAAO,UAAQ,MAAC,QAAM,MAAC;AAAA,QAC3G;AAAA,UAAC;AAAA;AAAA,YACC,UAAU;AAAA,YACV;AAAA,YACA,kBAAkB;AAAA,YAClB,iBAAiB;AAAA,YACjB,MAAM;AAAA,YACN,aAAa;AAAA,YACb,MAAM,EAAE,UAAU,OAAO,aAAa,SAAS,OAAO,iBAAiB,UAAU,OAAO,iBAAiB;AAAA;AAAA,QAC3G;AAAA,QACC,SAAS,WAAW,kBACnB,oBAAC,eAAY,MAAK,UAAS,MAAK,MAAK,MAAK,SACvC,kBAAQ,SACX;AAAA,QAEF,oBAAC,UAAO,MAAK,UAAS,WAAU,UAAS,UAAU,CAAC,WAAW,YAAY,MAAM,SAAS,SACvF,iBAAO,mBACV;AAAA,SACF;AAAA,MACA,oBAAC,YAAS,OAAO,OAAO,cAAc,SAAS,mBAAmB,UAAU,SAAS;AAAA,OACvF;AAAA,EAEJ;AAGA,QAAM,oBAAoB,CAAC,MAAiB;AAC1C,MAAE,eAAe;AACjB,UAAM,OAAO,MAAM,KAAK;AACxB,QAAI,SAAS,MAAM,aAAa,GAAI;AACpC,YAAQ,YAAY,MAAM,SAAS,EAAE,OAAO,MAAM,SAAS,CAAC,GAAG,IAAI;AAAA,EACrE;AAEA,QAAM,UAAU,SAAS,SAAS;AAClC,QAAM,SACJ,WAAW,SAAS,WAAW,cAAc,YAAY,QAAQ,QAC7D,YAAY,QAAQ,OAAO,QAAQ,IACnC;AACN,QAAM,kBAAkB,WAAW,WAAW,kBAAkB;AAEhE,SACE,qBAAC,SAAK,GAAG,MAAM,aAAU,eAAc,WAAW,WAChD;AAAA,yBAAC,UAAK,WAAU,aAAY,UAAU,mBACnC;AAAA;AAAA,MACD;AAAA,QAAC;AAAA;AAAA,UACC,KAAK;AAAA,UACL,MAAK;AAAA,UACL,MAAK;AAAA,UAGL,cAAa;AAAA,UACb,OAAO,OAAO;AAAA,UACd,OAAO;AAAA,UACP,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,UACxC,UAAU;AAAA,UACV,UAAQ;AAAA;AAAA,MACV;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,KAAK;AAAA,UACL,MAAK;AAAA,UACL,OAAO,OAAO;AAAA,UACd,OAAO;AAAA,UACP,eAAe;AAAA,UACf,MAAM;AAAA,UACN,UAAQ;AAAA;AAAA,MACV;AAAA,MACC;AAAA,MAEC,qBAAC,SAAI,MAAK,SAAQ,WAAU,eAC1B;AAAA,4BAAC,eAAY,MAAK,UAAS,MAAK,MAC7B,kBAAQ,SACX;AAAA,QACC,mBAAmB,oBAAC,OAAE,WAAW,YAAa,sBAAY,OAAO,iBAAiB,kBAAkB,GAAE;AAAA,QACtG,UAAU,oBAAC,OAAE,WAAW,YAAa,iBAAO,QAAQ,MAAM,GAAE;AAAA,SAC/D;AAAA,MAEF;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAU;AAAA,UACV,SAAS,SAAS,cAAc,SAAS;AAAA,UACzC,UAAU,SAAS;AAAA,UAElB,iBAAO;AAAA;AAAA,MACV;AAAA,MACC,aACC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,WAAU;AAAA,UACV,SAAS,SAAS;AAAA,UAClB,UAAU,SAAS,cAAc,SAAS;AAAA,UAC1C,SAAS,MAAM,QAAQ,WAAW,SAAS;AAAA,UAE3C;AAAA,gCAAC,YAAS,eAAW,MAAC,WAAU,UAAS;AAAA,YACxC,OAAO;AAAA;AAAA;AAAA,MACV;AAAA,OAEJ;AAAA,IACC,eAAe,UACd,oBAAC,OAAE,WAAU,uBACX,8BAAC,YAAS,MAAM,YAAY,MAAK,aAC9B,iBAAO,gBACV,GACF;AAAA,IAED,gBAAgB,QACf,qBAAC,OAAE,WAAU,sCACV;AAAA,aAAO;AAAA,MAAW;AAAA,MACnB,oBAAC,YAAS,MAAM,YAAY,MAAM,MAAK,WACpC,sBAAY,OACf;AAAA,OACF;AAAA,KAEJ;AAEJ;AAIA,SAAS,SAAS,EAAE,OAAO,SAAS,SAAS,GAA8D;AACzG,SACE,oBAAC,OAAE,WAAU,uBACX,8BAAC,UAAO,MAAK,UAAS,SAAQ,QAAO,MAAK,SAAQ,MAAK,MAAK,UAAoB,SAC7E,iBACH,GACF;AAEJ;","names":["code"]}
|
|
@@ -75,14 +75,7 @@ function ResendStatus({
|
|
|
75
75
|
className
|
|
76
76
|
}) {
|
|
77
77
|
const failed = phase === "failed" ? describeError?.(error) ?? (isRateLimited(error) ? (labels.rateLimited ?? englishRateLimited)(retryAfterSeconds(error)) : labels.resendError) : "";
|
|
78
|
-
return /* @__PURE__ */ jsx(
|
|
79
|
-
"span",
|
|
80
|
-
{
|
|
81
|
-
role: "status",
|
|
82
|
-
className: cn(phase === "failed" ? "text-danger" : "text-success", className),
|
|
83
|
-
children: phase === "sent" ? labels.resent : failed
|
|
84
|
-
}
|
|
85
|
-
);
|
|
78
|
+
return /* @__PURE__ */ jsx("span", { role: "status", className: cn(phase === "failed" ? "text-danger" : "text-success", className), children: phase === "sent" ? labels.resent : failed });
|
|
86
79
|
}
|
|
87
80
|
function defaultVerifyFailure(error) {
|
|
88
81
|
return authErrorCode(error) === "token_expired" ? "expired" : "invalid";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/verify-email.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { CheckCircle2, MailWarning, TimerOff, XCircle } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { LoadingState } from \"../components/loading-state\";\nimport { Button } from \"../components/button\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { OUTCOME_CLASS, STATUS_HEADING_CLASS, isThenable, useOncePerKey } from \"./status-parts\";\nimport { authErrorCode, englishRateLimited, isRateLimited, retryAfterSeconds } from \"./auth-errors\";\nimport type { AuthHeadingLevel } from \"./status-parts\";\nimport { DEFAULT_VERIFY_EMAIL_LABELS } from \"./verify-email-labels\";\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the verification page and banner render — the `verifyEmail` namespace.\n * keksdose's words (`verify_email.*`, and its server's \"has expired\"). */\nexport interface VerifyEmailLabels {\n /** The page's heading. */\n title: string;\n /** While the link is redeemed. */\n verifying: string;\n verified: string;\n /** The way on from the page — into the app, or to sign-in. */\n continue: string;\n invalid: string;\n expired: string;\n /** The page was opened without a `token`. */\n noToken: string;\n /** Under `invalid`: why a link stops working. */\n invalidHint: string;\n /** Under a dead link, signed out: where a new one comes from. */\n requestInApp: string;\n /** Under a dead link, signed in, beside \"Send again\". */\n requestHere: string;\n /** The resend button, on the page and in the banner. */\n resend: string;\n /** The resend button while a just-sent mail cools down, given the seconds left. */\n resendIn: (seconds: number) => string;\n /** Said (and shown) after a resend went out. */\n resent: string;\n /** Said (and shown) after a resend failed. */\n resendError: string;\n /** 0.31.1: a resend refused as throttled — HTTP 429 (`isRateLimited`) — given the\n * `Retry-After` seconds, when the app's `describeError` has no words of its own\n * (keksdose borrowed the sign-in namespace's). */\n rateLimited?: (seconds?: number) => string;\n /** The banner's message. */\n banner: string;\n /** The banner's ×: it defers, it does not close anything. */\n dismiss: string;\n}\n\n// Defined in ./verify-email-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_VERIFY_EMAIL_LABELS };\n\n/** A minute: long enough that an impatient second click does not mail twice, short\n * enough that a mail lost to a spam filter can be re-sent without leaving the page. The\n * server's own limit is the real one (kastlan: 3 an hour per address). */\nconst DEFAULT_COOLDOWN = 60;\n\n/* ── Resend ──────────────────────────────────────────────────────────────── */\n\ntype ResendPhase = \"idle\" | \"sending\" | \"sent\" | \"failed\";\n\n/**\n * \"Send again\" with its own memory: busy while the request runs, then — after a mail\n * went out — held for `cooldown` seconds with the countdown on the button, so a second\n * click cannot mail twice and a throttled server is not asked to refuse. A failure frees\n * it at once. The outcome is a polite live region that is always rendered (a region that\n * mounts with its message is often not read), and visible.\n */\nfunction useResend(onResend: (() => Promise<unknown> | void) | undefined, cooldown: number) {\n const [phase, setPhase] = useState<ResendPhase>(\"idle\");\n // The refusal itself, for `describeError` (0.30.0): the server's own sentence — keksdose's\n // per-address throttle, \"try again in N minutes\" — beats a fixed \"could not send\".\n const [error, setError] = useState<unknown>(undefined);\n const [remaining, setRemaining] = useState(0);\n const counting = remaining > 0;\n useEffect(() => {\n if (!counting) return;\n const id = setInterval(() => setRemaining((r) => Math.max(0, r - 1)), 1000);\n return () => clearInterval(id);\n }, [counting]);\n\n const sent = () => {\n setPhase(\"sent\");\n setRemaining(Math.max(0, Math.round(cooldown)));\n };\n const send = () => {\n if (!onResend || phase === \"sending\" || counting) return;\n let result: unknown;\n try {\n result = onResend();\n } catch (e) {\n setError(e);\n setPhase(\"failed\");\n return;\n }\n if (!isThenable(result)) {\n sent();\n return;\n }\n setPhase(\"sending\");\n result.then(sent, (e: unknown) => {\n setError(e);\n setPhase(\"failed\");\n });\n };\n return { phase, remaining, send, error };\n}\n\nfunction ResendButton({\n state,\n labels,\n variant,\n}: {\n state: ReturnType<typeof useResend>;\n labels: VerifyEmailLabels;\n variant: \"secondary\" | \"accent\";\n}) {\n return (\n <Button\n type=\"button\"\n variant={variant}\n size={variant === \"secondary\" ? \"sm\" : undefined}\n onClick={state.send}\n pending={state.phase === \"sending\"}\n // `disabledReason`, not `disabled`: the button just pressed has the focus, and a\n // native `disabled` would drop it to <body>. The reason is what just happened.\n disabledReason={state.remaining > 0 ? labels.resent : undefined}\n >\n {state.remaining > 0 ? labels.resendIn(state.remaining) : labels.resend}\n </Button>\n );\n}\n\nfunction ResendStatus({\n phase,\n error,\n describeError,\n labels,\n className,\n}: {\n phase: ResendPhase;\n error?: unknown;\n describeError?: (error: unknown) => string | undefined;\n labels: VerifyEmailLabels;\n className?: string;\n}) {\n // The app's words first, then the throttle's, then the kit's catch-all.\n const failed =\n phase === \"failed\"\n ? (describeError?.(error) ??\n (isRateLimited(error)\n ? (labels.rateLimited ?? englishRateLimited)(retryAfterSeconds(error))\n : labels.resendError))\n : \"\";\n return (\n <span\n role=\"status\"\n className={cn(phase === \"failed\" ? \"text-danger\" : \"text-success\", className)}\n >\n {phase === \"sent\" ? labels.resent : failed}\n </span>\n );\n}\n\n/* ── The page ────────────────────────────────────────────────────────────── */\n\n/** What became of a confirmation link that did not verify. */\nexport type VerifyEmailFailure = \"invalid\" | \"expired\";\n\n/** The default when the app gives no `classifyError`: the coded refusal decides. */\nfunction defaultVerifyFailure(error: unknown): VerifyEmailFailure {\n return authErrorCode(error) === \"token_expired\" ? \"expired\" : \"invalid\";\n}\n\nexport interface VerifyEmailStatusProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"title\"> {\n /** The link's token — the app's `?token=`. Empty: the link was cut short, and the\n * page says so without asking anyone. */\n token: string;\n /**\n * Redeem it — `POST /auth/verify-email {token}`, once per token, on mount. A POST and\n * not the link's own GET (§10.10): a mail scanner that opens every link must not\n * confirm an address nobody read. Resolve when it verified (an address that already\n * was counts: keksdose's redeem is idempotent); reject with the client's error.\n */\n onVerify: (token: string) => Promise<unknown>;\n /** Which dead end a rejection is. Default: `invalid` for everything — §5.2's servers\n * answer one `token_invalid`; an app whose server tells expiry apart maps it here. */\n classifyError?: (error: unknown) => VerifyEmailFailure;\n /** 0.30.0: the words for a refused resend — the server's own sentence (a throttle's\n * \"try again in N minutes\"), else the kit's `resendError`. */\n describeError?: (error: unknown) => string | undefined;\n /** Someone is signed in on this device: \"Continue\" goes into the app, and a dead link\n * offers \"Send again\" (with `onResend`). */\n signedIn?: boolean;\n /** Send a new confirmation mail (`POST /auth/verify-email/resend`) — offered on a dead\n * link while signed in. */\n onResend?: () => Promise<unknown> | void;\n /** Seconds \"Send again\" waits after a mail went out. Default 60; `0` never waits. */\n cooldown?: number;\n /** Where \"Continue\" goes. Default `/` signed in, `/login` signed out. */\n continueHref?: string;\n /** App content under \"confirmed\" — keksdose's \"Others can now share a budget with you\n * directly.\" */\n verifiedContent?: ReactNode;\n /** The heading. Default `labels.title`; `null` draws none, for a page that puts it in\n * `AuthLayout`'s `title` instead. */\n title?: ReactNode;\n /** The heading's level. Default `h2`, under the page's `h1`. */\n headingAs?: AuthHeadingLevel;\n labels?: LabelOverride<VerifyEmailLabels>;\n}\n\ntype VerifyOutcome = \"verified\" | VerifyEmailFailure;\n\n/**\n * Redeem an email-confirmation link (`/verify-email?token=…`, docs/auth-harmonization.md\n * §7) — keksdose's `VerifyEmailPage` (features/auth/verify-email-page.tsx), with\n * kastlan's outcome blocks and its separate \"expired\".\n *\n * Opened from an inbox, often in a browser with no session, so the token is the only\n * credential and the page renders signed-out. It redeems once per token on mount\n * (StrictMode's double effect included — the token is single-use) and shows one of:\n * confirmed; not valid; expired; or the link had no token at all. Verification gates\n * nothing (§2.8), so every outcome ends on \"Continue\". Signed in, a dead link also\n * offers \"Send again\", which waits `cooldown` seconds after a mail went out.\n *\n * The outcome replaces the spinner, which says nothing to a screen reader on its own,\n * so it is also announced.\n */\nexport function VerifyEmailStatus({\n token,\n onVerify,\n classifyError,\n describeError,\n signedIn = false,\n onResend,\n cooldown = DEFAULT_COOLDOWN,\n continueHref,\n verifiedContent,\n title,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n className,\n ...rest\n}: VerifyEmailStatusProps) {\n const labels = useKitLabels(\"verifyEmail\", DEFAULT_VERIFY_EMAIL_LABELS, labelsProp);\n const [settled, setSettled] = useState<{ token: string; outcome: VerifyOutcome } | null>(null);\n const outcome: VerifyOutcome | \"verifying\" =\n token === \"\" ? \"invalid\" : settled?.token === token ? settled.outcome : \"verifying\";\n const { announce, regionProps } = useAnnounce();\n const resend = useResend(onResend, cooldown);\n\n useOncePerKey(token === \"\" ? null : token, onVerify, (key, result) => {\n // Without a classifier, the coded refusal decides (0.30.0): `token_expired` is\n // \"expired\", anything else \"invalid\".\n const next: VerifyOutcome = result.ok\n ? \"verified\"\n : (classifyError ?? defaultVerifyFailure)(result.error);\n setSettled({ token: key, outcome: next });\n announce(labels[next]);\n });\n\n const href = continueHref ?? (signedIn ? \"/\" : \"/login\");\n // `replace`: the token is spent, so Back must not land on this page again (keksdose F1).\n const continueButton = (\n <Button href={href} replace variant={outcome === \"verified\" ? \"accent\" : \"secondary\"}>\n {labels.continue}\n </Button>\n );\n const canResend = signedIn && onResend !== undefined;\n\n let body: ReactNode;\n if (outcome === \"verifying\") {\n body = <LoadingState size=\"sm\" label={labels.verifying} />;\n } else if (outcome === \"verified\") {\n body = (\n <EmptyState\n tone=\"success\"\n icon={<CheckCircle2 />}\n title={labels.verified}\n hint={verifiedContent}\n action={continueButton}\n className={OUTCOME_CLASS}\n />\n );\n } else {\n const hint = (\n <>\n {outcome === \"invalid\" && token !== \"\" ? `${labels.invalidHint} ` : \"\"}\n {canResend ? labels.requestHere : labels.requestInApp}\n </>\n );\n body = (\n <EmptyState\n tone=\"danger\"\n icon={outcome === \"expired\" ? <TimerOff /> : <XCircle />}\n title={token === \"\" ? labels.noToken : labels[outcome]}\n hint={hint}\n action={\n <>\n {canResend && <ResendButton state={resend} labels={labels} variant=\"accent\" />}\n {continueButton}\n </>\n }\n className={OUTCOME_CLASS}\n />\n );\n }\n\n return (\n <div\n {...rest}\n data-state={outcome}\n aria-busy={outcome === \"verifying\" || undefined}\n className={cn(\"space-y-3\", className)}\n >\n {title === null ? null : <Heading className={STATUS_HEADING_CLASS}>{title ?? labels.title}</Heading>}\n {body}\n {canResend && (\n <ResendStatus\n phase={resend.phase}\n error={resend.error}\n describeError={describeError}\n labels={labels}\n className=\"block text-center text-xs\"\n />\n )}\n {/* Rendered from the start, so the outcome is read when it arrives. */}\n <div {...regionProps} />\n </div>\n );\n}\n\n/* ── The banner ──────────────────────────────────────────────────────────── */\n\nexport interface EmailVerificationBannerProps {\n /** Send a new confirmation mail (`POST /auth/verify-email/resend`). Resolve when it\n * went out; reject and the banner says it could not. */\n onResend: () => Promise<unknown> | void;\n /** 0.30.0: the words for a refused resend — the server's own sentence, else the kit's. */\n describeError?: (error: unknown) => string | undefined;\n /** Seconds \"Send again\" waits after a mail went out. Default 60; `0` never waits. */\n cooldown?: number;\n /** The ×, \"Not now\". The banner does not hide itself: the app stops rendering it —\n * keksdose for the rest of the session. Left out, no ×. */\n onDismiss?: () => void;\n /** `strip` (default): keksdose's band across the top of the shell. `box`: kastlan's\n * callout inside the page. */\n variant?: \"strip\" | \"box\";\n /** The app's sentence after the kit's — keksdose's \"Until you do, nobody can share a\n * budget with you directly.\" */\n children?: ReactNode;\n className?: string;\n labels?: LabelOverride<VerifyEmailLabels>;\n /**\n * 0.48: on a phone hold the message to one line with a More / Less toggle —\n * {@link AlertBanner}'s `clampMobile`, passed through (kastlan S2: its banner took ~110px\n * of a 390px phone). The resend's outcome (\"Sent\", the error) then moves out of the\n * message to sit after \"Send again\", at every width, so the line that answers the press\n * is never hidden behind \"More\". Default `false`: the outcome stays in the message.\n *\n * 0.51 (kastlan K1, KX-5): in the `strip` variant \"Send again\" then stays on the clamped\n * message's line on a phone — one line, 43px, on a 390px phone in all seven catalogues —\n * where it took a line of its own (71px). The box's equivalent is `actionPlacement`.\n */\n clampMobile?: boolean;\n /**\n * 0.50 (kastlan K1): where \"Send again\" goes on a phone in the `box` variant —\n * {@link AlertBanner}'s `actionPlacement`, passed through. `\"flow\"` keeps it on the\n * message's line while the message keeps 10rem, else at the end of the next line,\n * instead of a line of its own (kastlan's ≤ 56px banner on a 390px phone, KX-5).\n * Default `\"own-line\"`, as before. The `strip` variant flows like that already.\n */\n actionPlacement?: \"own-line\" | \"flow\";\n}\n\n/**\n * The nudge for a signed-in account whose address is not confirmed — keksdose's\n * `VerifyEmailBanner` (features/auth/verify-email-banner.tsx), kastlan's copy of it.\n *\n * A prompt, not a wall: verification gates nothing (§2.8). The app renders it while\n * `/auth/me` says `email_verified: false`, and stops when the user dismisses it or the\n * page above confirms the address. \"Send again\" waits `cooldown` seconds after a mail\n * went out (the button says how long), and its outcome is written into the banner and\n * read out — no toast needed.\n */\nexport function EmailVerificationBanner({\n onResend,\n describeError,\n cooldown = DEFAULT_COOLDOWN,\n onDismiss,\n variant = \"strip\",\n children,\n className,\n labels: labelsProp,\n clampMobile = false,\n actionPlacement,\n}: EmailVerificationBannerProps) {\n const labels = useKitLabels(\"verifyEmail\", DEFAULT_VERIFY_EMAIL_LABELS, labelsProp);\n const resend = useResend(onResend, cooldown);\n // One live region either way, mounted from the start (a region added later is often not\n // read out): in the message, or — clamped — after the button, outside the clamp.\n const status = (\n <ResendStatus\n phase={resend.phase}\n error={resend.error}\n describeError={describeError}\n labels={labels}\n className=\"font-medium\"\n />\n );\n const button = <ResendButton state={resend} labels={labels} variant=\"secondary\" />;\n return (\n <AlertBanner\n tone=\"warning\"\n variant={variant}\n icon={<MailWarning />}\n action={\n clampMobile ? (\n <>\n {button}\n {status}\n </>\n ) : (\n button\n )\n }\n onDismiss={onDismiss}\n dismissLabel={onDismiss ? labels.dismiss : undefined}\n clampMobile={clampMobile}\n actionPlacement={actionPlacement}\n className={className}\n >\n {labels.banner}\n {children != null && children !== false && <> {children}</>}\n {!clampMobile && <> {status}</>}\n </AlertBanner>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/auth/verify-email.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { CheckCircle2, MailWarning, TimerOff, XCircle } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { LoadingState } from \"../components/loading-state\";\nimport { Button } from \"../components/button\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { OUTCOME_CLASS, STATUS_HEADING_CLASS, isThenable, useOncePerKey } from \"./status-parts\";\nimport { authErrorCode, englishRateLimited, isRateLimited, retryAfterSeconds } from \"./auth-errors\";\nimport type { AuthHeadingLevel } from \"./status-parts\";\nimport { DEFAULT_VERIFY_EMAIL_LABELS } from \"./verify-email-labels\";\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the verification page and banner render — the `verifyEmail` namespace.\n * keksdose's words (`verify_email.*`, and its server's \"has expired\"). */\nexport interface VerifyEmailLabels {\n /** The page's heading. */\n title: string;\n /** While the link is redeemed. */\n verifying: string;\n verified: string;\n /** The way on from the page — into the app, or to sign-in. */\n continue: string;\n invalid: string;\n expired: string;\n /** The page was opened without a `token`. */\n noToken: string;\n /** Under `invalid`: why a link stops working. */\n invalidHint: string;\n /** Under a dead link, signed out: where a new one comes from. */\n requestInApp: string;\n /** Under a dead link, signed in, beside \"Send again\". */\n requestHere: string;\n /** The resend button, on the page and in the banner. */\n resend: string;\n /** The resend button while a just-sent mail cools down, given the seconds left. */\n resendIn: (seconds: number) => string;\n /** Said (and shown) after a resend went out. */\n resent: string;\n /** Said (and shown) after a resend failed. */\n resendError: string;\n /** 0.31.1: a resend refused as throttled — HTTP 429 (`isRateLimited`) — given the\n * `Retry-After` seconds, when the app's `describeError` has no words of its own\n * (keksdose borrowed the sign-in namespace's). */\n rateLimited?: (seconds?: number) => string;\n /** The banner's message. */\n banner: string;\n /** The banner's ×: it defers, it does not close anything. */\n dismiss: string;\n}\n\n// Defined in ./verify-email-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_VERIFY_EMAIL_LABELS };\n\n/** A minute: long enough that an impatient second click does not mail twice, short\n * enough that a mail lost to a spam filter can be re-sent without leaving the page. The\n * server's own limit is the real one (kastlan: 3 an hour per address). */\nconst DEFAULT_COOLDOWN = 60;\n\n/* ── Resend ──────────────────────────────────────────────────────────────── */\n\ntype ResendPhase = \"idle\" | \"sending\" | \"sent\" | \"failed\";\n\n/**\n * \"Send again\" with its own memory: busy while the request runs, then — after a mail\n * went out — held for `cooldown` seconds with the countdown on the button, so a second\n * click cannot mail twice and a throttled server is not asked to refuse. A failure frees\n * it at once. The outcome is a polite live region that is always rendered (a region that\n * mounts with its message is often not read), and visible.\n */\nfunction useResend(onResend: (() => Promise<unknown> | void) | undefined, cooldown: number) {\n const [phase, setPhase] = useState<ResendPhase>(\"idle\");\n // The refusal itself, for `describeError` (0.30.0): the server's own sentence — keksdose's\n // per-address throttle, \"try again in N minutes\" — beats a fixed \"could not send\".\n const [error, setError] = useState<unknown>(undefined);\n const [remaining, setRemaining] = useState(0);\n const counting = remaining > 0;\n useEffect(() => {\n if (!counting) return;\n const id = setInterval(() => setRemaining((r) => Math.max(0, r - 1)), 1000);\n return () => clearInterval(id);\n }, [counting]);\n\n const sent = () => {\n setPhase(\"sent\");\n setRemaining(Math.max(0, Math.round(cooldown)));\n };\n const send = () => {\n if (!onResend || phase === \"sending\" || counting) return;\n let result: unknown;\n try {\n result = onResend();\n } catch (e) {\n setError(e);\n setPhase(\"failed\");\n return;\n }\n if (!isThenable(result)) {\n sent();\n return;\n }\n setPhase(\"sending\");\n result.then(sent, (e: unknown) => {\n setError(e);\n setPhase(\"failed\");\n });\n };\n return { phase, remaining, send, error };\n}\n\nfunction ResendButton({\n state,\n labels,\n variant,\n}: {\n state: ReturnType<typeof useResend>;\n labels: VerifyEmailLabels;\n variant: \"secondary\" | \"accent\";\n}) {\n return (\n <Button\n type=\"button\"\n variant={variant}\n size={variant === \"secondary\" ? \"sm\" : undefined}\n onClick={state.send}\n pending={state.phase === \"sending\"}\n // `disabledReason`, not `disabled`: the button just pressed has the focus, and a\n // native `disabled` would drop it to <body>. The reason is what just happened.\n disabledReason={state.remaining > 0 ? labels.resent : undefined}\n >\n {state.remaining > 0 ? labels.resendIn(state.remaining) : labels.resend}\n </Button>\n );\n}\n\nfunction ResendStatus({\n phase,\n error,\n describeError,\n labels,\n className,\n}: {\n phase: ResendPhase;\n error?: unknown;\n describeError?: (error: unknown) => string | undefined;\n labels: VerifyEmailLabels;\n className?: string;\n}) {\n // The app's words first, then the throttle's, then the kit's catch-all.\n const failed =\n phase === \"failed\"\n ? (describeError?.(error) ??\n (isRateLimited(error)\n ? (labels.rateLimited ?? englishRateLimited)(retryAfterSeconds(error))\n : labels.resendError))\n : \"\";\n return (\n <span role=\"status\" className={cn(phase === \"failed\" ? \"text-danger\" : \"text-success\", className)}>\n {phase === \"sent\" ? labels.resent : failed}\n </span>\n );\n}\n\n/* ── The page ────────────────────────────────────────────────────────────── */\n\n/** What became of a confirmation link that did not verify. */\nexport type VerifyEmailFailure = \"invalid\" | \"expired\";\n\n/** The default when the app gives no `classifyError`: the coded refusal decides. */\nfunction defaultVerifyFailure(error: unknown): VerifyEmailFailure {\n return authErrorCode(error) === \"token_expired\" ? \"expired\" : \"invalid\";\n}\n\nexport interface VerifyEmailStatusProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"title\"> {\n /** The link's token — the app's `?token=`. Empty: the link was cut short, and the\n * page says so without asking anyone. */\n token: string;\n /**\n * Redeem it — `POST /auth/verify-email {token}`, once per token, on mount. A POST and\n * not the link's own GET (§10.10): a mail scanner that opens every link must not\n * confirm an address nobody read. Resolve when it verified (an address that already\n * was counts: keksdose's redeem is idempotent); reject with the client's error.\n */\n onVerify: (token: string) => Promise<unknown>;\n /** Which dead end a rejection is. Default: `invalid` for everything — §5.2's servers\n * answer one `token_invalid`; an app whose server tells expiry apart maps it here. */\n classifyError?: (error: unknown) => VerifyEmailFailure;\n /** 0.30.0: the words for a refused resend — the server's own sentence (a throttle's\n * \"try again in N minutes\"), else the kit's `resendError`. */\n describeError?: (error: unknown) => string | undefined;\n /** Someone is signed in on this device: \"Continue\" goes into the app, and a dead link\n * offers \"Send again\" (with `onResend`). */\n signedIn?: boolean;\n /** Send a new confirmation mail (`POST /auth/verify-email/resend`) — offered on a dead\n * link while signed in. */\n onResend?: () => Promise<unknown> | void;\n /** Seconds \"Send again\" waits after a mail went out. Default 60; `0` never waits. */\n cooldown?: number;\n /** Where \"Continue\" goes. Default `/` signed in, `/login` signed out. */\n continueHref?: string;\n /** App content under \"confirmed\" — keksdose's \"Others can now share a budget with you\n * directly.\" */\n verifiedContent?: ReactNode;\n /** The heading. Default `labels.title`; `null` draws none, for a page that puts it in\n * `AuthLayout`'s `title` instead. */\n title?: ReactNode;\n /** The heading's level. Default `h2`, under the page's `h1`. */\n headingAs?: AuthHeadingLevel;\n labels?: LabelOverride<VerifyEmailLabels>;\n}\n\ntype VerifyOutcome = \"verified\" | VerifyEmailFailure;\n\n/**\n * Redeem an email-confirmation link (`/verify-email?token=…`, docs/auth-harmonization.md\n * §7) — keksdose's `VerifyEmailPage` (features/auth/verify-email-page.tsx), with\n * kastlan's outcome blocks and its separate \"expired\".\n *\n * Opened from an inbox, often in a browser with no session, so the token is the only\n * credential and the page renders signed-out. It redeems once per token on mount\n * (StrictMode's double effect included — the token is single-use) and shows one of:\n * confirmed; not valid; expired; or the link had no token at all. Verification gates\n * nothing (§2.8), so every outcome ends on \"Continue\". Signed in, a dead link also\n * offers \"Send again\", which waits `cooldown` seconds after a mail went out.\n *\n * The outcome replaces the spinner, which says nothing to a screen reader on its own,\n * so it is also announced.\n */\nexport function VerifyEmailStatus({\n token,\n onVerify,\n classifyError,\n describeError,\n signedIn = false,\n onResend,\n cooldown = DEFAULT_COOLDOWN,\n continueHref,\n verifiedContent,\n title,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n className,\n ...rest\n}: VerifyEmailStatusProps) {\n const labels = useKitLabels(\"verifyEmail\", DEFAULT_VERIFY_EMAIL_LABELS, labelsProp);\n const [settled, setSettled] = useState<{ token: string; outcome: VerifyOutcome } | null>(null);\n const outcome: VerifyOutcome | \"verifying\" =\n token === \"\" ? \"invalid\" : settled?.token === token ? settled.outcome : \"verifying\";\n const { announce, regionProps } = useAnnounce();\n const resend = useResend(onResend, cooldown);\n\n useOncePerKey(token === \"\" ? null : token, onVerify, (key, result) => {\n // Without a classifier, the coded refusal decides (0.30.0): `token_expired` is\n // \"expired\", anything else \"invalid\".\n const next: VerifyOutcome = result.ok ? \"verified\" : (classifyError ?? defaultVerifyFailure)(result.error);\n setSettled({ token: key, outcome: next });\n announce(labels[next]);\n });\n\n const href = continueHref ?? (signedIn ? \"/\" : \"/login\");\n // `replace`: the token is spent, so Back must not land on this page again (keksdose F1).\n const continueButton = (\n <Button href={href} replace variant={outcome === \"verified\" ? \"accent\" : \"secondary\"}>\n {labels.continue}\n </Button>\n );\n const canResend = signedIn && onResend !== undefined;\n\n let body: ReactNode;\n if (outcome === \"verifying\") {\n body = <LoadingState size=\"sm\" label={labels.verifying} />;\n } else if (outcome === \"verified\") {\n body = (\n <EmptyState\n tone=\"success\"\n icon={<CheckCircle2 />}\n title={labels.verified}\n hint={verifiedContent}\n action={continueButton}\n className={OUTCOME_CLASS}\n />\n );\n } else {\n const hint = (\n <>\n {outcome === \"invalid\" && token !== \"\" ? `${labels.invalidHint} ` : \"\"}\n {canResend ? labels.requestHere : labels.requestInApp}\n </>\n );\n body = (\n <EmptyState\n tone=\"danger\"\n icon={outcome === \"expired\" ? <TimerOff /> : <XCircle />}\n title={token === \"\" ? labels.noToken : labels[outcome]}\n hint={hint}\n action={\n <>\n {canResend && <ResendButton state={resend} labels={labels} variant=\"accent\" />}\n {continueButton}\n </>\n }\n className={OUTCOME_CLASS}\n />\n );\n }\n\n return (\n <div\n {...rest}\n data-state={outcome}\n aria-busy={outcome === \"verifying\" || undefined}\n className={cn(\"space-y-3\", className)}\n >\n {title === null ? null : <Heading className={STATUS_HEADING_CLASS}>{title ?? labels.title}</Heading>}\n {body}\n {canResend && (\n <ResendStatus\n phase={resend.phase}\n error={resend.error}\n describeError={describeError}\n labels={labels}\n className=\"block text-center text-xs\"\n />\n )}\n {/* Rendered from the start, so the outcome is read when it arrives. */}\n <div {...regionProps} />\n </div>\n );\n}\n\n/* ── The banner ──────────────────────────────────────────────────────────── */\n\nexport interface EmailVerificationBannerProps {\n /** Send a new confirmation mail (`POST /auth/verify-email/resend`). Resolve when it\n * went out; reject and the banner says it could not. */\n onResend: () => Promise<unknown> | void;\n /** 0.30.0: the words for a refused resend — the server's own sentence, else the kit's. */\n describeError?: (error: unknown) => string | undefined;\n /** Seconds \"Send again\" waits after a mail went out. Default 60; `0` never waits. */\n cooldown?: number;\n /** The ×, \"Not now\". The banner does not hide itself: the app stops rendering it —\n * keksdose for the rest of the session. Left out, no ×. */\n onDismiss?: () => void;\n /** `strip` (default): keksdose's band across the top of the shell. `box`: kastlan's\n * callout inside the page. */\n variant?: \"strip\" | \"box\";\n /** The app's sentence after the kit's — keksdose's \"Until you do, nobody can share a\n * budget with you directly.\" */\n children?: ReactNode;\n className?: string;\n labels?: LabelOverride<VerifyEmailLabels>;\n /**\n * 0.48: on a phone hold the message to one line with a More / Less toggle —\n * {@link AlertBanner}'s `clampMobile`, passed through (kastlan S2: its banner took ~110px\n * of a 390px phone). The resend's outcome (\"Sent\", the error) then moves out of the\n * message to sit after \"Send again\", at every width, so the line that answers the press\n * is never hidden behind \"More\". Default `false`: the outcome stays in the message.\n *\n * 0.51 (kastlan K1, KX-5): in the `strip` variant \"Send again\" then stays on the clamped\n * message's line on a phone — one line, 43px, on a 390px phone in all seven catalogues —\n * where it took a line of its own (71px). The box's equivalent is `actionPlacement`.\n */\n clampMobile?: boolean;\n /**\n * 0.50 (kastlan K1): where \"Send again\" goes on a phone in the `box` variant —\n * {@link AlertBanner}'s `actionPlacement`, passed through. `\"flow\"` keeps it on the\n * message's line while the message keeps 10rem, else at the end of the next line,\n * instead of a line of its own (kastlan's ≤ 56px banner on a 390px phone, KX-5).\n * Default `\"own-line\"`, as before. The `strip` variant flows like that already.\n */\n actionPlacement?: \"own-line\" | \"flow\";\n}\n\n/**\n * The nudge for a signed-in account whose address is not confirmed — keksdose's\n * `VerifyEmailBanner` (features/auth/verify-email-banner.tsx), kastlan's copy of it.\n *\n * A prompt, not a wall: verification gates nothing (§2.8). The app renders it while\n * `/auth/me` says `email_verified: false`, and stops when the user dismisses it or the\n * page above confirms the address. \"Send again\" waits `cooldown` seconds after a mail\n * went out (the button says how long), and its outcome is written into the banner and\n * read out — no toast needed.\n */\nexport function EmailVerificationBanner({\n onResend,\n describeError,\n cooldown = DEFAULT_COOLDOWN,\n onDismiss,\n variant = \"strip\",\n children,\n className,\n labels: labelsProp,\n clampMobile = false,\n actionPlacement,\n}: EmailVerificationBannerProps) {\n const labels = useKitLabels(\"verifyEmail\", DEFAULT_VERIFY_EMAIL_LABELS, labelsProp);\n const resend = useResend(onResend, cooldown);\n // One live region either way, mounted from the start (a region added later is often not\n // read out): in the message, or — clamped — after the button, outside the clamp.\n const status = (\n <ResendStatus\n phase={resend.phase}\n error={resend.error}\n describeError={describeError}\n labels={labels}\n className=\"font-medium\"\n />\n );\n const button = <ResendButton state={resend} labels={labels} variant=\"secondary\" />;\n return (\n <AlertBanner\n tone=\"warning\"\n variant={variant}\n icon={<MailWarning />}\n action={\n clampMobile ? (\n <>\n {button}\n {status}\n </>\n ) : (\n button\n )\n }\n onDismiss={onDismiss}\n dismissLabel={onDismiss ? labels.dismiss : undefined}\n clampMobile={clampMobile}\n actionPlacement={actionPlacement}\n className={className}\n >\n {labels.banner}\n {children != null && children !== false && <> {children}</>}\n {!clampMobile && <> {status}</>}\n </AlertBanner>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/billing/billing-banner.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { CreditCard, Hourglass, PencilOff } from \"lucide-react\";\n\nimport { DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport type { AlertSize, AlertTone } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { Spinner } from \"../components/spinner\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport type { BillingPortalAccess } from \"./billing-overview\";\nimport { ExternalMark } from \"./subscription-actions\";\n\n/**\n * The billing banners (docs/billing-harmonization.md §3.3, §7, §12.6, §12.21):\n *\n * - `trial-ending` — from 7 days before `trial_ends_at` (§3.3);\n * - `grant-ending` — from 7 days before `comped_until`: a beta user's 12 months, an\n * operator's grant;\n * - `payment-failed` — from the first failed payment (`past_due`), while the provider\n * retries; since 0.42, for an admin who isn't the buyer (`portal=\"buyer_only\"`), it\n * names who can update the payment method, with no button;\n * - `plan-ended` — out of good standing: read-only, never locked (§2.3);\n * - `guest` — a guest in someone else's read-only item (§12.6);\n * - `processing` — back from checkout before the webhook landed (§12.21).\n */\nexport type BillingBannerKind =\n | \"trial-ending\"\n | \"grant-ending\"\n | \"payment-failed\"\n | \"plan-ended\"\n | \"guest\"\n | \"processing\";\n\n/** A `div`'s props, `ref` included since 0.35 (round 0.35 R4, the follow-up to API-2): they\n * go on to the {@link AlertBanner}, so the ref is the banner's `<div>` — with an × too,\n * since the banner has no `href` or `onClick` that would make its row a link or a button. */\ninterface BillingBannerBaseProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"onClick\"> {\n /**\n * Over the kind's tone: `trial-ending` and `grant-ending` are `info` until the last\n * three days and `warning` in them; `payment-failed` is `danger` (money did not\n * arrive — kastlan's past-due strip was red); `plan-ended` is `warning`, a state with\n * everything still there rather than an error; `guest` and `processing` are `info`.\n */\n tone?: AlertTone;\n /** `strip` (default): the band under the top bar, on every page. `box`: the same\n * message on the subscription page itself. */\n variant?: \"strip\" | \"box\";\n /** Default `sm` for a strip (the demo's banner), `md` for a box. */\n size?: AlertSize;\n /**\n * Where the action goes — the subscription page, say. The app's action, as an `href`\n * or as {@link onAction}; without either there is no button. Never a write-locked\n * commit: billing itself always works (§3.3).\n */\n actionHref?: string;\n /** What the action does — the app's portal call (`POST /billing/portal`, then off to\n * its `url`): for `payment-failed` with `{target: \"payment_method\"}`, the portal's\n * page for the card (§14.5); for `processing`, `useCheckoutProcessing`'s\n * `checkAgain`. The kit sends no request. */\n onAction?: () => void;\n /**\n * Over the kind's action words: \"Choose a plan\" (trial, grant, plan ended), \"Update\n * payment method\" (payment failed), \"Check again\" (processing). The guest banner has\n * none of its own: an action there needs this.\n */\n actionLabel?: string;\n /** The action is under way: the button's spinner, no second press. */\n actionPending?: boolean;\n /**\n * 0.43 (§17): the action opens the provider's portal in a NEW TAB (`openProviderPage`\n * in {@link onAction}): the external-link mark, \"(opens in a new tab)\" for a reader,\n * and `target=\"_blank\"` on an {@link actionHref}. Default: true for `payment-failed`\n * with `onAction` (the portal's card page), false otherwise — an `actionHref` is\n * usually the app's own subscription page.\n */\n actionNewTab?: boolean;\n /** An × that calls this; the app removes the banner and remembers that it did. */\n onDismiss?: () => void;\n dismissLabel?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\nexport type BillingBannerProps = BillingBannerBaseProps &\n (\n | {\n kind: \"trial-ending\" | \"grant-ending\";\n /** Calendar days left — `billingDaysLeft(trial_ends_at)`. 0 is today. */\n daysLeft: number;\n item?: never;\n portal?: never;\n buyerName?: never;\n }\n | {\n kind: \"guest\";\n /** The item's own name (\"Household 2026\"), or nothing. NEVER the owner's status:\n * the banner says \"for now\" and nothing more (§12.6). */\n item?: string;\n daysLeft?: never;\n portal?: never;\n buyerName?: never;\n }\n | {\n kind: \"payment-failed\";\n /**\n * 0.42 (§16.7 A, §16.8): `GET /billing/status`'s `portal` (`BillingStanding`).\n * `\"buyer_only\"` — the viewer isn't the buyer, so the portal's page for the card\n * would answer 403: the banner says who can update the payment method\n * (`paymentFailedAskBuyer`) and shows NO button, whatever `actionHref` or\n * `onAction` say. `\"open\"` (default): as before.\n */\n portal?: BillingPortalAccess;\n /** 0.42: the status's `buyer_name`, under `\"buyer_only\"`. Empty: \"…the admin who\n * bought the subscription\". */\n buyerName?: string | null;\n daysLeft?: never;\n item?: never;\n }\n | {\n kind: \"plan-ended\" | \"processing\";\n daysLeft?: never;\n item?: never;\n portal?: never;\n buyerName?: never;\n }\n );\n\n/** The last days, in which an ending trial or grant turns from news into a warning. */\nconst LAST_DAYS = 3;\n\nfunction defaultTone(kind: BillingBannerKind, daysLeft: number | undefined): AlertTone {\n switch (kind) {\n case \"trial-ending\":\n case \"grant-ending\":\n return (daysLeft ?? 0) > LAST_DAYS ? \"info\" : \"warning\";\n case \"payment-failed\":\n return \"danger\";\n case \"plan-ended\":\n return \"warning\";\n default:\n return \"info\";\n }\n}\n\n/**\n * One of the billing banners, as a kit `AlertBanner` strip (§7): the kind's sentence, its\n * glyph and tone, and the app's action. Carries `data-billing-banner` with the kind.\n *\n * The app decides WHICH banner shows, from `GET /billing/overview` (and hides all of them\n * while billing is off, §4); the kit only says it. A guest's banner takes no status at\n * all — the type does not let one in — so the owner's payment state cannot leak into it\n * by a wrong prop (§12.6).\n *\n * `processing` is a live region (`role=\"status\"`): it appears when the person comes back\n * from checkout and goes when the overview catches up (§12.21), and a screen reader\n * should hear both. The others are page furniture, role-less as the strip is.\n */\nexport function BillingBanner(props: BillingBannerProps) {\n const {\n kind,\n daysLeft,\n item,\n portal,\n buyerName,\n tone,\n variant = \"strip\",\n size,\n actionHref,\n onAction,\n actionLabel,\n actionPending,\n actionNewTab,\n onDismiss,\n dismissLabel,\n labels: labelsProp,\n role,\n ...rest\n } = props;\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const days = Math.max(0, Math.floor(daysLeft ?? 0));\n\n let text: string;\n let icon: ReactNode;\n let ownAction: string | undefined;\n // 0.42 (§16.7 A): only the buyer can reach the portal's card page.\n const askBuyer = kind === \"payment-failed\" && portal === \"buyer_only\";\n switch (kind) {\n case \"trial-ending\":\n text = labels.trialEnding(days);\n icon = <Hourglass />;\n ownAction = labels.choosePlan;\n break;\n case \"grant-ending\":\n text = labels.grantEnding(days);\n icon = <Hourglass />;\n ownAction = labels.choosePlan;\n break;\n case \"payment-failed\":\n text = askBuyer ? labels.paymentFailedAskBuyer(buyerName || undefined) : labels.paymentFailed;\n icon = <CreditCard />;\n ownAction = labels.updatePayment;\n break;\n case \"plan-ended\":\n text = labels.planEnded;\n icon = <PencilOff />;\n ownAction = labels.choosePlan;\n break;\n case \"guest\":\n text = labels.guestReadOnly(item);\n icon = <PencilOff />;\n ownAction = undefined;\n break;\n case \"processing\":\n text = labels.processing;\n icon = <Spinner label={null} />;\n ownAction = labels.checkAgain;\n break;\n }\n\n const words = actionLabel ?? ownAction;\n const newTab = actionNewTab ?? (kind === \"payment-failed\" && actionHref === undefined && onAction !== undefined);\n const action =\n askBuyer || words === undefined || (actionHref === undefined && onAction === undefined) ? undefined : actionHref !== undefined ? (\n <Button href={actionHref} variant=\"secondary\" size=\"sm\" external={newTab || undefined}>\n {words}\n {newTab && <ExternalMark />}\n </Button>\n ) : (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={onAction}\n pending={actionPending}\n className={newTab ? \"relative\" : undefined}\n data-portal-action={newTab ? \"payment_method\" : undefined}\n >\n {words}\n {newTab && <ExternalMark reader={common.opensInNewTab} />}\n </Button>\n );\n\n return (\n <AlertBanner\n {...rest}\n role={role ?? (kind === \"processing\" ? \"status\" : undefined)}\n variant={variant}\n size={size ?? (variant === \"strip\" ? \"sm\" : \"md\")}\n tone={tone ?? defaultTone(kind, days)}\n icon={icon}\n action={action}\n onDismiss={onDismiss}\n dismissLabel={dismissLabel}\n data-billing-banner={kind}\n >\n {text}\n </AlertBanner>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/billing/billing-banner.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { CreditCard, Hourglass, PencilOff } from \"lucide-react\";\n\nimport { DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport type { AlertSize, AlertTone } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { Spinner } from \"../components/spinner\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport type { BillingPortalAccess } from \"./billing-overview\";\nimport { ExternalMark } from \"./subscription-actions\";\n\n/**\n * The billing banners (docs/billing-harmonization.md §3.3, §7, §12.6, §12.21):\n *\n * - `trial-ending` — from 7 days before `trial_ends_at` (§3.3);\n * - `grant-ending` — from 7 days before `comped_until`: a beta user's 12 months, an\n * operator's grant;\n * - `payment-failed` — from the first failed payment (`past_due`), while the provider\n * retries; since 0.42, for an admin who isn't the buyer (`portal=\"buyer_only\"`), it\n * names who can update the payment method, with no button;\n * - `plan-ended` — out of good standing: read-only, never locked (§2.3);\n * - `guest` — a guest in someone else's read-only item (§12.6);\n * - `processing` — back from checkout before the webhook landed (§12.21).\n */\nexport type BillingBannerKind =\n \"trial-ending\" | \"grant-ending\" | \"payment-failed\" | \"plan-ended\" | \"guest\" | \"processing\";\n\n/** A `div`'s props, `ref` included since 0.35 (round 0.35 R4, the follow-up to API-2): they\n * go on to the {@link AlertBanner}, so the ref is the banner's `<div>` — with an × too,\n * since the banner has no `href` or `onClick` that would make its row a link or a button. */\ninterface BillingBannerBaseProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"onClick\"> {\n /**\n * Over the kind's tone: `trial-ending` and `grant-ending` are `info` until the last\n * three days and `warning` in them; `payment-failed` is `danger` (money did not\n * arrive — kastlan's past-due strip was red); `plan-ended` is `warning`, a state with\n * everything still there rather than an error; `guest` and `processing` are `info`.\n */\n tone?: AlertTone;\n /** `strip` (default): the band under the top bar, on every page. `box`: the same\n * message on the subscription page itself. */\n variant?: \"strip\" | \"box\";\n /** Default `sm` for a strip (the demo's banner), `md` for a box. */\n size?: AlertSize;\n /**\n * Where the action goes — the subscription page, say. The app's action, as an `href`\n * or as {@link onAction}; without either there is no button. Never a write-locked\n * commit: billing itself always works (§3.3).\n */\n actionHref?: string;\n /** What the action does — the app's portal call (`POST /billing/portal`, then off to\n * its `url`): for `payment-failed` with `{target: \"payment_method\"}`, the portal's\n * page for the card (§14.5); for `processing`, `useCheckoutProcessing`'s\n * `checkAgain`. The kit sends no request. */\n onAction?: () => void;\n /**\n * Over the kind's action words: \"Choose a plan\" (trial, grant, plan ended), \"Update\n * payment method\" (payment failed), \"Check again\" (processing). The guest banner has\n * none of its own: an action there needs this.\n */\n actionLabel?: string;\n /** The action is under way: the button's spinner, no second press. */\n actionPending?: boolean;\n /**\n * 0.43 (§17): the action opens the provider's portal in a NEW TAB (`openProviderPage`\n * in {@link onAction}): the external-link mark, \"(opens in a new tab)\" for a reader,\n * and `target=\"_blank\"` on an {@link actionHref}. Default: true for `payment-failed`\n * with `onAction` (the portal's card page), false otherwise — an `actionHref` is\n * usually the app's own subscription page.\n */\n actionNewTab?: boolean;\n /** An × that calls this; the app removes the banner and remembers that it did. */\n onDismiss?: () => void;\n dismissLabel?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\nexport type BillingBannerProps = BillingBannerBaseProps &\n (\n | {\n kind: \"trial-ending\" | \"grant-ending\";\n /** Calendar days left — `billingDaysLeft(trial_ends_at)`. 0 is today. */\n daysLeft: number;\n item?: never;\n portal?: never;\n buyerName?: never;\n }\n | {\n kind: \"guest\";\n /** The item's own name (\"Household 2026\"), or nothing. NEVER the owner's status:\n * the banner says \"for now\" and nothing more (§12.6). */\n item?: string;\n daysLeft?: never;\n portal?: never;\n buyerName?: never;\n }\n | {\n kind: \"payment-failed\";\n /**\n * 0.42 (§16.7 A, §16.8): `GET /billing/status`'s `portal` (`BillingStanding`).\n * `\"buyer_only\"` — the viewer isn't the buyer, so the portal's page for the card\n * would answer 403: the banner says who can update the payment method\n * (`paymentFailedAskBuyer`) and shows NO button, whatever `actionHref` or\n * `onAction` say. `\"open\"` (default): as before.\n */\n portal?: BillingPortalAccess;\n /** 0.42: the status's `buyer_name`, under `\"buyer_only\"`. Empty: \"…the admin who\n * bought the subscription\". */\n buyerName?: string | null;\n daysLeft?: never;\n item?: never;\n }\n | {\n kind: \"plan-ended\" | \"processing\";\n daysLeft?: never;\n item?: never;\n portal?: never;\n buyerName?: never;\n }\n );\n\n/** The last days, in which an ending trial or grant turns from news into a warning. */\nconst LAST_DAYS = 3;\n\nfunction defaultTone(kind: BillingBannerKind, daysLeft: number | undefined): AlertTone {\n switch (kind) {\n case \"trial-ending\":\n case \"grant-ending\":\n return (daysLeft ?? 0) > LAST_DAYS ? \"info\" : \"warning\";\n case \"payment-failed\":\n return \"danger\";\n case \"plan-ended\":\n return \"warning\";\n default:\n return \"info\";\n }\n}\n\n/**\n * One of the billing banners, as a kit `AlertBanner` strip (§7): the kind's sentence, its\n * glyph and tone, and the app's action. Carries `data-billing-banner` with the kind.\n *\n * The app decides WHICH banner shows, from `GET /billing/overview` (and hides all of them\n * while billing is off, §4); the kit only says it. A guest's banner takes no status at\n * all — the type does not let one in — so the owner's payment state cannot leak into it\n * by a wrong prop (§12.6).\n *\n * `processing` is a live region (`role=\"status\"`): it appears when the person comes back\n * from checkout and goes when the overview catches up (§12.21), and a screen reader\n * should hear both. The others are page furniture, role-less as the strip is.\n */\nexport function BillingBanner(props: BillingBannerProps) {\n const {\n kind,\n daysLeft,\n item,\n portal,\n buyerName,\n tone,\n variant = \"strip\",\n size,\n actionHref,\n onAction,\n actionLabel,\n actionPending,\n actionNewTab,\n onDismiss,\n dismissLabel,\n labels: labelsProp,\n role,\n ...rest\n } = props;\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const days = Math.max(0, Math.floor(daysLeft ?? 0));\n\n let text: string;\n let icon: ReactNode;\n let ownAction: string | undefined;\n // 0.42 (§16.7 A): only the buyer can reach the portal's card page.\n const askBuyer = kind === \"payment-failed\" && portal === \"buyer_only\";\n switch (kind) {\n case \"trial-ending\":\n text = labels.trialEnding(days);\n icon = <Hourglass />;\n ownAction = labels.choosePlan;\n break;\n case \"grant-ending\":\n text = labels.grantEnding(days);\n icon = <Hourglass />;\n ownAction = labels.choosePlan;\n break;\n case \"payment-failed\":\n text = askBuyer ? labels.paymentFailedAskBuyer(buyerName || undefined) : labels.paymentFailed;\n icon = <CreditCard />;\n ownAction = labels.updatePayment;\n break;\n case \"plan-ended\":\n text = labels.planEnded;\n icon = <PencilOff />;\n ownAction = labels.choosePlan;\n break;\n case \"guest\":\n text = labels.guestReadOnly(item);\n icon = <PencilOff />;\n ownAction = undefined;\n break;\n case \"processing\":\n text = labels.processing;\n icon = <Spinner label={null} />;\n ownAction = labels.checkAgain;\n break;\n }\n\n const words = actionLabel ?? ownAction;\n const newTab = actionNewTab ?? (kind === \"payment-failed\" && actionHref === undefined && onAction !== undefined);\n const action =\n askBuyer || words === undefined || (actionHref === undefined && onAction === undefined) ? undefined : actionHref !==\n undefined ? (\n <Button href={actionHref} variant=\"secondary\" size=\"sm\" external={newTab || undefined}>\n {words}\n {newTab && <ExternalMark />}\n </Button>\n ) : (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={onAction}\n pending={actionPending}\n className={newTab ? \"relative\" : undefined}\n data-portal-action={newTab ? \"payment_method\" : undefined}\n >\n {words}\n {newTab && <ExternalMark reader={common.opensInNewTab} />}\n </Button>\n );\n\n return (\n <AlertBanner\n {...rest}\n role={role ?? (kind === \"processing\" ? \"status\" : undefined)}\n variant={variant}\n size={size ?? (variant === \"strip\" ? \"sm\" : \"md\")}\n tone={tone ?? defaultTone(kind, days)}\n icon={icon}\n action={action}\n onDismiss={onDismiss}\n dismissLabel={dismissLabel}\n data-billing-banner={kind}\n >\n {text}\n </AlertBanner>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/billing/billing-labels.ts"],"sourcesContent":["import { useKitLabels } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\n\n/**\n * The subscription's status vocabulary (docs/billing-harmonization.md §3.2) — server-kit's\n * `SubscriptionStatus`, value for value:\n *\n * - `trialing` — the 30-day trial without a card (§2.7), or a pure guest's row whose trial\n * has not started (§12.9, `trial_ends_at` empty);\n * - `active` — paid;\n * - `past_due` — a payment failed; in good standing during the provider's retries (§3.3);\n * - `canceled` — the payer cancelled; the American spelling is the wire's, the word shown\n * is \"Cancelled\";\n * - `expired` — a trial, a grant or a failed subscription ran out: read-only (§2.3);\n * - `comped` — free by the operator's grant or as a beta user (§3.2, `comped_until`).\n */\nexport type SubscriptionStatus = \"trialing\" | \"active\" | \"past_due\" | \"canceled\" | \"expired\" | \"comped\";\n\n/** Every status, in the order a lifecycle reads: trial, paid, trouble, ended, granted. */\nexport const SUBSCRIPTION_STATUSES: readonly SubscriptionStatus[] = Object.freeze([\n \"trialing\",\n \"active\",\n \"past_due\",\n \"canceled\",\n \"expired\",\n \"comped\",\n]);\n\n/**\n * The words of the subscription parts — the `billing` namespace of `<UiKitProvider labels>`\n * (docs/billing-harmonization.md §7, §10; ui-kit 0.32.0).\n *\n * WORDING (§12.24): the English group is \"Subscription\", never \"Plan\" — keksdose's \"plan\"\n * is the month's budget plan (\"One plan per month\"), and two meanings of one word on one\n * settings page is the clash the review found. \"Plan\" stays the word for WHAT is bought\n * (Free, Pro), \"subscription\" for the standing arrangement. German says \"Tarif\" for the\n * plan and \"Abonnement\" for the subscription (§10).\n *\n * Prices are GROSS (§12.17): Swiss consumer law (PBV) wants a consumer price with the\n * MWST in it, so the cards say so (`vatIncluded`).\n */\nexport interface BillingLabels {\n /** The settings group or admin section the parts live on (§7, §12.24): \"Subscription\". */\n group: string;\n\n /* ── PlanPicker / PlanCard (§7) ── */\n /** The picker's name when it has no visible `legend`: \"Plans\". */\n plans: string;\n /** The name of the billing-period switch: \"Billing period\". */\n interval: string;\n /** The switch's two options — yearly first (§12.17). */\n yearly: string;\n monthly: string;\n /** The name of the currency switch: \"Currency\". */\n currency: string;\n /** After a price: \"per year\", \"per month\". */\n perYear: string;\n perMonth: string;\n /** Under a price (§12.17): \"VAT included\". German: \"inkl. MWST\". */\n vatIncluded: string;\n /** A plan with no price, or a price of 0: \"Free\". */\n free: string;\n /** A plan with prices, but none for this currency and period. */\n notOffered: string;\n /** A limit of `null` (§3.1): \"Unlimited\". */\n unlimited: string;\n /** The accessible names of a card's two lists. */\n limits: string;\n features: string;\n /** The chip on the payer's current plan. */\n current: string;\n /** The picker's button for `plan` (the plan's display name) with no current plan —\n * during a trial, or after a plan ended: \"Choose Pro\". */\n choose: (plan: string) => string;\n /** …for a plan after the current one in the catalogue's order: \"Upgrade to Pro\". */\n upgrade: (plan: string) => string;\n /** …for a plan before it: \"Switch to Free\". */\n downgrade: (plan: string) => string;\n /** Why the button is off while the current plan is the one picked. */\n isCurrent: string;\n /** Why it is off while nothing is picked. */\n pickFirst: string;\n\n /* ── SubscriptionStatusChip (§6) ── */\n /** One word per status. */\n status: Record<SubscriptionStatus, string>;\n\n /* ── BillingBanner (§3.3, §12.6, §12.21) ── */\n /** A trial ends in `days` calendar days — 0 is today, 1 tomorrow. */\n trialEnding: (days: number) => string;\n /** A free grant (`comped_until`: a beta user's 12 months, an operator's grant) ends in\n * `days` calendar days. */\n grantEnding: (days: number) => string;\n /** The first failed payment (`past_due`), while the provider retries. */\n paymentFailed: string;\n /** Out of good standing: read-only, never locked (§2.3). */\n planEnded: string;\n /**\n * A guest in someone else's item that is read-only for billing (§12.6). It never says\n * WHY beyond \"for now\": the owner's payment status is the owner's personal data.\n * `item` is the item's own name (\"Household 2026\"), shown in quotes — or `undefined`.\n */\n guestReadOnly: (item?: string) => string;\n /** Back from checkout before the provider's webhook landed (§12.21). */\n processing: string;\n /** The action of the trial, grant and plan-ended banners, and of `PlanLimitNotice`'s\n * `upgrade` mode: \"Choose a plan\". */\n choosePlan: string;\n /** The payment-failed banner's action, to the provider's portal: \"Update payment method\". */\n updatePayment: string;\n /**\n * 0.42 (§16.7 A, §16.8): the payment-failed banner for an admin who isn't the buyer\n * (`portal: \"buyer_only\"`), with no button — only the buyer can change the card.\n * `buyer` is their display name, or `undefined` when they are gone (\"the admin who\n * bought the subscription\").\n */\n paymentFailedAskBuyer: (buyer?: string) => string;\n /** The processing banner's action, when the app gives one: \"Check again\". */\n checkAgain: string;\n\n /* ── The read-only lock (§3.3, §12.6) ── */\n /** The `WriteLockProvider` reason for the payer (§3.3, verbatim). A guest's reason is\n * {@link guestReadOnly}. */\n lockReason: string;\n\n /* ── PlanLimitNotice (§3.4) ── */\n /** The notice's first line: \"Plan limit reached\". */\n limitReached: string;\n /** Mode `upgrade`: what the action does. */\n limitUpgrade: string;\n /** Mode `contact` (the operator grants plans by hand, keksdose's today). */\n limitContact: string;\n /** Mode `contact`'s action, a mail to support: \"Ask for more\". */\n askForMore: string;\n /** The meter's value: \"3 of 3\". Both figures are formatted already. */\n usage: (used: string, limit: string) => string;\n /** The subject of the `contact` mail; `dimension` is the app's name for it (\"Budgets\"). */\n contactSubject: (dimension: string) => string;\n /**\n * The figure line of the limit's toast (`usePlanLimitToast`, §14.11) and of an\n * operator's plan change over the new limit (§14.12): \"Budgets: 3 of 3\". A key of its\n * own rather than `${dimension}: ${usage}`, because French puts a space before the\n * colon. Both figures are formatted already.\n */\n limitUsageLine: (dimension: string, used: string, limit: string) => string;\n\n /* ── The provider's portal (§7, §12.26) ── */\n /** The portal: payment method and invoices (§2.8, the provider hosts both). */\n manage: string;\n /**\n * The visible cancel link on the app's subscription page (§12.26). German: §312k BGB\n * wants the button that leads to the cancellation labelled \"Verträge hier kündigen\" or\n * as unambiguously — a translator should use exactly that.\n */\n cancel: string;\n /**\n * In place of both while the payer hasn't reached the provider (the overview's\n * `at_provider` is false, §14.5): there is no customer to open a portal for yet. Also\n * the words for `409 billing_not_at_provider`, a stale page that asked anyway.\n */\n notAtProvider: string;\n\n /* ── The portal for the buyer only, the in-app cancel (0.42, §16) ── */\n /**\n * In place of \"Payment and invoices\" for an admin who isn't the buyer (`portal:\n * \"buyer_only\"`, §16.2 decision 3): the provider's portal opens only for the buyer.\n * `buyer` is their display name — or `undefined` when they are gone or have left the\n * company, and the words then say \"the admin who bought the subscription\".\n */\n buyerHolds: (buyer?: string) => string;\n /** `CancelSubscriptionConfirm`'s title: \"Cancel subscription\". */\n cancelConfirmTitle: string;\n /** Its body; `date` is the period's end, formatted: \"Your subscription ends on\n * 9 Nov 2026. Until then everything works as before.\" */\n cancelConfirmBody: (date: string) => string;\n /** Its confirm button (§16.4): \"Cancel subscription now\". German: \"Jetzt kündigen\",\n * the confirmation button's words in §312k BGB. */\n cancelConfirmAction: string;\n /** In place of \"Cancel subscription\" while a cancel is scheduled (§16.7 C, `POST\n * /billing/resume`): \"Keep subscription\". */\n keepSubscription: string;\n /** 0.43 (§17.3): the muted note under the actions while the portal opens in a new tab —\n * `provider` the provider's name (\"Paddle\"), `app` the app's; without the app, \"this\n * page\". \"Payment and invoices open at Paddle in a new tab; Kastlan stays open here.\" */\n portalOpensNewTab: (provider: string, app?: string) => string;\n\n /* ── The pay page (§14.4) ── */\n /* Built into the static page `dist/pay/` at the kit's build: it has no React and no\n provider, so an app's override never reaches it. */\n /** While the checkout opens. */\n payOpening: string;\n /** A link without a transaction, or billing switched off: nothing to pay. */\n payNothing: string;\n /** Paddle's script didn't load, or the page's configuration is wrong. */\n payFailed: string;\n /** The buyer closed the checkout without paying. */\n payBack: string;\n /** 0.39 (§15.2.5): paid, for a transaction that names no app the page knows (a\n * payment-method update from Paddle's e-mail) — no way back to offer. */\n payDone: string;\n\n /* ── PlanSwitchConfirm (0.39, §15.2.10) ── */\n /** The dialog's title: \"Change plan\". */\n switchTitle: string;\n /** The line naming both plans, each with its billing period: \"From Pro (Monthly) to\n * Business (Yearly)\". */\n switchFromTo: (from: string, to: string) => string;\n /** What an upgrade charges at once (Paddle's preview, prorated): \"Charged now\". */\n switchChargedNow: string;\n /** What a downgrade credits — the customer's balance, which Paddle takes off the next\n * charges: \"Credited towards your next charges\". */\n switchCredited: string;\n /** The next regular charge; `date` is formatted: \"Next charge on 9 Nov 2026\". */\n switchNextCharge: (date: string) => string;\n /** The confirm button: \"Change plan\". */\n switchConfirm: string;\n\n /* ── The permanent-grant warning (0.42, §16.2 decision 5, §16.8 B) ── */\n /** The warning's title, before a checkout (`PlanPicker`) or a switch\n * (`PlanSwitchConfirm`) under a grant without an end: \"Your free access continues\". */\n grantWarningTitle: string;\n /** What the purchase does under it: nothing to the plan while the grant runs. */\n grantWarningBody: string;\n /** `PlanPicker`'s button that goes on to the checkout: \"Buy anyway\". */\n grantWarningAction: string;\n\n /* ── Offline (§12.5) ── */\n /** Changes refused as `billing_read_only` inside a sync reply, kept queued and sent\n * after payment: \"3 changes waiting for a plan\". */\n waitingChanges: (count: number) => string;\n /** The sign-out guard's way out for someone who will not pay. */\n discardWaiting: string;\n\n /* ── Refusals with no part of their own (0.32.1, kastlan's 0.32 report) ── */\n /**\n * `billing_not_configured`: billing is on but the provider's settings are missing — the\n * operator's mistake, so the words ask the payer to wait, not to act. For the toast or\n * alert after \"Choose a plan\" or \"Payment and invoices\" (`isBillingError(err,\n * \"billing_not_configured\")`). `billing_read_only` and `plan_limit` need none: the\n * write lock, the banner and `PlanLimitNotice` say them.\n */\n notConfigured: string;\n /** `billing_disabled`: billing is switched off on this server (§2.9), for a page or a\n * link that reached it anyway. */\n disabled: string;\n /* 0.33.0 — server-kit 0.7's refusals (§14.2). `billing_not_at_provider` is\n {@link notAtProvider}'s: a stale page that asked for the portal anyway. */\n /** `billing_provider_unavailable` (502): the provider didn't answer. Try again. */\n providerUnavailable: string;\n /** `billing_already_subscribed` (409): a second checkout while a subscription runs\n * (§14.6). Since 0.39 plan changes go through the app's own switch (§15.2.10), not the\n * portal, and the words say so. */\n alreadySubscribed: string;\n /** `billing_plan_not_sold` (422): the catalogue doesn't sell this plan with that\n * period or currency — at a checkout, or (0.39) a switch to a plan no longer sold. */\n planNotSold: string;\n /** 0.39 (§15.2.10): `billing_not_subscribed` (409), a plan switch with no running\n * provider subscription — the page offers the checkout instead. */\n notSubscribed: string;\n /** 0.39: `billing_plan_unchanged` (409), a switch to the price the payer pays now. */\n planUnchanged: string;\n}\n\n/** \"today\", \"tomorrow\", \"in 5 days\". */\nfunction inDays(days: number): string {\n if (days <= 0) return \"today\";\n if (days === 1) return \"tomorrow\";\n return `in ${days} days`;\n}\n\nexport const DEFAULT_BILLING_LABELS: BillingLabels = {\n group: \"Subscription\",\n\n plans: \"Plans\",\n interval: \"Billing period\",\n yearly: \"Yearly\",\n monthly: \"Monthly\",\n currency: \"Currency\",\n perYear: \"per year\",\n perMonth: \"per month\",\n vatIncluded: \"VAT included\",\n free: \"Free\",\n notOffered: \"Not offered for this billing period\",\n unlimited: \"Unlimited\",\n limits: \"Limits\",\n features: \"Included\",\n current: \"Current plan\",\n choose: (plan) => `Choose ${plan}`,\n upgrade: (plan) => `Upgrade to ${plan}`,\n downgrade: (plan) => `Switch to ${plan}`,\n isCurrent: \"This is your current plan.\",\n pickFirst: \"Pick a plan first.\",\n\n status: {\n trialing: \"Trial\",\n active: \"Active\",\n past_due: \"Payment overdue\",\n canceled: \"Cancelled\",\n expired: \"Expired\",\n comped: \"Complimentary\",\n },\n\n trialEnding: (days) =>\n `Your trial ends ${inDays(days)}. After that you can still view everything, but changes need a plan.`,\n grantEnding: (days) =>\n `Your free access ends ${inDays(days)}. After that you can still view everything, but changes need a plan.`,\n paymentFailed: \"Your last payment didn’t go through. Update your payment method to keep your plan.\",\n planEnded: \"Your plan has ended. You can still view and export everything; choose a plan to make changes again.\",\n guestReadOnly: (item) =>\n item ? `“${item}” is read-only for now; its owner can lift that.` : \"This is read-only for now; its owner can lift that.\",\n processing: \"Your payment is being processed. Your plan starts as soon as it’s confirmed; this page updates by itself.\",\n choosePlan: \"Choose a plan\",\n updatePayment: \"Update payment method\",\n paymentFailedAskBuyer: (buyer) =>\n buyer\n ? `The payment failed. ${buyer} can update the payment method.`\n : \"The payment failed. The admin who bought the subscription can update the payment method.\",\n checkAgain: \"Check again\",\n\n lockReason: \"Your plan has ended. Choose a plan to make changes again.\",\n\n limitReached: \"Plan limit reached\",\n limitUpgrade: \"To add more, choose a plan with a higher limit.\",\n limitContact: \"To add more, ask us for a higher limit.\",\n askForMore: \"Ask for more\",\n usage: (used, limit) => `${used} of ${limit}`,\n contactSubject: (dimension) => `Plan limit: ${dimension}`,\n limitUsageLine: (dimension, used, limit) => `${dimension}: ${used} of ${limit}`,\n\n manage: \"Payment and invoices\",\n cancel: \"Cancel subscription\",\n notAtProvider: \"Nothing to manage yet. Payments and invoices appear here once you have a paid plan.\",\n buyerHolds: (buyer) =>\n buyer\n ? `The payment method and the invoices are with ${buyer}, who bought the subscription.`\n : \"The payment method and the invoices are with the admin who bought the subscription.\",\n cancelConfirmTitle: \"Cancel subscription\",\n cancelConfirmBody: (date) => `Your subscription ends on ${date}. Until then everything works as before.`,\n cancelConfirmAction: \"Cancel subscription now\",\n keepSubscription: \"Keep subscription\",\n portalOpensNewTab: (provider, app) =>\n `Payment and invoices open at ${provider} in a new tab; ${app ? `${app} stays open here` : \"this page stays open\"}.`,\n\n payOpening: \"Opening the secure checkout…\",\n payNothing: \"There’s no payment to make here. To choose a plan, go back to the app.\",\n payFailed: \"The checkout couldn’t be opened. Please go back and try again in a moment.\",\n payBack: \"The checkout was closed. You can go back, or reload this page to open it again.\",\n payDone: \"Payment complete. You can close this page.\",\n\n switchTitle: \"Change plan\",\n switchFromTo: (from, to) => `From ${from} to ${to}`,\n switchChargedNow: \"Charged now\",\n switchCredited: \"Credited towards your next charges\",\n switchNextCharge: (date) => `Next charge on ${date}`,\n switchConfirm: \"Change plan\",\n\n grantWarningTitle: \"Your free access continues\",\n grantWarningBody:\n \"You have free access with no end date. A purchase won’t change your plan while the free access runs: you would pay for a plan that doesn’t apply.\",\n grantWarningAction: \"Buy anyway\",\n\n waitingChanges: (count) =>\n count === 1 ? \"1 change waiting for a plan\" : `${count} changes waiting for a plan`,\n discardWaiting: \"Discard waiting changes\",\n\n notConfigured: \"Payments aren't set up yet. Please try again later.\",\n disabled: \"Subscriptions aren't available here.\",\n providerUnavailable: \"The payment provider can’t be reached right now. Please try again in a moment.\",\n alreadySubscribed: \"You already have a subscription. Change your plan above.\",\n planNotSold: \"This plan isn’t offered for that billing period or currency. Please choose another.\",\n notSubscribed: \"There’s no running subscription to change. Choose a plan to subscribe.\",\n planUnchanged: \"That’s already your plan. Pick another plan or billing period.\",\n};\n\n/** The `billing` namespace, resolved: English, then the provider, then `labels` — which\n * may be partial at every depth (`{ status: { comped: \"Beta\" } }`). */\nexport function useBillingLabels(labels?: LabelOverride<BillingLabels>): BillingLabels {\n return useKitLabels(\"billing\", DEFAULT_BILLING_LABELS, labels);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/billing/billing-labels.ts"],"sourcesContent":["import { useKitLabels } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\n\n/**\n * The subscription's status vocabulary (docs/billing-harmonization.md §3.2) — server-kit's\n * `SubscriptionStatus`, value for value:\n *\n * - `trialing` — the 30-day trial without a card (§2.7), or a pure guest's row whose trial\n * has not started (§12.9, `trial_ends_at` empty);\n * - `active` — paid;\n * - `past_due` — a payment failed; in good standing during the provider's retries (§3.3);\n * - `canceled` — the payer cancelled; the American spelling is the wire's, the word shown\n * is \"Cancelled\";\n * - `expired` — a trial, a grant or a failed subscription ran out: read-only (§2.3);\n * - `comped` — free by the operator's grant or as a beta user (§3.2, `comped_until`).\n */\nexport type SubscriptionStatus = \"trialing\" | \"active\" | \"past_due\" | \"canceled\" | \"expired\" | \"comped\";\n\n/** Every status, in the order a lifecycle reads: trial, paid, trouble, ended, granted. */\nexport const SUBSCRIPTION_STATUSES: readonly SubscriptionStatus[] = Object.freeze([\n \"trialing\",\n \"active\",\n \"past_due\",\n \"canceled\",\n \"expired\",\n \"comped\",\n]);\n\n/**\n * The words of the subscription parts — the `billing` namespace of `<UiKitProvider labels>`\n * (docs/billing-harmonization.md §7, §10; ui-kit 0.32.0).\n *\n * WORDING (§12.24): the English group is \"Subscription\", never \"Plan\" — keksdose's \"plan\"\n * is the month's budget plan (\"One plan per month\"), and two meanings of one word on one\n * settings page is the clash the review found. \"Plan\" stays the word for WHAT is bought\n * (Free, Pro), \"subscription\" for the standing arrangement. German says \"Tarif\" for the\n * plan and \"Abonnement\" for the subscription (§10).\n *\n * Prices are GROSS (§12.17): Swiss consumer law (PBV) wants a consumer price with the\n * MWST in it, so the cards say so (`vatIncluded`).\n */\nexport interface BillingLabels {\n /** The settings group or admin section the parts live on (§7, §12.24): \"Subscription\". */\n group: string;\n\n /* ── PlanPicker / PlanCard (§7) ── */\n /** The picker's name when it has no visible `legend`: \"Plans\". */\n plans: string;\n /** The name of the billing-period switch: \"Billing period\". */\n interval: string;\n /** The switch's two options — yearly first (§12.17). */\n yearly: string;\n monthly: string;\n /** The name of the currency switch: \"Currency\". */\n currency: string;\n /** After a price: \"per year\", \"per month\". */\n perYear: string;\n perMonth: string;\n /** Under a price (§12.17): \"VAT included\". German: \"inkl. MWST\". */\n vatIncluded: string;\n /** A plan with no price, or a price of 0: \"Free\". */\n free: string;\n /** A plan with prices, but none for this currency and period. */\n notOffered: string;\n /** A limit of `null` (§3.1): \"Unlimited\". */\n unlimited: string;\n /** The accessible names of a card's two lists. */\n limits: string;\n features: string;\n /** The chip on the payer's current plan. */\n current: string;\n /** The picker's button for `plan` (the plan's display name) with no current plan —\n * during a trial, or after a plan ended: \"Choose Pro\". */\n choose: (plan: string) => string;\n /** …for a plan after the current one in the catalogue's order: \"Upgrade to Pro\". */\n upgrade: (plan: string) => string;\n /** …for a plan before it: \"Switch to Free\". */\n downgrade: (plan: string) => string;\n /** Why the button is off while the current plan is the one picked. */\n isCurrent: string;\n /** Why it is off while nothing is picked. */\n pickFirst: string;\n\n /* ── SubscriptionStatusChip (§6) ── */\n /** One word per status. */\n status: Record<SubscriptionStatus, string>;\n\n /* ── BillingBanner (§3.3, §12.6, §12.21) ── */\n /** A trial ends in `days` calendar days — 0 is today, 1 tomorrow. */\n trialEnding: (days: number) => string;\n /** A free grant (`comped_until`: a beta user's 12 months, an operator's grant) ends in\n * `days` calendar days. */\n grantEnding: (days: number) => string;\n /** The first failed payment (`past_due`), while the provider retries. */\n paymentFailed: string;\n /** Out of good standing: read-only, never locked (§2.3). */\n planEnded: string;\n /**\n * A guest in someone else's item that is read-only for billing (§12.6). It never says\n * WHY beyond \"for now\": the owner's payment status is the owner's personal data.\n * `item` is the item's own name (\"Household 2026\"), shown in quotes — or `undefined`.\n */\n guestReadOnly: (item?: string) => string;\n /** Back from checkout before the provider's webhook landed (§12.21). */\n processing: string;\n /** The action of the trial, grant and plan-ended banners, and of `PlanLimitNotice`'s\n * `upgrade` mode: \"Choose a plan\". */\n choosePlan: string;\n /** The payment-failed banner's action, to the provider's portal: \"Update payment method\". */\n updatePayment: string;\n /**\n * 0.42 (§16.7 A, §16.8): the payment-failed banner for an admin who isn't the buyer\n * (`portal: \"buyer_only\"`), with no button — only the buyer can change the card.\n * `buyer` is their display name, or `undefined` when they are gone (\"the admin who\n * bought the subscription\").\n */\n paymentFailedAskBuyer: (buyer?: string) => string;\n /** The processing banner's action, when the app gives one: \"Check again\". */\n checkAgain: string;\n\n /* ── The read-only lock (§3.3, §12.6) ── */\n /** The `WriteLockProvider` reason for the payer (§3.3, verbatim). A guest's reason is\n * {@link guestReadOnly}. */\n lockReason: string;\n\n /* ── PlanLimitNotice (§3.4) ── */\n /** The notice's first line: \"Plan limit reached\". */\n limitReached: string;\n /** Mode `upgrade`: what the action does. */\n limitUpgrade: string;\n /** Mode `contact` (the operator grants plans by hand, keksdose's today). */\n limitContact: string;\n /** Mode `contact`'s action, a mail to support: \"Ask for more\". */\n askForMore: string;\n /** The meter's value: \"3 of 3\". Both figures are formatted already. */\n usage: (used: string, limit: string) => string;\n /** The subject of the `contact` mail; `dimension` is the app's name for it (\"Budgets\"). */\n contactSubject: (dimension: string) => string;\n /**\n * The figure line of the limit's toast (`usePlanLimitToast`, §14.11) and of an\n * operator's plan change over the new limit (§14.12): \"Budgets: 3 of 3\". A key of its\n * own rather than `${dimension}: ${usage}`, because French puts a space before the\n * colon. Both figures are formatted already.\n */\n limitUsageLine: (dimension: string, used: string, limit: string) => string;\n\n /* ── The provider's portal (§7, §12.26) ── */\n /** The portal: payment method and invoices (§2.8, the provider hosts both). */\n manage: string;\n /**\n * The visible cancel link on the app's subscription page (§12.26). German: §312k BGB\n * wants the button that leads to the cancellation labelled \"Verträge hier kündigen\" or\n * as unambiguously — a translator should use exactly that.\n */\n cancel: string;\n /**\n * In place of both while the payer hasn't reached the provider (the overview's\n * `at_provider` is false, §14.5): there is no customer to open a portal for yet. Also\n * the words for `409 billing_not_at_provider`, a stale page that asked anyway.\n */\n notAtProvider: string;\n\n /* ── The portal for the buyer only, the in-app cancel (0.42, §16) ── */\n /**\n * In place of \"Payment and invoices\" for an admin who isn't the buyer (`portal:\n * \"buyer_only\"`, §16.2 decision 3): the provider's portal opens only for the buyer.\n * `buyer` is their display name — or `undefined` when they are gone or have left the\n * company, and the words then say \"the admin who bought the subscription\".\n */\n buyerHolds: (buyer?: string) => string;\n /** `CancelSubscriptionConfirm`'s title: \"Cancel subscription\". */\n cancelConfirmTitle: string;\n /** Its body; `date` is the period's end, formatted: \"Your subscription ends on\n * 9 Nov 2026. Until then everything works as before.\" */\n cancelConfirmBody: (date: string) => string;\n /** Its confirm button (§16.4): \"Cancel subscription now\". German: \"Jetzt kündigen\",\n * the confirmation button's words in §312k BGB. */\n cancelConfirmAction: string;\n /** In place of \"Cancel subscription\" while a cancel is scheduled (§16.7 C, `POST\n * /billing/resume`): \"Keep subscription\". */\n keepSubscription: string;\n /** 0.43 (§17.3): the muted note under the actions while the portal opens in a new tab —\n * `provider` the provider's name (\"Paddle\"), `app` the app's; without the app, \"this\n * page\". \"Payment and invoices open at Paddle in a new tab; Kastlan stays open here.\" */\n portalOpensNewTab: (provider: string, app?: string) => string;\n\n /* ── The pay page (§14.4) ── */\n /* Built into the static page `dist/pay/` at the kit's build: it has no React and no\n provider, so an app's override never reaches it. */\n /** While the checkout opens. */\n payOpening: string;\n /** A link without a transaction, or billing switched off: nothing to pay. */\n payNothing: string;\n /** Paddle's script didn't load, or the page's configuration is wrong. */\n payFailed: string;\n /** The buyer closed the checkout without paying. */\n payBack: string;\n /** 0.39 (§15.2.5): paid, for a transaction that names no app the page knows (a\n * payment-method update from Paddle's e-mail) — no way back to offer. */\n payDone: string;\n\n /* ── PlanSwitchConfirm (0.39, §15.2.10) ── */\n /** The dialog's title: \"Change plan\". */\n switchTitle: string;\n /** The line naming both plans, each with its billing period: \"From Pro (Monthly) to\n * Business (Yearly)\". */\n switchFromTo: (from: string, to: string) => string;\n /** What an upgrade charges at once (Paddle's preview, prorated): \"Charged now\". */\n switchChargedNow: string;\n /** What a downgrade credits — the customer's balance, which Paddle takes off the next\n * charges: \"Credited towards your next charges\". */\n switchCredited: string;\n /** The next regular charge; `date` is formatted: \"Next charge on 9 Nov 2026\". */\n switchNextCharge: (date: string) => string;\n /** The confirm button: \"Change plan\". */\n switchConfirm: string;\n\n /* ── The permanent-grant warning (0.42, §16.2 decision 5, §16.8 B) ── */\n /** The warning's title, before a checkout (`PlanPicker`) or a switch\n * (`PlanSwitchConfirm`) under a grant without an end: \"Your free access continues\". */\n grantWarningTitle: string;\n /** What the purchase does under it: nothing to the plan while the grant runs. */\n grantWarningBody: string;\n /** `PlanPicker`'s button that goes on to the checkout: \"Buy anyway\". */\n grantWarningAction: string;\n\n /* ── Offline (§12.5) ── */\n /** Changes refused as `billing_read_only` inside a sync reply, kept queued and sent\n * after payment: \"3 changes waiting for a plan\". */\n waitingChanges: (count: number) => string;\n /** The sign-out guard's way out for someone who will not pay. */\n discardWaiting: string;\n\n /* ── Refusals with no part of their own (0.32.1, kastlan's 0.32 report) ── */\n /**\n * `billing_not_configured`: billing is on but the provider's settings are missing — the\n * operator's mistake, so the words ask the payer to wait, not to act. For the toast or\n * alert after \"Choose a plan\" or \"Payment and invoices\" (`isBillingError(err,\n * \"billing_not_configured\")`). `billing_read_only` and `plan_limit` need none: the\n * write lock, the banner and `PlanLimitNotice` say them.\n */\n notConfigured: string;\n /** `billing_disabled`: billing is switched off on this server (§2.9), for a page or a\n * link that reached it anyway. */\n disabled: string;\n /* 0.33.0 — server-kit 0.7's refusals (§14.2). `billing_not_at_provider` is\n {@link notAtProvider}'s: a stale page that asked for the portal anyway. */\n /** `billing_provider_unavailable` (502): the provider didn't answer. Try again. */\n providerUnavailable: string;\n /** `billing_already_subscribed` (409): a second checkout while a subscription runs\n * (§14.6). Since 0.39 plan changes go through the app's own switch (§15.2.10), not the\n * portal, and the words say so. */\n alreadySubscribed: string;\n /** `billing_plan_not_sold` (422): the catalogue doesn't sell this plan with that\n * period or currency — at a checkout, or (0.39) a switch to a plan no longer sold. */\n planNotSold: string;\n /** 0.39 (§15.2.10): `billing_not_subscribed` (409), a plan switch with no running\n * provider subscription — the page offers the checkout instead. */\n notSubscribed: string;\n /** 0.39: `billing_plan_unchanged` (409), a switch to the price the payer pays now. */\n planUnchanged: string;\n}\n\n/** \"today\", \"tomorrow\", \"in 5 days\". */\nfunction inDays(days: number): string {\n if (days <= 0) return \"today\";\n if (days === 1) return \"tomorrow\";\n return `in ${days} days`;\n}\n\nexport const DEFAULT_BILLING_LABELS: BillingLabels = {\n group: \"Subscription\",\n\n plans: \"Plans\",\n interval: \"Billing period\",\n yearly: \"Yearly\",\n monthly: \"Monthly\",\n currency: \"Currency\",\n perYear: \"per year\",\n perMonth: \"per month\",\n vatIncluded: \"VAT included\",\n free: \"Free\",\n notOffered: \"Not offered for this billing period\",\n unlimited: \"Unlimited\",\n limits: \"Limits\",\n features: \"Included\",\n current: \"Current plan\",\n choose: (plan) => `Choose ${plan}`,\n upgrade: (plan) => `Upgrade to ${plan}`,\n downgrade: (plan) => `Switch to ${plan}`,\n isCurrent: \"This is your current plan.\",\n pickFirst: \"Pick a plan first.\",\n\n status: {\n trialing: \"Trial\",\n active: \"Active\",\n past_due: \"Payment overdue\",\n canceled: \"Cancelled\",\n expired: \"Expired\",\n comped: \"Complimentary\",\n },\n\n trialEnding: (days) =>\n `Your trial ends ${inDays(days)}. After that you can still view everything, but changes need a plan.`,\n grantEnding: (days) =>\n `Your free access ends ${inDays(days)}. After that you can still view everything, but changes need a plan.`,\n paymentFailed: \"Your last payment didn’t go through. Update your payment method to keep your plan.\",\n planEnded: \"Your plan has ended. You can still view and export everything; choose a plan to make changes again.\",\n guestReadOnly: (item) =>\n item\n ? `“${item}” is read-only for now; its owner can lift that.`\n : \"This is read-only for now; its owner can lift that.\",\n processing:\n \"Your payment is being processed. Your plan starts as soon as it’s confirmed; this page updates by itself.\",\n choosePlan: \"Choose a plan\",\n updatePayment: \"Update payment method\",\n paymentFailedAskBuyer: (buyer) =>\n buyer\n ? `The payment failed. ${buyer} can update the payment method.`\n : \"The payment failed. The admin who bought the subscription can update the payment method.\",\n checkAgain: \"Check again\",\n\n lockReason: \"Your plan has ended. Choose a plan to make changes again.\",\n\n limitReached: \"Plan limit reached\",\n limitUpgrade: \"To add more, choose a plan with a higher limit.\",\n limitContact: \"To add more, ask us for a higher limit.\",\n askForMore: \"Ask for more\",\n usage: (used, limit) => `${used} of ${limit}`,\n contactSubject: (dimension) => `Plan limit: ${dimension}`,\n limitUsageLine: (dimension, used, limit) => `${dimension}: ${used} of ${limit}`,\n\n manage: \"Payment and invoices\",\n cancel: \"Cancel subscription\",\n notAtProvider: \"Nothing to manage yet. Payments and invoices appear here once you have a paid plan.\",\n buyerHolds: (buyer) =>\n buyer\n ? `The payment method and the invoices are with ${buyer}, who bought the subscription.`\n : \"The payment method and the invoices are with the admin who bought the subscription.\",\n cancelConfirmTitle: \"Cancel subscription\",\n cancelConfirmBody: (date) => `Your subscription ends on ${date}. Until then everything works as before.`,\n cancelConfirmAction: \"Cancel subscription now\",\n keepSubscription: \"Keep subscription\",\n portalOpensNewTab: (provider, app) =>\n `Payment and invoices open at ${provider} in a new tab; ${app ? `${app} stays open here` : \"this page stays open\"}.`,\n\n payOpening: \"Opening the secure checkout…\",\n payNothing: \"There’s no payment to make here. To choose a plan, go back to the app.\",\n payFailed: \"The checkout couldn’t be opened. Please go back and try again in a moment.\",\n payBack: \"The checkout was closed. You can go back, or reload this page to open it again.\",\n payDone: \"Payment complete. You can close this page.\",\n\n switchTitle: \"Change plan\",\n switchFromTo: (from, to) => `From ${from} to ${to}`,\n switchChargedNow: \"Charged now\",\n switchCredited: \"Credited towards your next charges\",\n switchNextCharge: (date) => `Next charge on ${date}`,\n switchConfirm: \"Change plan\",\n\n grantWarningTitle: \"Your free access continues\",\n grantWarningBody:\n \"You have free access with no end date. A purchase won’t change your plan while the free access runs: you would pay for a plan that doesn’t apply.\",\n grantWarningAction: \"Buy anyway\",\n\n waitingChanges: (count) => (count === 1 ? \"1 change waiting for a plan\" : `${count} changes waiting for a plan`),\n discardWaiting: \"Discard waiting changes\",\n\n notConfigured: \"Payments aren't set up yet. Please try again later.\",\n disabled: \"Subscriptions aren't available here.\",\n providerUnavailable: \"The payment provider can’t be reached right now. Please try again in a moment.\",\n alreadySubscribed: \"You already have a subscription. Change your plan above.\",\n planNotSold: \"This plan isn’t offered for that billing period or currency. Please choose another.\",\n notSubscribed: \"There’s no running subscription to change. Choose a plan to subscribe.\",\n planUnchanged: \"That’s already your plan. Pick another plan or billing period.\",\n};\n\n/** The `billing` namespace, resolved: English, then the provider, then `labels` — which\n * may be partial at every depth (`{ status: { comped: \"Beta\" } }`). */\nexport function useBillingLabels(labels?: LabelOverride<BillingLabels>): BillingLabels {\n return useKitLabels(\"billing\", DEFAULT_BILLING_LABELS, labels);\n}\n"],"mappings":"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","names":[]}
|