@entropy-softworks/ui 2026.7.31
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/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/lib/components/auth/LockOverlay.d.ts +22 -0
- package/lib/components/auth/LockOverlay.d.ts.map +1 -0
- package/lib/components/auth/LockOverlay.jsx +82 -0
- package/lib/components/auth/LockOverlay.jsx.map +1 -0
- package/lib/components/auth/PasswordStrengthMeter.d.ts +12 -0
- package/lib/components/auth/PasswordStrengthMeter.d.ts.map +1 -0
- package/lib/components/auth/PasswordStrengthMeter.jsx +81 -0
- package/lib/components/auth/PasswordStrengthMeter.jsx.map +1 -0
- package/lib/components/auth/ProtectedRoute.d.ts +24 -0
- package/lib/components/auth/ProtectedRoute.d.ts.map +1 -0
- package/lib/components/auth/ProtectedRoute.jsx +56 -0
- package/lib/components/auth/ProtectedRoute.jsx.map +1 -0
- package/lib/components/auth/PwnedPasswordWarning.d.ts +13 -0
- package/lib/components/auth/PwnedPasswordWarning.d.ts.map +1 -0
- package/lib/components/auth/PwnedPasswordWarning.jsx +29 -0
- package/lib/components/auth/PwnedPasswordWarning.jsx.map +1 -0
- package/lib/components/auth/index.d.ts +5 -0
- package/lib/components/auth/index.d.ts.map +1 -0
- package/lib/components/auth/index.js +5 -0
- package/lib/components/auth/index.js.map +1 -0
- package/lib/components/bottom-nav.d.ts +25 -0
- package/lib/components/bottom-nav.d.ts.map +1 -0
- package/lib/components/bottom-nav.jsx +51 -0
- package/lib/components/bottom-nav.jsx.map +1 -0
- package/lib/components/button.d.ts +16 -0
- package/lib/components/button.d.ts.map +1 -0
- package/lib/components/button.jsx +73 -0
- package/lib/components/button.jsx.map +1 -0
- package/lib/components/card.d.ts +22 -0
- package/lib/components/card.d.ts.map +1 -0
- package/lib/components/card.jsx +36 -0
- package/lib/components/card.jsx.map +1 -0
- package/lib/components/checkbox.d.ts +12 -0
- package/lib/components/checkbox.d.ts.map +1 -0
- package/lib/components/checkbox.jsx +16 -0
- package/lib/components/checkbox.jsx.map +1 -0
- package/lib/components/connection-settings.d.ts +41 -0
- package/lib/components/connection-settings.d.ts.map +1 -0
- package/lib/components/connection-settings.jsx +354 -0
- package/lib/components/connection-settings.jsx.map +1 -0
- package/lib/components/context-menu.d.ts +52 -0
- package/lib/components/context-menu.d.ts.map +1 -0
- package/lib/components/context-menu.jsx +24 -0
- package/lib/components/context-menu.jsx.map +1 -0
- package/lib/components/data-table.d.ts +59 -0
- package/lib/components/data-table.d.ts.map +1 -0
- package/lib/components/data-table.jsx +101 -0
- package/lib/components/data-table.jsx.map +1 -0
- package/lib/components/dialog.d.ts +58 -0
- package/lib/components/dialog.d.ts.map +1 -0
- package/lib/components/dialog.jsx +197 -0
- package/lib/components/dialog.jsx.map +1 -0
- package/lib/components/dropdown-menu.d.ts +55 -0
- package/lib/components/dropdown-menu.d.ts.map +1 -0
- package/lib/components/dropdown-menu.jsx +38 -0
- package/lib/components/dropdown-menu.jsx.map +1 -0
- package/lib/components/entropy-boot-splash.d.ts +24 -0
- package/lib/components/entropy-boot-splash.d.ts.map +1 -0
- package/lib/components/entropy-boot-splash.jsx +83 -0
- package/lib/components/entropy-boot-splash.jsx.map +1 -0
- package/lib/components/entropy-mark-animated.d.ts +20 -0
- package/lib/components/entropy-mark-animated.d.ts.map +1 -0
- package/lib/components/entropy-mark-animated.jsx +148 -0
- package/lib/components/entropy-mark-animated.jsx.map +1 -0
- package/lib/components/entropy-mark-geometry.d.ts +8 -0
- package/lib/components/entropy-mark-geometry.d.ts.map +1 -0
- package/lib/components/entropy-mark-geometry.js +10 -0
- package/lib/components/entropy-mark-geometry.js.map +1 -0
- package/lib/components/entropy-mark-sequential.d.ts +12 -0
- package/lib/components/entropy-mark-sequential.d.ts.map +1 -0
- package/lib/components/entropy-mark-sequential.jsx +53 -0
- package/lib/components/entropy-mark-sequential.jsx.map +1 -0
- package/lib/components/entropy-mark.d.ts +29 -0
- package/lib/components/entropy-mark.d.ts.map +1 -0
- package/lib/components/entropy-mark.jsx +66 -0
- package/lib/components/entropy-mark.jsx.map +1 -0
- package/lib/components/error-boundary.d.ts +31 -0
- package/lib/components/error-boundary.d.ts.map +1 -0
- package/lib/components/error-boundary.jsx +40 -0
- package/lib/components/error-boundary.jsx.map +1 -0
- package/lib/components/index.d.ts +41 -0
- package/lib/components/index.d.ts.map +1 -0
- package/lib/components/index.js +43 -0
- package/lib/components/index.js.map +1 -0
- package/lib/components/input.d.ts +26 -0
- package/lib/components/input.d.ts.map +1 -0
- package/lib/components/input.jsx +95 -0
- package/lib/components/input.jsx.map +1 -0
- package/lib/components/item-icon.d.ts +32 -0
- package/lib/components/item-icon.d.ts.map +1 -0
- package/lib/components/item-icon.jsx +54 -0
- package/lib/components/item-icon.jsx.map +1 -0
- package/lib/components/label.d.ts +13 -0
- package/lib/components/label.d.ts.map +1 -0
- package/lib/components/label.jsx +9 -0
- package/lib/components/label.jsx.map +1 -0
- package/lib/components/legal-dialog.d.ts +18 -0
- package/lib/components/legal-dialog.d.ts.map +1 -0
- package/lib/components/legal-dialog.jsx +31 -0
- package/lib/components/legal-dialog.jsx.map +1 -0
- package/lib/components/license-gate.d.ts +17 -0
- package/lib/components/license-gate.d.ts.map +1 -0
- package/lib/components/license-gate.jsx +21 -0
- package/lib/components/license-gate.jsx.map +1 -0
- package/lib/components/logo.d.ts +15 -0
- package/lib/components/logo.d.ts.map +1 -0
- package/lib/components/logo.jsx +20 -0
- package/lib/components/logo.jsx.map +1 -0
- package/lib/components/onboarding-wizard.d.ts +89 -0
- package/lib/components/onboarding-wizard.d.ts.map +1 -0
- package/lib/components/onboarding-wizard.jsx +185 -0
- package/lib/components/onboarding-wizard.jsx.map +1 -0
- package/lib/components/pagination.d.ts +43 -0
- package/lib/components/pagination.d.ts.map +1 -0
- package/lib/components/pagination.jsx +72 -0
- package/lib/components/pagination.jsx.map +1 -0
- package/lib/components/password-generator.d.ts +15 -0
- package/lib/components/password-generator.d.ts.map +1 -0
- package/lib/components/password-generator.jsx +273 -0
- package/lib/components/password-generator.jsx.map +1 -0
- package/lib/components/pin-pad.d.ts +20 -0
- package/lib/components/pin-pad.d.ts.map +1 -0
- package/lib/components/pin-pad.jsx +93 -0
- package/lib/components/pin-pad.jsx.map +1 -0
- package/lib/components/popover.d.ts +27 -0
- package/lib/components/popover.d.ts.map +1 -0
- package/lib/components/popover.jsx +18 -0
- package/lib/components/popover.jsx.map +1 -0
- package/lib/components/profile/ProfileDialog.d.ts +25 -0
- package/lib/components/profile/ProfileDialog.d.ts.map +1 -0
- package/lib/components/profile/ProfileDialog.jsx +71 -0
- package/lib/components/profile/ProfileDialog.jsx.map +1 -0
- package/lib/components/profile/ProfileTab.d.ts +12 -0
- package/lib/components/profile/ProfileTab.d.ts.map +1 -0
- package/lib/components/profile/ProfileTab.jsx +112 -0
- package/lib/components/profile/ProfileTab.jsx.map +1 -0
- package/lib/components/profile/SecurityTab.d.ts +32 -0
- package/lib/components/profile/SecurityTab.d.ts.map +1 -0
- package/lib/components/profile/SecurityTab.jsx +145 -0
- package/lib/components/profile/SecurityTab.jsx.map +1 -0
- package/lib/components/profile/index.d.ts +4 -0
- package/lib/components/profile/index.d.ts.map +1 -0
- package/lib/components/profile/index.js +4 -0
- package/lib/components/profile/index.js.map +1 -0
- package/lib/components/segmented-toggle.d.ts +28 -0
- package/lib/components/segmented-toggle.d.ts.map +1 -0
- package/lib/components/segmented-toggle.jsx +64 -0
- package/lib/components/segmented-toggle.jsx.map +1 -0
- package/lib/components/separator.d.ts +13 -0
- package/lib/components/separator.d.ts.map +1 -0
- package/lib/components/separator.jsx +16 -0
- package/lib/components/separator.jsx.map +1 -0
- package/lib/components/server-settings.d.ts +27 -0
- package/lib/components/server-settings.d.ts.map +1 -0
- package/lib/components/server-settings.jsx +307 -0
- package/lib/components/server-settings.jsx.map +1 -0
- package/lib/components/settings/AccountSection.d.ts +26 -0
- package/lib/components/settings/AccountSection.d.ts.map +1 -0
- package/lib/components/settings/AccountSection.jsx +32 -0
- package/lib/components/settings/AccountSection.jsx.map +1 -0
- package/lib/components/settings/DangerZoneSection.d.ts +13 -0
- package/lib/components/settings/DangerZoneSection.d.ts.map +1 -0
- package/lib/components/settings/DangerZoneSection.jsx +16 -0
- package/lib/components/settings/DangerZoneSection.jsx.map +1 -0
- package/lib/components/settings/GeneralSection.d.ts +11 -0
- package/lib/components/settings/GeneralSection.d.ts.map +1 -0
- package/lib/components/settings/GeneralSection.jsx +12 -0
- package/lib/components/settings/GeneralSection.jsx.map +1 -0
- package/lib/components/settings/SupportSection.d.ts +11 -0
- package/lib/components/settings/SupportSection.d.ts.map +1 -0
- package/lib/components/settings/SupportSection.jsx +22 -0
- package/lib/components/settings/SupportSection.jsx.map +1 -0
- package/lib/components/settings/index.d.ts +5 -0
- package/lib/components/settings/index.d.ts.map +1 -0
- package/lib/components/settings/index.js +5 -0
- package/lib/components/settings/index.js.map +1 -0
- package/lib/components/settings-row.d.ts +28 -0
- package/lib/components/settings-row.d.ts.map +1 -0
- package/lib/components/settings-row.jsx +68 -0
- package/lib/components/settings-row.jsx.map +1 -0
- package/lib/components/skeleton.d.ts +8 -0
- package/lib/components/skeleton.d.ts.map +1 -0
- package/lib/components/skeleton.jsx +24 -0
- package/lib/components/skeleton.jsx.map +1 -0
- package/lib/components/social-connections.d.ts +15 -0
- package/lib/components/social-connections.d.ts.map +1 -0
- package/lib/components/social-connections.jsx +31 -0
- package/lib/components/social-connections.jsx.map +1 -0
- package/lib/components/stat-card.d.ts +29 -0
- package/lib/components/stat-card.d.ts.map +1 -0
- package/lib/components/stat-card.jsx +36 -0
- package/lib/components/stat-card.jsx.map +1 -0
- package/lib/components/tag-chip.d.ts +18 -0
- package/lib/components/tag-chip.d.ts.map +1 -0
- package/lib/components/tag-chip.jsx +54 -0
- package/lib/components/tag-chip.jsx.map +1 -0
- package/lib/components/tag-multi-select.d.ts +27 -0
- package/lib/components/tag-multi-select.d.ts.map +1 -0
- package/lib/components/tag-multi-select.jsx +65 -0
- package/lib/components/tag-multi-select.jsx.map +1 -0
- package/lib/components/text.d.ts +22 -0
- package/lib/components/text.d.ts.map +1 -0
- package/lib/components/text.jsx +43 -0
- package/lib/components/text.jsx.map +1 -0
- package/lib/components/toast/ToastPortal.d.ts +11 -0
- package/lib/components/toast/ToastPortal.d.ts.map +1 -0
- package/lib/components/toast/ToastPortal.jsx +15 -0
- package/lib/components/toast/ToastPortal.jsx.map +1 -0
- package/lib/components/toast/ToastPortal.web.d.ts +14 -0
- package/lib/components/toast/ToastPortal.web.d.ts.map +1 -0
- package/lib/components/toast/ToastPortal.web.jsx +39 -0
- package/lib/components/toast/ToastPortal.web.jsx.map +1 -0
- package/lib/components/toast/index.d.ts +3 -0
- package/lib/components/toast/index.d.ts.map +1 -0
- package/lib/components/toast/index.js +3 -0
- package/lib/components/toast/index.js.map +1 -0
- package/lib/components/toast/toast-config.d.ts +14 -0
- package/lib/components/toast/toast-config.d.ts.map +1 -0
- package/lib/components/toast/toast-config.jsx +24 -0
- package/lib/components/toast/toast-config.jsx.map +1 -0
- package/lib/components/tooltip.d.ts +30 -0
- package/lib/components/tooltip.d.ts.map +1 -0
- package/lib/components/tooltip.jsx +14 -0
- package/lib/components/tooltip.jsx.map +1 -0
- package/lib/components/totp-display.d.ts +23 -0
- package/lib/components/totp-display.d.ts.map +1 -0
- package/lib/components/totp-display.jsx +132 -0
- package/lib/components/totp-display.jsx.map +1 -0
- package/lib/components/user-avatar.d.ts +18 -0
- package/lib/components/user-avatar.d.ts.map +1 -0
- package/lib/components/user-avatar.jsx +25 -0
- package/lib/components/user-avatar.jsx.map +1 -0
- package/lib/context/AppConfigContext.d.ts +56 -0
- package/lib/context/AppConfigContext.d.ts.map +1 -0
- package/lib/context/AppConfigContext.jsx +14 -0
- package/lib/context/AppConfigContext.jsx.map +1 -0
- package/lib/context/NavigationContext.d.ts +42 -0
- package/lib/context/NavigationContext.d.ts.map +1 -0
- package/lib/context/NavigationContext.jsx +53 -0
- package/lib/context/NavigationContext.jsx.map +1 -0
- package/lib/context/NotificationContext.d.ts +32 -0
- package/lib/context/NotificationContext.d.ts.map +1 -0
- package/lib/context/NotificationContext.jsx +49 -0
- package/lib/context/NotificationContext.jsx.map +1 -0
- package/lib/context/ThemeContext.d.ts +12 -0
- package/lib/context/ThemeContext.d.ts.map +1 -0
- package/lib/context/ThemeContext.jsx +66 -0
- package/lib/context/ThemeContext.jsx.map +1 -0
- package/lib/context/index.d.ts +5 -0
- package/lib/context/index.d.ts.map +1 -0
- package/lib/context/index.js +5 -0
- package/lib/context/index.js.map +1 -0
- package/lib/flags.d.ts +9 -0
- package/lib/flags.d.ts.map +1 -0
- package/lib/flags.js +37 -0
- package/lib/flags.js.map +1 -0
- package/lib/forms/form-error.d.ts +12 -0
- package/lib/forms/form-error.d.ts.map +1 -0
- package/lib/forms/form-error.jsx +14 -0
- package/lib/forms/form-error.jsx.map +1 -0
- package/lib/forms/index.d.ts +3 -0
- package/lib/forms/index.d.ts.map +1 -0
- package/lib/forms/index.js +3 -0
- package/lib/forms/index.js.map +1 -0
- package/lib/forms/shake-field.d.ts +18 -0
- package/lib/forms/shake-field.d.ts.map +1 -0
- package/lib/forms/shake-field.jsx +30 -0
- package/lib/forms/shake-field.jsx.map +1 -0
- package/lib/hooks/index.d.ts +6 -0
- package/lib/hooks/index.d.ts.map +1 -0
- package/lib/hooks/index.js +6 -0
- package/lib/hooks/index.js.map +1 -0
- package/lib/hooks/useAuth.d.ts +109 -0
- package/lib/hooks/useAuth.d.ts.map +1 -0
- package/lib/hooks/useAuth.jsx +404 -0
- package/lib/hooks/useAuth.jsx.map +1 -0
- package/lib/hooks/useAutoLock.d.ts +22 -0
- package/lib/hooks/useAutoLock.d.ts.map +1 -0
- package/lib/hooks/useAutoLock.js +85 -0
- package/lib/hooks/useAutoLock.js.map +1 -0
- package/lib/hooks/useDebouncedValue.d.ts +19 -0
- package/lib/hooks/useDebouncedValue.d.ts.map +1 -0
- package/lib/hooks/useDebouncedValue.js +29 -0
- package/lib/hooks/useDebouncedValue.js.map +1 -0
- package/lib/hooks/useShakeAnimation.d.ts +17 -0
- package/lib/hooks/useShakeAnimation.d.ts.map +1 -0
- package/lib/hooks/useShakeAnimation.js +22 -0
- package/lib/hooks/useShakeAnimation.js.map +1 -0
- package/lib/hooks/useUploadBlobUrl.d.ts +13 -0
- package/lib/hooks/useUploadBlobUrl.d.ts.map +1 -0
- package/lib/hooks/useUploadBlobUrl.js +60 -0
- package/lib/hooks/useUploadBlobUrl.js.map +1 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +47 -0
- package/lib/index.js.map +1 -0
- package/lib/screens/auth/AccountPickerScreen.d.ts +24 -0
- package/lib/screens/auth/AccountPickerScreen.d.ts.map +1 -0
- package/lib/screens/auth/AccountPickerScreen.jsx +47 -0
- package/lib/screens/auth/AccountPickerScreen.jsx.map +1 -0
- package/lib/screens/auth/AuthScreen.d.ts +59 -0
- package/lib/screens/auth/AuthScreen.d.ts.map +1 -0
- package/lib/screens/auth/AuthScreen.jsx +594 -0
- package/lib/screens/auth/AuthScreen.jsx.map +1 -0
- package/lib/screens/auth/ConsentScreen.d.ts +27 -0
- package/lib/screens/auth/ConsentScreen.d.ts.map +1 -0
- package/lib/screens/auth/ConsentScreen.jsx +55 -0
- package/lib/screens/auth/ConsentScreen.jsx.map +1 -0
- package/lib/screens/auth/ErrorScreen.d.ts +20 -0
- package/lib/screens/auth/ErrorScreen.d.ts.map +1 -0
- package/lib/screens/auth/ErrorScreen.jsx +43 -0
- package/lib/screens/auth/ErrorScreen.jsx.map +1 -0
- package/lib/screens/auth/ExpiredLinkScreen.d.ts +19 -0
- package/lib/screens/auth/ExpiredLinkScreen.d.ts.map +1 -0
- package/lib/screens/auth/ExpiredLinkScreen.jsx +38 -0
- package/lib/screens/auth/ExpiredLinkScreen.jsx.map +1 -0
- package/lib/screens/auth/ForgotPasswordScreen.d.ts +4 -0
- package/lib/screens/auth/ForgotPasswordScreen.d.ts.map +1 -0
- package/lib/screens/auth/ForgotPasswordScreen.jsx +123 -0
- package/lib/screens/auth/ForgotPasswordScreen.jsx.map +1 -0
- package/lib/screens/auth/ImportDataScreen.d.ts +17 -0
- package/lib/screens/auth/ImportDataScreen.d.ts.map +1 -0
- package/lib/screens/auth/ImportDataScreen.jsx +73 -0
- package/lib/screens/auth/ImportDataScreen.jsx.map +1 -0
- package/lib/screens/auth/LoginScreen.d.ts +38 -0
- package/lib/screens/auth/LoginScreen.d.ts.map +1 -0
- package/lib/screens/auth/LoginScreen.jsx +326 -0
- package/lib/screens/auth/LoginScreen.jsx.map +1 -0
- package/lib/screens/auth/MfaChallengeScreen.d.ts +4 -0
- package/lib/screens/auth/MfaChallengeScreen.d.ts.map +1 -0
- package/lib/screens/auth/MfaChallengeScreen.jsx +139 -0
- package/lib/screens/auth/MfaChallengeScreen.jsx.map +1 -0
- package/lib/screens/auth/MfaEnrollScreen.d.ts +12 -0
- package/lib/screens/auth/MfaEnrollScreen.d.ts.map +1 -0
- package/lib/screens/auth/MfaEnrollScreen.jsx +267 -0
- package/lib/screens/auth/MfaEnrollScreen.jsx.map +1 -0
- package/lib/screens/auth/PasskeyEnrollScreen.d.ts +18 -0
- package/lib/screens/auth/PasskeyEnrollScreen.d.ts.map +1 -0
- package/lib/screens/auth/PasskeyEnrollScreen.jsx +51 -0
- package/lib/screens/auth/PasskeyEnrollScreen.jsx.map +1 -0
- package/lib/screens/auth/PrivacyPolicyScreen.d.ts +19 -0
- package/lib/screens/auth/PrivacyPolicyScreen.d.ts.map +1 -0
- package/lib/screens/auth/PrivacyPolicyScreen.jsx +143 -0
- package/lib/screens/auth/PrivacyPolicyScreen.jsx.map +1 -0
- package/lib/screens/auth/RegisterScreen.d.ts +14 -0
- package/lib/screens/auth/RegisterScreen.d.ts.map +1 -0
- package/lib/screens/auth/RegisterScreen.jsx +296 -0
- package/lib/screens/auth/RegisterScreen.jsx.map +1 -0
- package/lib/screens/auth/ResetPasswordScreen.d.ts +12 -0
- package/lib/screens/auth/ResetPasswordScreen.d.ts.map +1 -0
- package/lib/screens/auth/ResetPasswordScreen.jsx +276 -0
- package/lib/screens/auth/ResetPasswordScreen.jsx.map +1 -0
- package/lib/screens/auth/ServerConfigScreen.d.ts +18 -0
- package/lib/screens/auth/ServerConfigScreen.d.ts.map +1 -0
- package/lib/screens/auth/ServerConfigScreen.jsx +259 -0
- package/lib/screens/auth/ServerConfigScreen.jsx.map +1 -0
- package/lib/screens/auth/TermsOfServiceScreen.d.ts +30 -0
- package/lib/screens/auth/TermsOfServiceScreen.d.ts.map +1 -0
- package/lib/screens/auth/TermsOfServiceScreen.jsx +130 -0
- package/lib/screens/auth/TermsOfServiceScreen.jsx.map +1 -0
- package/lib/screens/auth/VerifyEmailScreen.d.ts +4 -0
- package/lib/screens/auth/VerifyEmailScreen.d.ts.map +1 -0
- package/lib/screens/auth/VerifyEmailScreen.jsx +249 -0
- package/lib/screens/auth/VerifyEmailScreen.jsx.map +1 -0
- package/lib/screens/auth/WelcomeScreen.d.ts +17 -0
- package/lib/screens/auth/WelcomeScreen.d.ts.map +1 -0
- package/lib/screens/auth/WelcomeScreen.jsx +58 -0
- package/lib/screens/auth/WelcomeScreen.jsx.map +1 -0
- package/lib/screens/auth/index.d.ts +19 -0
- package/lib/screens/auth/index.d.ts.map +1 -0
- package/lib/screens/auth/index.js +19 -0
- package/lib/screens/auth/index.js.map +1 -0
- package/lib/screens/index.d.ts +2 -0
- package/lib/screens/index.d.ts.map +1 -0
- package/lib/screens/index.js +2 -0
- package/lib/screens/index.js.map +1 -0
- package/lib/services/api-config.d.ts +87 -0
- package/lib/services/api-config.d.ts.map +1 -0
- package/lib/services/api-config.js +202 -0
- package/lib/services/api-config.js.map +1 -0
- package/lib/services/auth.service.d.ts +140 -0
- package/lib/services/auth.service.d.ts.map +1 -0
- package/lib/services/auth.service.js +376 -0
- package/lib/services/auth.service.js.map +1 -0
- package/lib/services/favicon.d.ts +46 -0
- package/lib/services/favicon.d.ts.map +1 -0
- package/lib/services/favicon.js +84 -0
- package/lib/services/favicon.js.map +1 -0
- package/lib/services/hibp-cache.d.ts +23 -0
- package/lib/services/hibp-cache.d.ts.map +1 -0
- package/lib/services/hibp-cache.js +56 -0
- package/lib/services/hibp-cache.js.map +1 -0
- package/lib/services/hibp.d.ts +28 -0
- package/lib/services/hibp.d.ts.map +1 -0
- package/lib/services/hibp.js +92 -0
- package/lib/services/hibp.js.map +1 -0
- package/lib/services/index.d.ts +8 -0
- package/lib/services/index.d.ts.map +1 -0
- package/lib/services/index.js +7 -0
- package/lib/services/index.js.map +1 -0
- package/lib/services/passkey.d.ts +10 -0
- package/lib/services/passkey.d.ts.map +1 -0
- package/lib/services/passkey.js +192 -0
- package/lib/services/passkey.js.map +1 -0
- package/lib/utils/api-fetch.d.ts +11 -0
- package/lib/utils/api-fetch.d.ts.map +1 -0
- package/lib/utils/api-fetch.js +52 -0
- package/lib/utils/api-fetch.js.map +1 -0
- package/lib/utils/clipboard.d.ts +31 -0
- package/lib/utils/clipboard.d.ts.map +1 -0
- package/lib/utils/clipboard.js +66 -0
- package/lib/utils/clipboard.js.map +1 -0
- package/lib/utils/cn.d.ts +9 -0
- package/lib/utils/cn.d.ts.map +1 -0
- package/lib/utils/cn.js +12 -0
- package/lib/utils/cn.js.map +1 -0
- package/lib/utils/debounce.d.ts +13 -0
- package/lib/utils/debounce.d.ts.map +1 -0
- package/lib/utils/debounce.js +28 -0
- package/lib/utils/debounce.js.map +1 -0
- package/lib/utils/error.d.ts +9 -0
- package/lib/utils/error.d.ts.map +1 -0
- package/lib/utils/error.js +11 -0
- package/lib/utils/error.js.map +1 -0
- package/lib/utils/fetch.d.ts +17 -0
- package/lib/utils/fetch.d.ts.map +1 -0
- package/lib/utils/fetch.js +29 -0
- package/lib/utils/fetch.js.map +1 -0
- package/lib/utils/http-error.d.ts +41 -0
- package/lib/utils/http-error.d.ts.map +1 -0
- package/lib/utils/http-error.js +63 -0
- package/lib/utils/http-error.js.map +1 -0
- package/lib/utils/index.d.ts +11 -0
- package/lib/utils/index.d.ts.map +1 -0
- package/lib/utils/index.js +11 -0
- package/lib/utils/index.js.map +1 -0
- package/lib/utils/palette.d.ts +46 -0
- package/lib/utils/palette.d.ts.map +1 -0
- package/lib/utils/palette.js +75 -0
- package/lib/utils/palette.js.map +1 -0
- package/lib/utils/password.d.ts +20 -0
- package/lib/utils/password.d.ts.map +1 -0
- package/lib/utils/password.js +51 -0
- package/lib/utils/password.js.map +1 -0
- package/lib/utils/settings-storage.d.ts +3 -0
- package/lib/utils/settings-storage.d.ts.map +1 -0
- package/lib/utils/settings-storage.js +62 -0
- package/lib/utils/settings-storage.js.map +1 -0
- package/lib/utils/validation.d.ts +29 -0
- package/lib/utils/validation.d.ts.map +1 -0
- package/lib/utils/validation.js +58 -0
- package/lib/utils/validation.js.map +1 -0
- package/package.json +204 -0
- package/src/components/__tests__/data-table.test.ts +94 -0
- package/src/components/__tests__/pagination.test.ts +73 -0
- package/src/components/__tests__/stat-card.test.ts +21 -0
- package/src/components/auth/LockOverlay.tsx +122 -0
- package/src/components/auth/PasswordStrengthMeter.tsx +96 -0
- package/src/components/auth/ProtectedRoute.tsx +91 -0
- package/src/components/auth/PwnedPasswordWarning.tsx +54 -0
- package/src/components/auth/index.ts +10 -0
- package/src/components/bottom-nav.tsx +99 -0
- package/src/components/button.tsx +144 -0
- package/src/components/card.tsx +82 -0
- package/src/components/checkbox.tsx +48 -0
- package/src/components/connection-settings.tsx +583 -0
- package/src/components/context-menu.tsx +95 -0
- package/src/components/data-table.tsx +223 -0
- package/src/components/dialog.tsx +376 -0
- package/src/components/dropdown-menu.tsx +120 -0
- package/src/components/entropy-boot-splash.tsx +114 -0
- package/src/components/entropy-mark-animated.tsx +260 -0
- package/src/components/entropy-mark-geometry.ts +11 -0
- package/src/components/entropy-mark-sequential.tsx +132 -0
- package/src/components/entropy-mark.tsx +135 -0
- package/src/components/error-boundary.tsx +70 -0
- package/src/components/index.ts +156 -0
- package/src/components/input.tsx +246 -0
- package/src/components/item-icon.tsx +111 -0
- package/src/components/label.tsx +25 -0
- package/src/components/legal-dialog.tsx +47 -0
- package/src/components/license-gate.tsx +28 -0
- package/src/components/logo.tsx +34 -0
- package/src/components/onboarding-wizard.tsx +353 -0
- package/src/components/pagination.tsx +154 -0
- package/src/components/password-generator.tsx +393 -0
- package/src/components/pin-pad.tsx +156 -0
- package/src/components/popover.tsx +38 -0
- package/src/components/profile/ProfileDialog.tsx +129 -0
- package/src/components/profile/ProfileTab.tsx +151 -0
- package/src/components/profile/SecurityTab.tsx +229 -0
- package/src/components/profile/index.ts +3 -0
- package/src/components/segmented-toggle.tsx +119 -0
- package/src/components/separator.tsx +41 -0
- package/src/components/server-settings.tsx +442 -0
- package/src/components/settings/AccountSection.tsx +76 -0
- package/src/components/settings/DangerZoneSection.tsx +37 -0
- package/src/components/settings/GeneralSection.tsx +39 -0
- package/src/components/settings/SupportSection.tsx +53 -0
- package/src/components/settings/index.ts +4 -0
- package/src/components/settings-row.tsx +126 -0
- package/src/components/skeleton.tsx +48 -0
- package/src/components/social-connections.tsx +55 -0
- package/src/components/stat-card.tsx +64 -0
- package/src/components/tag-chip.tsx +78 -0
- package/src/components/tag-multi-select.tsx +136 -0
- package/src/components/text.tsx +77 -0
- package/src/components/toast/ToastPortal.tsx +16 -0
- package/src/components/toast/ToastPortal.web.tsx +47 -0
- package/src/components/toast/index.ts +2 -0
- package/src/components/toast/toast-config.tsx +31 -0
- package/src/components/tooltip.tsx +30 -0
- package/src/components/totp-display.tsx +207 -0
- package/src/components/user-avatar.tsx +46 -0
- package/src/context/AppConfigContext.tsx +76 -0
- package/src/context/NavigationContext.tsx +75 -0
- package/src/context/NotificationContext.tsx +69 -0
- package/src/context/ThemeContext.tsx +83 -0
- package/src/context/__tests__/NavigationContext.test.tsx +53 -0
- package/src/context/index.ts +24 -0
- package/src/flags.ts +44 -0
- package/src/forms/form-error.tsx +20 -0
- package/src/forms/index.ts +2 -0
- package/src/forms/shake-field.tsx +52 -0
- package/src/hooks/__tests__/useUploadBlobUrl.test.tsx +77 -0
- package/src/hooks/index.ts +18 -0
- package/src/hooks/useAuth.tsx +567 -0
- package/src/hooks/useAutoLock.ts +110 -0
- package/src/hooks/useDebouncedValue.ts +32 -0
- package/src/hooks/useShakeAnimation.ts +42 -0
- package/src/hooks/useUploadBlobUrl.ts +75 -0
- package/src/index.ts +48 -0
- package/src/screens/auth/AccountPickerScreen.tsx +103 -0
- package/src/screens/auth/AuthScreen.tsx +941 -0
- package/src/screens/auth/ConsentScreen.tsx +118 -0
- package/src/screens/auth/ErrorScreen.tsx +96 -0
- package/src/screens/auth/ExpiredLinkScreen.tsx +87 -0
- package/src/screens/auth/ForgotPasswordScreen.tsx +182 -0
- package/src/screens/auth/ImportDataScreen.tsx +112 -0
- package/src/screens/auth/LoginScreen.tsx +484 -0
- package/src/screens/auth/MfaChallengeScreen.tsx +216 -0
- package/src/screens/auth/MfaEnrollScreen.tsx +388 -0
- package/src/screens/auth/PasskeyEnrollScreen.tsx +96 -0
- package/src/screens/auth/PrivacyPolicyScreen.tsx +249 -0
- package/src/screens/auth/RegisterScreen.tsx +430 -0
- package/src/screens/auth/ResetPasswordScreen.tsx +377 -0
- package/src/screens/auth/ServerConfigScreen.tsx +388 -0
- package/src/screens/auth/TermsOfServiceScreen.tsx +236 -0
- package/src/screens/auth/VerifyEmailScreen.tsx +310 -0
- package/src/screens/auth/WelcomeScreen.tsx +78 -0
- package/src/screens/auth/index.ts +36 -0
- package/src/screens/index.ts +1 -0
- package/src/services/__tests__/api-config.test.ts +67 -0
- package/src/services/__tests__/favicon.test.ts +88 -0
- package/src/services/__tests__/hibp-cache.test.ts +37 -0
- package/src/services/__tests__/hibp.test.ts +39 -0
- package/src/services/api-config.ts +260 -0
- package/src/services/auth.service.ts +498 -0
- package/src/services/favicon.ts +102 -0
- package/src/services/hibp-cache.ts +71 -0
- package/src/services/hibp.ts +120 -0
- package/src/services/index.ts +52 -0
- package/src/services/passkey.ts +213 -0
- package/src/types/external.d.ts +24 -0
- package/src/utils/__tests__/clipboard.test.ts +59 -0
- package/src/utils/__tests__/cn.test.ts +24 -0
- package/src/utils/__tests__/debounce.test.ts +42 -0
- package/src/utils/__tests__/error.test.ts +18 -0
- package/src/utils/__tests__/http-error.test.ts +33 -0
- package/src/utils/__tests__/palette.test.ts +22 -0
- package/src/utils/__tests__/password.test.ts +55 -0
- package/src/utils/__tests__/settings-storage.test.ts +15 -0
- package/src/utils/__tests__/validation.test.ts +52 -0
- package/src/utils/api-fetch.ts +61 -0
- package/src/utils/clipboard.ts +69 -0
- package/src/utils/cn.ts +12 -0
- package/src/utils/debounce.ts +33 -0
- package/src/utils/error.ts +10 -0
- package/src/utils/fetch.ts +33 -0
- package/src/utils/http-error.ts +81 -0
- package/src/utils/index.ts +23 -0
- package/src/utils/palette.ts +105 -0
- package/src/utils/password.ts +64 -0
- package/src/utils/settings-storage.ts +68 -0
- package/src/utils/validation.ts +71 -0
- package/styles/globals.css +262 -0
- package/tailwind-preset.js +163 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { ScrollView, View } from "react-native";
|
|
3
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
4
|
+
import { type Href } from "expo-router";
|
|
5
|
+
import { useNavigation } from "../../context/NavigationContext";
|
|
6
|
+
import { StatusBar } from "expo-status-bar";
|
|
7
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
8
|
+
import { useColorScheme } from "nativewind";
|
|
9
|
+
import * as Haptics from "expo-haptics";
|
|
10
|
+
import { Heading, MutedText, Text } from "../../components/text";
|
|
11
|
+
import { Button, IconButton } from "../../components/button";
|
|
12
|
+
import {
|
|
13
|
+
Card,
|
|
14
|
+
CardContent,
|
|
15
|
+
CardDescription,
|
|
16
|
+
CardFooter,
|
|
17
|
+
CardHeader,
|
|
18
|
+
CardTitle,
|
|
19
|
+
} from "../../components/card";
|
|
20
|
+
import { EmailInput, PasswordInput } from "../../components/input";
|
|
21
|
+
import { Dialog, DialogButton, DialogFooter } from "../../components/dialog";
|
|
22
|
+
import { SeparatorWithText } from "../../components/separator";
|
|
23
|
+
import { useAuth } from "../../hooks/useAuth";
|
|
24
|
+
import { ApiError } from "../../utils/http-error";
|
|
25
|
+
import { validateEmail } from "../../utils/validation";
|
|
26
|
+
import { useShakeAnimation } from "../../hooks/useShakeAnimation";
|
|
27
|
+
import ShakeField from "../../forms/shake-field";
|
|
28
|
+
import FormError from "../../forms/form-error";
|
|
29
|
+
import { useAppConfig } from "../../context/AppConfigContext";
|
|
30
|
+
|
|
31
|
+
const SCROLL_CONTENT_STYLE = { flexGrow: 1 } as const;
|
|
32
|
+
|
|
33
|
+
export interface LoginScreenProps {
|
|
34
|
+
/** Optional widget rendered top-right (vault wires its admin cog here). */
|
|
35
|
+
topAccessory?: React.ReactNode;
|
|
36
|
+
/**
|
|
37
|
+
* Optional social-auth block. When provided, the screen renders a
|
|
38
|
+
* separator above it.
|
|
39
|
+
*/
|
|
40
|
+
socialAuth?: React.ReactNode;
|
|
41
|
+
/** Master-password field placeholder. */
|
|
42
|
+
passwordPlaceholder?: string;
|
|
43
|
+
/** Master-password field label. */
|
|
44
|
+
passwordLabel?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Render the "Sign in with passkey" button + ceremony flow. Set to
|
|
47
|
+
* `true` only when the platform reports a usable WebAuthn implementation
|
|
48
|
+
* — pass the result of `isPasskeySupported()` or equivalent.
|
|
49
|
+
*
|
|
50
|
+
* The button drives `useAuth().loginWithPasskey`, which the consumer
|
|
51
|
+
* wires through `<AuthProvider loginWithPasskey={…}>`. A non-PRF
|
|
52
|
+
* outcome opens an inline unlock dialog and calls
|
|
53
|
+
* `useAuth().unlockWithPassword(password)` after submit.
|
|
54
|
+
*/
|
|
55
|
+
passkeyEnabled?: boolean;
|
|
56
|
+
/** Master-password placeholder shown inside the passkey unlock dialog. */
|
|
57
|
+
passkeyUnlockPlaceholder?: string;
|
|
58
|
+
/** Title shown inside the passkey unlock dialog. */
|
|
59
|
+
passkeyUnlockTitle?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Description rendered above the unlock input in the passkey dialog.
|
|
62
|
+
* Defaults to "Signed in successfully. This passkey can't unlock the
|
|
63
|
+
* vault on its own — enter your master password to decrypt your data."
|
|
64
|
+
*/
|
|
65
|
+
passkeyUnlockDescription?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function LoginScreen({
|
|
69
|
+
topAccessory,
|
|
70
|
+
socialAuth,
|
|
71
|
+
passwordPlaceholder = "Enter your password",
|
|
72
|
+
passwordLabel = "Password",
|
|
73
|
+
passkeyEnabled = false,
|
|
74
|
+
passkeyUnlockPlaceholder = "Enter your master password",
|
|
75
|
+
passkeyUnlockTitle = "Unlock your vault",
|
|
76
|
+
passkeyUnlockDescription = "Signed in successfully. This passkey can't unlock the vault on its own — enter your master password to decrypt your data.",
|
|
77
|
+
}: LoginScreenProps = {}) {
|
|
78
|
+
const { login: authLogin, loginWithPasskey, unlockWithPassword } = useAuth();
|
|
79
|
+
const router = useNavigation();
|
|
80
|
+
const { appName, Logo, routes } = useAppConfig();
|
|
81
|
+
|
|
82
|
+
const [email, setEmail] = useState("");
|
|
83
|
+
const [password, setPassword] = useState("");
|
|
84
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
85
|
+
const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
|
|
86
|
+
// Passkey ceremony state machine.
|
|
87
|
+
// "idle" — default; primary CTA is the email/password form.
|
|
88
|
+
// "passkey-loading" — passkey ceremony is in flight.
|
|
89
|
+
// "passkey-unlock" — server accepted the assertion but the
|
|
90
|
+
// authenticator did not honor PRF; collect the
|
|
91
|
+
// master password to complete local unlock.
|
|
92
|
+
const [passkeyStep, setPasskeyStep] = useState<"idle" | "passkey-loading" | "passkey-unlock">(
|
|
93
|
+
"idle"
|
|
94
|
+
);
|
|
95
|
+
const [unlockPassword, setUnlockPassword] = useState("");
|
|
96
|
+
const [unlockError, setUnlockError] = useState("");
|
|
97
|
+
const [pendingOnboardingStep, setPendingOnboardingStep] = useState<string | null>(null);
|
|
98
|
+
|
|
99
|
+
// Abort an in-flight passkey ceremony on unmount so the OS prompt
|
|
100
|
+
// tears down and the resolved credential doesn't land on a stale
|
|
101
|
+
// setState. Ref + cleanup so navigating away mid-ceremony is clean.
|
|
102
|
+
const passkeyAbortRef = useRef<AbortController | null>(null);
|
|
103
|
+
useEffect(
|
|
104
|
+
() => () => {
|
|
105
|
+
passkeyAbortRef.current?.abort();
|
|
106
|
+
},
|
|
107
|
+
[]
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const { colorScheme } = useColorScheme();
|
|
111
|
+
const isDark = colorScheme === "dark";
|
|
112
|
+
|
|
113
|
+
const emailShake = useShakeAnimation();
|
|
114
|
+
const passwordShake = useShakeAnimation();
|
|
115
|
+
const firstError = errors.email ?? errors.password;
|
|
116
|
+
|
|
117
|
+
const validateForm = () => {
|
|
118
|
+
const newErrors: typeof errors = {};
|
|
119
|
+
|
|
120
|
+
if (!email) {
|
|
121
|
+
newErrors.email = "Email is required";
|
|
122
|
+
} else if (!validateEmail(email)) {
|
|
123
|
+
newErrors.email = "Please enter a valid email";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!password) {
|
|
127
|
+
newErrors.password = "Password is required";
|
|
128
|
+
} else if (password.length < 8) {
|
|
129
|
+
newErrors.password = "Password must be at least 8 characters";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return newErrors;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const performLogin = async () => {
|
|
136
|
+
setIsLoading(true);
|
|
137
|
+
try {
|
|
138
|
+
await authLogin(email.trim().toLowerCase(), password);
|
|
139
|
+
setPassword("");
|
|
140
|
+
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
|
143
|
+
const errorMessage = error instanceof Error ? error.message : "Login failed";
|
|
144
|
+
// Prefer the server's machine-readable `error.code` over
|
|
145
|
+
// substring-matching the English message — i18n on the server
|
|
146
|
+
// (or a wording tweak) silently broke the old branches.
|
|
147
|
+
const code = error instanceof ApiError ? error.code : undefined;
|
|
148
|
+
|
|
149
|
+
// Codes the server emits today. Unknown / missing codes fall
|
|
150
|
+
// through to the generic "show the server's message" branch.
|
|
151
|
+
if (code === "INVALID_CREDENTIALS") {
|
|
152
|
+
setErrors({ password: "Invalid email or password" });
|
|
153
|
+
emailShake.shake();
|
|
154
|
+
passwordShake.shake();
|
|
155
|
+
} else if (code === "ACCOUNT_LOCKED") {
|
|
156
|
+
setErrors({ email: "Account is locked. Please contact support." });
|
|
157
|
+
emailShake.shake();
|
|
158
|
+
} else {
|
|
159
|
+
setErrors({ password: errorMessage });
|
|
160
|
+
passwordShake.shake();
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
setIsLoading(false);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const handleLogin = async () => {
|
|
168
|
+
const newErrors = validateForm();
|
|
169
|
+
if (Object.keys(newErrors).length > 0) {
|
|
170
|
+
setErrors(newErrors);
|
|
171
|
+
if (newErrors.email) {
|
|
172
|
+
emailShake.shake();
|
|
173
|
+
}
|
|
174
|
+
if (newErrors.password) {
|
|
175
|
+
passwordShake.shake();
|
|
176
|
+
}
|
|
177
|
+
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
setErrors({});
|
|
181
|
+
|
|
182
|
+
await performLogin();
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const routeAfterUnlock = (onboardingStep?: string | null) => {
|
|
186
|
+
if (onboardingStep === "email_verification") {
|
|
187
|
+
router.replace({ pathname: routes.verifyEmail, params: { email } } as Href);
|
|
188
|
+
} else if (onboardingStep === "mfa_enrollment") {
|
|
189
|
+
router.replace(routes.mfaEnroll);
|
|
190
|
+
} else if (onboardingStep === "import") {
|
|
191
|
+
router.replace(routes.importData);
|
|
192
|
+
} else {
|
|
193
|
+
// `complete`, null, undefined, or any unknown step the client
|
|
194
|
+
// doesn't recognise — the consumer's route guard re-evaluates
|
|
195
|
+
// and bounces to welcome / the right step if the server says
|
|
196
|
+
// onboarding isn't done. Default to home rather than stranding
|
|
197
|
+
// the user.
|
|
198
|
+
router.replace(routes.home);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const handlePasskeyLogin = async () => {
|
|
203
|
+
setPasskeyStep("passkey-loading");
|
|
204
|
+
setErrors({});
|
|
205
|
+
// Abort any prior ceremony before starting a new one so a
|
|
206
|
+
// double-tap doesn't race two outstanding credentials.get() calls.
|
|
207
|
+
passkeyAbortRef.current?.abort();
|
|
208
|
+
const controller = new AbortController();
|
|
209
|
+
passkeyAbortRef.current = controller;
|
|
210
|
+
try {
|
|
211
|
+
const trimmedEmail = email.trim().toLowerCase();
|
|
212
|
+
const result = await loginWithPasskey(
|
|
213
|
+
trimmedEmail.length > 0 ? trimmedEmail : undefined,
|
|
214
|
+
controller.signal
|
|
215
|
+
);
|
|
216
|
+
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
|
217
|
+
|
|
218
|
+
if (result.unlocked) {
|
|
219
|
+
setPasskeyStep("idle");
|
|
220
|
+
routeAfterUnlock(result.user.onboarding_step);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
setPendingOnboardingStep(result.user.onboarding_step ?? null);
|
|
225
|
+
setPasskeyStep("passkey-unlock");
|
|
226
|
+
} catch (error) {
|
|
227
|
+
// Distinguish user-cancel / programmatic-abort from real
|
|
228
|
+
// failures. `NotAllowedError` (user cancel, timeout) and
|
|
229
|
+
// `AbortError` (signal abort) shouldn't paint a destructive
|
|
230
|
+
// error on the password field — the user knows they cancelled.
|
|
231
|
+
// Real failures (`SecurityError`, `UnknownError`, network) do
|
|
232
|
+
// want the inline message + shake so the user falls back to
|
|
233
|
+
// password.
|
|
234
|
+
const name = error instanceof Error ? error.name : undefined;
|
|
235
|
+
const isBenign = name === "NotAllowedError" || name === "AbortError";
|
|
236
|
+
if (!isBenign) {
|
|
237
|
+
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
|
238
|
+
const message = error instanceof Error ? error.message : "Passkey sign-in failed";
|
|
239
|
+
setErrors({ password: message });
|
|
240
|
+
}
|
|
241
|
+
setPasskeyStep("idle");
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const handleUnlockAfterPasskey = async () => {
|
|
246
|
+
if (!unlockPassword) {
|
|
247
|
+
setUnlockError("Master password is required");
|
|
248
|
+
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
setIsLoading(true);
|
|
252
|
+
try {
|
|
253
|
+
await unlockWithPassword(unlockPassword);
|
|
254
|
+
setUnlockPassword("");
|
|
255
|
+
setUnlockError("");
|
|
256
|
+
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
|
257
|
+
routeAfterUnlock(pendingOnboardingStep ?? "complete");
|
|
258
|
+
setPasskeyStep("idle");
|
|
259
|
+
} catch (error) {
|
|
260
|
+
const message =
|
|
261
|
+
error instanceof Error ? error.message : "Failed to unlock the vault. Check your password.";
|
|
262
|
+
setUnlockError(message);
|
|
263
|
+
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
|
264
|
+
} finally {
|
|
265
|
+
setIsLoading(false);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<>
|
|
271
|
+
<SafeAreaView className="flex-1 bg-background">
|
|
272
|
+
<StatusBar style={isDark ? "light" : "dark"} />
|
|
273
|
+
<ScrollView
|
|
274
|
+
className="flex-1"
|
|
275
|
+
contentContainerStyle={SCROLL_CONTENT_STYLE}
|
|
276
|
+
keyboardShouldPersistTaps="handled"
|
|
277
|
+
showsVerticalScrollIndicator={false}
|
|
278
|
+
>
|
|
279
|
+
<View className="flex-1 justify-between px-6">
|
|
280
|
+
<View className="mt-4">
|
|
281
|
+
<IconButton
|
|
282
|
+
variant="ghost"
|
|
283
|
+
onPress={() => router.push(routes.welcome)}
|
|
284
|
+
className="self-start"
|
|
285
|
+
accessibilityLabel="Go back"
|
|
286
|
+
accessibilityRole="button"
|
|
287
|
+
>
|
|
288
|
+
<Ionicons name="arrow-back" size={24} color={isDark ? "#FFFFFF" : "#000000"} />
|
|
289
|
+
</IconButton>
|
|
290
|
+
</View>
|
|
291
|
+
{topAccessory}
|
|
292
|
+
|
|
293
|
+
<View className="flex-1 items-center justify-center">
|
|
294
|
+
<View className="mb-6 items-center">
|
|
295
|
+
<View className="flex-row items-center">
|
|
296
|
+
<Logo width={72} height={72} style={{ marginRight: -2 }} />
|
|
297
|
+
<Heading className="text-4xl">{appName}</Heading>
|
|
298
|
+
</View>
|
|
299
|
+
</View>
|
|
300
|
+
|
|
301
|
+
<Card className="w-full max-w-[360px]">
|
|
302
|
+
<CardHeader>
|
|
303
|
+
<CardTitle>Sign In</CardTitle>
|
|
304
|
+
<CardDescription>Enter your credentials to sign in to {appName}</CardDescription>
|
|
305
|
+
</CardHeader>
|
|
306
|
+
|
|
307
|
+
<CardContent className="gap-4">
|
|
308
|
+
{/* Passkey CTA above email/password — passkeys are
|
|
309
|
+
the preferred primary factor when the device has
|
|
310
|
+
one enrolled. Email is optional; when blank the
|
|
311
|
+
server emits a discoverable-credential challenge
|
|
312
|
+
and the platform UI picks the account. */}
|
|
313
|
+
{passkeyEnabled ? (
|
|
314
|
+
<>
|
|
315
|
+
<Button
|
|
316
|
+
variant="outline"
|
|
317
|
+
className="w-full"
|
|
318
|
+
onPress={handlePasskeyLogin}
|
|
319
|
+
isLoading={passkeyStep === "passkey-loading"}
|
|
320
|
+
accessibilityLabel="Sign in with passkey"
|
|
321
|
+
accessibilityRole="button"
|
|
322
|
+
accessibilityHint="Use a passkey from this device to sign in"
|
|
323
|
+
testID="login-passkey-button"
|
|
324
|
+
>
|
|
325
|
+
<View className="flex-row items-center justify-center">
|
|
326
|
+
<Ionicons
|
|
327
|
+
name="key-outline"
|
|
328
|
+
size={20}
|
|
329
|
+
color={isDark ? "#FFFFFF" : "#000000"}
|
|
330
|
+
className="mr-2"
|
|
331
|
+
/>
|
|
332
|
+
<Text className="text-base font-medium">Sign in with passkey</Text>
|
|
333
|
+
</View>
|
|
334
|
+
</Button>
|
|
335
|
+
<SeparatorWithText />
|
|
336
|
+
</>
|
|
337
|
+
) : null}
|
|
338
|
+
|
|
339
|
+
<ShakeField shake={emailShake} label="Email">
|
|
340
|
+
<EmailInput
|
|
341
|
+
value={email}
|
|
342
|
+
onChangeText={(text) => {
|
|
343
|
+
setEmail(text);
|
|
344
|
+
if (errors.email) {
|
|
345
|
+
setErrors({ ...errors, email: undefined });
|
|
346
|
+
}
|
|
347
|
+
}}
|
|
348
|
+
autoFocus
|
|
349
|
+
invalid={!!errors.email}
|
|
350
|
+
autoComplete="email"
|
|
351
|
+
accessibilityLabel="Email address"
|
|
352
|
+
testID="login-email-input"
|
|
353
|
+
/>
|
|
354
|
+
</ShakeField>
|
|
355
|
+
|
|
356
|
+
<ShakeField shake={passwordShake} label={passwordLabel}>
|
|
357
|
+
<PasswordInput
|
|
358
|
+
value={password}
|
|
359
|
+
onChangeText={(text) => {
|
|
360
|
+
setPassword(text);
|
|
361
|
+
if (errors.password) {
|
|
362
|
+
setErrors({ ...errors, password: undefined });
|
|
363
|
+
}
|
|
364
|
+
}}
|
|
365
|
+
placeholder={passwordPlaceholder}
|
|
366
|
+
invalid={!!errors.password}
|
|
367
|
+
autoComplete="password"
|
|
368
|
+
returnKeyType="done"
|
|
369
|
+
onSubmitEditing={handleLogin}
|
|
370
|
+
accessibilityLabel={passwordLabel}
|
|
371
|
+
testID="login-password-input"
|
|
372
|
+
/>
|
|
373
|
+
</ShakeField>
|
|
374
|
+
|
|
375
|
+
<View className="items-end">
|
|
376
|
+
<Text
|
|
377
|
+
className="font-mono text-sm underline"
|
|
378
|
+
onPress={() => router.push(routes.forgotPassword)}
|
|
379
|
+
accessibilityRole="link"
|
|
380
|
+
accessibilityLabel="Forgot password"
|
|
381
|
+
>
|
|
382
|
+
Forgot password?
|
|
383
|
+
</Text>
|
|
384
|
+
</View>
|
|
385
|
+
</CardContent>
|
|
386
|
+
|
|
387
|
+
<CardFooter className="flex-col gap-3 border-t-0">
|
|
388
|
+
<MutedText className="text-center text-sm">
|
|
389
|
+
Don't have an account?{" "}
|
|
390
|
+
<Text
|
|
391
|
+
className="text-sm text-foreground underline"
|
|
392
|
+
onPress={() => router.push(routes.register)}
|
|
393
|
+
accessibilityRole="link"
|
|
394
|
+
accessibilityLabel="Sign up"
|
|
395
|
+
>
|
|
396
|
+
Sign Up
|
|
397
|
+
</Text>
|
|
398
|
+
</MutedText>
|
|
399
|
+
|
|
400
|
+
<FormError message={firstError} />
|
|
401
|
+
|
|
402
|
+
<Button
|
|
403
|
+
className="w-full"
|
|
404
|
+
onPress={handleLogin}
|
|
405
|
+
isLoading={isLoading}
|
|
406
|
+
accessibilityLabel="Sign in"
|
|
407
|
+
accessibilityRole="button"
|
|
408
|
+
accessibilityHint="Double tap to sign in with your credentials"
|
|
409
|
+
testID="login-submit"
|
|
410
|
+
>
|
|
411
|
+
Sign In
|
|
412
|
+
</Button>
|
|
413
|
+
|
|
414
|
+
{socialAuth ? (
|
|
415
|
+
<>
|
|
416
|
+
<SeparatorWithText />
|
|
417
|
+
{socialAuth}
|
|
418
|
+
</>
|
|
419
|
+
) : null}
|
|
420
|
+
</CardFooter>
|
|
421
|
+
</Card>
|
|
422
|
+
</View>
|
|
423
|
+
|
|
424
|
+
<View className="mb-6" />
|
|
425
|
+
</View>
|
|
426
|
+
</ScrollView>
|
|
427
|
+
</SafeAreaView>
|
|
428
|
+
|
|
429
|
+
{/* Passkey unlock dialog — shown only when the authenticator
|
|
430
|
+
did NOT honor the PRF extension. The session is already
|
|
431
|
+
established server-side; this prompt gathers the master
|
|
432
|
+
password so the consumer's `unlockWithPassword` handler can
|
|
433
|
+
derive the symmetric key locally before routing. */}
|
|
434
|
+
<Dialog
|
|
435
|
+
visible={passkeyStep === "passkey-unlock"}
|
|
436
|
+
onClose={() => {
|
|
437
|
+
setPasskeyStep("idle");
|
|
438
|
+
setUnlockPassword("");
|
|
439
|
+
setUnlockError("");
|
|
440
|
+
}}
|
|
441
|
+
draggable
|
|
442
|
+
dismissOnBackdrop={false}
|
|
443
|
+
>
|
|
444
|
+
<View className="mb-5 items-center">
|
|
445
|
+
<View
|
|
446
|
+
className={`mb-4 h-14 w-14 items-center justify-center rounded-full ${isDark ? "bg-white/10" : "bg-black/5"}`}
|
|
447
|
+
>
|
|
448
|
+
<Ionicons name="lock-open-outline" size={28} color={isDark ? "#FFFFFF" : "#000000"} />
|
|
449
|
+
</View>
|
|
450
|
+
<Heading className="mb-2 text-xl">{passkeyUnlockTitle}</Heading>
|
|
451
|
+
<Text className="mb-3 text-center text-sm leading-5">{passkeyUnlockDescription}</Text>
|
|
452
|
+
<PasswordInput
|
|
453
|
+
value={unlockPassword}
|
|
454
|
+
onChangeText={(text) => {
|
|
455
|
+
setUnlockPassword(text);
|
|
456
|
+
if (unlockError) {
|
|
457
|
+
setUnlockError("");
|
|
458
|
+
}
|
|
459
|
+
}}
|
|
460
|
+
placeholder={passkeyUnlockPlaceholder}
|
|
461
|
+
error={unlockError}
|
|
462
|
+
autoComplete="password"
|
|
463
|
+
returnKeyType="done"
|
|
464
|
+
onSubmitEditing={handleUnlockAfterPasskey}
|
|
465
|
+
accessibilityLabel="Master password"
|
|
466
|
+
testID="login-passkey-unlock-input"
|
|
467
|
+
/>
|
|
468
|
+
</View>
|
|
469
|
+
<DialogFooter style={{ justifyContent: "center" }}>
|
|
470
|
+
<DialogButton
|
|
471
|
+
variant="primary"
|
|
472
|
+
onPress={isLoading ? () => {} : handleUnlockAfterPasskey}
|
|
473
|
+
accessibilityLabel="Unlock"
|
|
474
|
+
accessibilityRole="button"
|
|
475
|
+
>
|
|
476
|
+
{isLoading ? "Unlocking…" : "Unlock"}
|
|
477
|
+
</DialogButton>
|
|
478
|
+
</DialogFooter>
|
|
479
|
+
</Dialog>
|
|
480
|
+
</>
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export default LoginScreen;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { ActivityIndicator, Platform, Pressable, Text, TextInput, View } from "react-native";
|
|
3
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
4
|
+
import { useNavigation } from "../../context/NavigationContext";
|
|
5
|
+
import { StatusBar } from "expo-status-bar";
|
|
6
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
7
|
+
import { useColorScheme } from "nativewind";
|
|
8
|
+
import * as Haptics from "expo-haptics";
|
|
9
|
+
|
|
10
|
+
import AuthService from "../../services/auth.service";
|
|
11
|
+
import { useAppConfig } from "../../context/AppConfigContext";
|
|
12
|
+
import { getErrorMessage } from "../../utils/error";
|
|
13
|
+
|
|
14
|
+
export function MfaChallengeScreen() {
|
|
15
|
+
const { colorScheme } = useColorScheme();
|
|
16
|
+
const isDark = colorScheme === "dark";
|
|
17
|
+
const router = useNavigation();
|
|
18
|
+
const { routes } = useAppConfig();
|
|
19
|
+
|
|
20
|
+
const [code, setCode] = useState("");
|
|
21
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
22
|
+
const [error, setError] = useState("");
|
|
23
|
+
const [isRecoveryMode, setIsRecoveryMode] = useState(false);
|
|
24
|
+
|
|
25
|
+
const handleVerify = async () => {
|
|
26
|
+
// Re-entry guard: a fast double-tap (button + onSubmitEditing)
|
|
27
|
+
// would otherwise re-issue verifyMfa before `setIsLoading(true)`
|
|
28
|
+
// had time to flow through React and disable the submit. Each
|
|
29
|
+
// verify burns a one-shot TOTP attempt server-side; cheaper to
|
|
30
|
+
// gate at the client.
|
|
31
|
+
if (isLoading) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const trimmed = code.trim();
|
|
35
|
+
if (!isRecoveryMode && trimmed.length !== 6) {
|
|
36
|
+
setError("Please enter a 6-digit code");
|
|
37
|
+
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (isRecoveryMode && trimmed.length === 0) {
|
|
41
|
+
setError("Please enter a recovery code");
|
|
42
|
+
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setIsLoading(true);
|
|
47
|
+
setError("");
|
|
48
|
+
try {
|
|
49
|
+
await AuthService.verifyMfa(trimmed);
|
|
50
|
+
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
|
51
|
+
router.replace(routes.home);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
setError(getErrorMessage(err, "Invalid code"));
|
|
54
|
+
setCode("");
|
|
55
|
+
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
|
56
|
+
} finally {
|
|
57
|
+
setIsLoading(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const toggleRecoveryMode = () => {
|
|
62
|
+
setIsRecoveryMode(!isRecoveryMode);
|
|
63
|
+
setCode("");
|
|
64
|
+
setError("");
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const inputStyle = {
|
|
68
|
+
borderWidth: 1,
|
|
69
|
+
borderColor: error ? "#EF4444" : isDark ? "#333333" : "#E5E5E5",
|
|
70
|
+
borderRadius: 8,
|
|
71
|
+
paddingHorizontal: 12,
|
|
72
|
+
paddingVertical: Platform.OS === "ios" ? 12 : 8,
|
|
73
|
+
backgroundColor: isDark ? "#0A0A0A" : "#FAFAFA",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<SafeAreaView className="flex-1" style={{ backgroundColor: isDark ? "#000000" : "#FFFFFF" }}>
|
|
78
|
+
<StatusBar style={isDark ? "light" : "dark"} />
|
|
79
|
+
<View className="flex-1 items-center justify-center px-6">
|
|
80
|
+
<View className="w-full max-w-[360px]">
|
|
81
|
+
<View className="items-center mb-6">
|
|
82
|
+
<Ionicons
|
|
83
|
+
name="shield-checkmark-outline"
|
|
84
|
+
size={48}
|
|
85
|
+
color={isDark ? "#FFFFFF" : "#000000"}
|
|
86
|
+
/>
|
|
87
|
+
</View>
|
|
88
|
+
|
|
89
|
+
<Text
|
|
90
|
+
className="font-sans-bold text-2xl text-center mb-2"
|
|
91
|
+
style={{ color: isDark ? "#FFFFFF" : "#000000" }}
|
|
92
|
+
>
|
|
93
|
+
Two-Factor Authentication
|
|
94
|
+
</Text>
|
|
95
|
+
|
|
96
|
+
<Text
|
|
97
|
+
className="font-sans text-sm text-center mb-8"
|
|
98
|
+
style={{ color: isDark ? "#999999" : "#666666", lineHeight: 22 }}
|
|
99
|
+
>
|
|
100
|
+
{isRecoveryMode
|
|
101
|
+
? "Enter one of your recovery codes"
|
|
102
|
+
: "Enter the 6-digit code from your authenticator app"}
|
|
103
|
+
</Text>
|
|
104
|
+
|
|
105
|
+
{error ? (
|
|
106
|
+
<View
|
|
107
|
+
className="rounded-lg p-3 mb-4"
|
|
108
|
+
style={{
|
|
109
|
+
backgroundColor: isDark ? "#1A0000" : "#FEF2F2",
|
|
110
|
+
borderWidth: 1,
|
|
111
|
+
borderColor: isDark ? "#7F1D1D" : "#FECACA",
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<Text className="font-sans text-sm text-center" style={{ color: "#EF4444" }}>
|
|
115
|
+
{error}
|
|
116
|
+
</Text>
|
|
117
|
+
</View>
|
|
118
|
+
) : null}
|
|
119
|
+
|
|
120
|
+
{isRecoveryMode ? (
|
|
121
|
+
<TextInput
|
|
122
|
+
value={code}
|
|
123
|
+
onChangeText={(text) => {
|
|
124
|
+
setCode(text);
|
|
125
|
+
if (error) {
|
|
126
|
+
setError("");
|
|
127
|
+
}
|
|
128
|
+
}}
|
|
129
|
+
placeholder="Enter recovery code"
|
|
130
|
+
placeholderTextColor={isDark ? "#999999" : "#666666"}
|
|
131
|
+
className="font-mono text-lg text-center tracking-wide"
|
|
132
|
+
style={{
|
|
133
|
+
...inputStyle,
|
|
134
|
+
color: isDark ? "#FFFFFF" : "#000000",
|
|
135
|
+
}}
|
|
136
|
+
autoCapitalize="none"
|
|
137
|
+
autoCorrect={false}
|
|
138
|
+
autoFocus
|
|
139
|
+
returnKeyType="done"
|
|
140
|
+
onSubmitEditing={handleVerify}
|
|
141
|
+
accessibilityLabel="Recovery code"
|
|
142
|
+
/>
|
|
143
|
+
) : (
|
|
144
|
+
<TextInput
|
|
145
|
+
value={code}
|
|
146
|
+
onChangeText={(text) => {
|
|
147
|
+
const digits = text.replace(/[^0-9]/g, "").slice(0, 6);
|
|
148
|
+
setCode(digits);
|
|
149
|
+
if (error) {
|
|
150
|
+
setError("");
|
|
151
|
+
}
|
|
152
|
+
}}
|
|
153
|
+
placeholder="000000"
|
|
154
|
+
placeholderTextColor={isDark ? "#999999" : "#666666"}
|
|
155
|
+
className="font-mono text-center"
|
|
156
|
+
style={{
|
|
157
|
+
...inputStyle,
|
|
158
|
+
fontSize: 32,
|
|
159
|
+
color: isDark ? "#FFFFFF" : "#000000",
|
|
160
|
+
letterSpacing: 12,
|
|
161
|
+
}}
|
|
162
|
+
keyboardType="number-pad"
|
|
163
|
+
maxLength={6}
|
|
164
|
+
autoFocus
|
|
165
|
+
returnKeyType="done"
|
|
166
|
+
onSubmitEditing={handleVerify}
|
|
167
|
+
accessibilityLabel="Authenticator code"
|
|
168
|
+
/>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
<Pressable
|
|
172
|
+
onPress={handleVerify}
|
|
173
|
+
disabled={isLoading}
|
|
174
|
+
className="rounded-lg items-center mt-5"
|
|
175
|
+
style={({ pressed }) => ({
|
|
176
|
+
backgroundColor: isDark ? "#FFFFFF" : "#000000",
|
|
177
|
+
paddingVertical: 14,
|
|
178
|
+
opacity: pressed || isLoading ? 0.7 : 1,
|
|
179
|
+
})}
|
|
180
|
+
accessibilityRole="button"
|
|
181
|
+
accessibilityLabel="Verify"
|
|
182
|
+
accessibilityHint="Double tap to verify your code"
|
|
183
|
+
>
|
|
184
|
+
{isLoading ? (
|
|
185
|
+
<ActivityIndicator color={isDark ? "#000000" : "#FFFFFF"} />
|
|
186
|
+
) : (
|
|
187
|
+
<Text
|
|
188
|
+
className="font-sans-bold text-base"
|
|
189
|
+
style={{ color: isDark ? "#000000" : "#FFFFFF" }}
|
|
190
|
+
>
|
|
191
|
+
Verify
|
|
192
|
+
</Text>
|
|
193
|
+
)}
|
|
194
|
+
</Pressable>
|
|
195
|
+
|
|
196
|
+
<Pressable
|
|
197
|
+
onPress={toggleRecoveryMode}
|
|
198
|
+
className="mt-5 items-center"
|
|
199
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
200
|
+
accessibilityRole="button"
|
|
201
|
+
accessibilityLabel={isRecoveryMode ? "Use authenticator code" : "Use recovery code"}
|
|
202
|
+
>
|
|
203
|
+
<Text
|
|
204
|
+
className="font-sans text-sm underline"
|
|
205
|
+
style={{ color: isDark ? "#999999" : "#666666" }}
|
|
206
|
+
>
|
|
207
|
+
{isRecoveryMode ? "Use authenticator code" : "Use recovery code"}
|
|
208
|
+
</Text>
|
|
209
|
+
</Pressable>
|
|
210
|
+
</View>
|
|
211
|
+
</View>
|
|
212
|
+
</SafeAreaView>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export default MfaChallengeScreen;
|