@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.
Files changed (590) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +21 -0
  3. package/README.md +115 -0
  4. package/lib/components/auth/LockOverlay.d.ts +22 -0
  5. package/lib/components/auth/LockOverlay.d.ts.map +1 -0
  6. package/lib/components/auth/LockOverlay.jsx +82 -0
  7. package/lib/components/auth/LockOverlay.jsx.map +1 -0
  8. package/lib/components/auth/PasswordStrengthMeter.d.ts +12 -0
  9. package/lib/components/auth/PasswordStrengthMeter.d.ts.map +1 -0
  10. package/lib/components/auth/PasswordStrengthMeter.jsx +81 -0
  11. package/lib/components/auth/PasswordStrengthMeter.jsx.map +1 -0
  12. package/lib/components/auth/ProtectedRoute.d.ts +24 -0
  13. package/lib/components/auth/ProtectedRoute.d.ts.map +1 -0
  14. package/lib/components/auth/ProtectedRoute.jsx +56 -0
  15. package/lib/components/auth/ProtectedRoute.jsx.map +1 -0
  16. package/lib/components/auth/PwnedPasswordWarning.d.ts +13 -0
  17. package/lib/components/auth/PwnedPasswordWarning.d.ts.map +1 -0
  18. package/lib/components/auth/PwnedPasswordWarning.jsx +29 -0
  19. package/lib/components/auth/PwnedPasswordWarning.jsx.map +1 -0
  20. package/lib/components/auth/index.d.ts +5 -0
  21. package/lib/components/auth/index.d.ts.map +1 -0
  22. package/lib/components/auth/index.js +5 -0
  23. package/lib/components/auth/index.js.map +1 -0
  24. package/lib/components/bottom-nav.d.ts +25 -0
  25. package/lib/components/bottom-nav.d.ts.map +1 -0
  26. package/lib/components/bottom-nav.jsx +51 -0
  27. package/lib/components/bottom-nav.jsx.map +1 -0
  28. package/lib/components/button.d.ts +16 -0
  29. package/lib/components/button.d.ts.map +1 -0
  30. package/lib/components/button.jsx +73 -0
  31. package/lib/components/button.jsx.map +1 -0
  32. package/lib/components/card.d.ts +22 -0
  33. package/lib/components/card.d.ts.map +1 -0
  34. package/lib/components/card.jsx +36 -0
  35. package/lib/components/card.jsx.map +1 -0
  36. package/lib/components/checkbox.d.ts +12 -0
  37. package/lib/components/checkbox.d.ts.map +1 -0
  38. package/lib/components/checkbox.jsx +16 -0
  39. package/lib/components/checkbox.jsx.map +1 -0
  40. package/lib/components/connection-settings.d.ts +41 -0
  41. package/lib/components/connection-settings.d.ts.map +1 -0
  42. package/lib/components/connection-settings.jsx +354 -0
  43. package/lib/components/connection-settings.jsx.map +1 -0
  44. package/lib/components/context-menu.d.ts +52 -0
  45. package/lib/components/context-menu.d.ts.map +1 -0
  46. package/lib/components/context-menu.jsx +24 -0
  47. package/lib/components/context-menu.jsx.map +1 -0
  48. package/lib/components/data-table.d.ts +59 -0
  49. package/lib/components/data-table.d.ts.map +1 -0
  50. package/lib/components/data-table.jsx +101 -0
  51. package/lib/components/data-table.jsx.map +1 -0
  52. package/lib/components/dialog.d.ts +58 -0
  53. package/lib/components/dialog.d.ts.map +1 -0
  54. package/lib/components/dialog.jsx +197 -0
  55. package/lib/components/dialog.jsx.map +1 -0
  56. package/lib/components/dropdown-menu.d.ts +55 -0
  57. package/lib/components/dropdown-menu.d.ts.map +1 -0
  58. package/lib/components/dropdown-menu.jsx +38 -0
  59. package/lib/components/dropdown-menu.jsx.map +1 -0
  60. package/lib/components/entropy-boot-splash.d.ts +24 -0
  61. package/lib/components/entropy-boot-splash.d.ts.map +1 -0
  62. package/lib/components/entropy-boot-splash.jsx +83 -0
  63. package/lib/components/entropy-boot-splash.jsx.map +1 -0
  64. package/lib/components/entropy-mark-animated.d.ts +20 -0
  65. package/lib/components/entropy-mark-animated.d.ts.map +1 -0
  66. package/lib/components/entropy-mark-animated.jsx +148 -0
  67. package/lib/components/entropy-mark-animated.jsx.map +1 -0
  68. package/lib/components/entropy-mark-geometry.d.ts +8 -0
  69. package/lib/components/entropy-mark-geometry.d.ts.map +1 -0
  70. package/lib/components/entropy-mark-geometry.js +10 -0
  71. package/lib/components/entropy-mark-geometry.js.map +1 -0
  72. package/lib/components/entropy-mark-sequential.d.ts +12 -0
  73. package/lib/components/entropy-mark-sequential.d.ts.map +1 -0
  74. package/lib/components/entropy-mark-sequential.jsx +53 -0
  75. package/lib/components/entropy-mark-sequential.jsx.map +1 -0
  76. package/lib/components/entropy-mark.d.ts +29 -0
  77. package/lib/components/entropy-mark.d.ts.map +1 -0
  78. package/lib/components/entropy-mark.jsx +66 -0
  79. package/lib/components/entropy-mark.jsx.map +1 -0
  80. package/lib/components/error-boundary.d.ts +31 -0
  81. package/lib/components/error-boundary.d.ts.map +1 -0
  82. package/lib/components/error-boundary.jsx +40 -0
  83. package/lib/components/error-boundary.jsx.map +1 -0
  84. package/lib/components/index.d.ts +41 -0
  85. package/lib/components/index.d.ts.map +1 -0
  86. package/lib/components/index.js +43 -0
  87. package/lib/components/index.js.map +1 -0
  88. package/lib/components/input.d.ts +26 -0
  89. package/lib/components/input.d.ts.map +1 -0
  90. package/lib/components/input.jsx +95 -0
  91. package/lib/components/input.jsx.map +1 -0
  92. package/lib/components/item-icon.d.ts +32 -0
  93. package/lib/components/item-icon.d.ts.map +1 -0
  94. package/lib/components/item-icon.jsx +54 -0
  95. package/lib/components/item-icon.jsx.map +1 -0
  96. package/lib/components/label.d.ts +13 -0
  97. package/lib/components/label.d.ts.map +1 -0
  98. package/lib/components/label.jsx +9 -0
  99. package/lib/components/label.jsx.map +1 -0
  100. package/lib/components/legal-dialog.d.ts +18 -0
  101. package/lib/components/legal-dialog.d.ts.map +1 -0
  102. package/lib/components/legal-dialog.jsx +31 -0
  103. package/lib/components/legal-dialog.jsx.map +1 -0
  104. package/lib/components/license-gate.d.ts +17 -0
  105. package/lib/components/license-gate.d.ts.map +1 -0
  106. package/lib/components/license-gate.jsx +21 -0
  107. package/lib/components/license-gate.jsx.map +1 -0
  108. package/lib/components/logo.d.ts +15 -0
  109. package/lib/components/logo.d.ts.map +1 -0
  110. package/lib/components/logo.jsx +20 -0
  111. package/lib/components/logo.jsx.map +1 -0
  112. package/lib/components/onboarding-wizard.d.ts +89 -0
  113. package/lib/components/onboarding-wizard.d.ts.map +1 -0
  114. package/lib/components/onboarding-wizard.jsx +185 -0
  115. package/lib/components/onboarding-wizard.jsx.map +1 -0
  116. package/lib/components/pagination.d.ts +43 -0
  117. package/lib/components/pagination.d.ts.map +1 -0
  118. package/lib/components/pagination.jsx +72 -0
  119. package/lib/components/pagination.jsx.map +1 -0
  120. package/lib/components/password-generator.d.ts +15 -0
  121. package/lib/components/password-generator.d.ts.map +1 -0
  122. package/lib/components/password-generator.jsx +273 -0
  123. package/lib/components/password-generator.jsx.map +1 -0
  124. package/lib/components/pin-pad.d.ts +20 -0
  125. package/lib/components/pin-pad.d.ts.map +1 -0
  126. package/lib/components/pin-pad.jsx +93 -0
  127. package/lib/components/pin-pad.jsx.map +1 -0
  128. package/lib/components/popover.d.ts +27 -0
  129. package/lib/components/popover.d.ts.map +1 -0
  130. package/lib/components/popover.jsx +18 -0
  131. package/lib/components/popover.jsx.map +1 -0
  132. package/lib/components/profile/ProfileDialog.d.ts +25 -0
  133. package/lib/components/profile/ProfileDialog.d.ts.map +1 -0
  134. package/lib/components/profile/ProfileDialog.jsx +71 -0
  135. package/lib/components/profile/ProfileDialog.jsx.map +1 -0
  136. package/lib/components/profile/ProfileTab.d.ts +12 -0
  137. package/lib/components/profile/ProfileTab.d.ts.map +1 -0
  138. package/lib/components/profile/ProfileTab.jsx +112 -0
  139. package/lib/components/profile/ProfileTab.jsx.map +1 -0
  140. package/lib/components/profile/SecurityTab.d.ts +32 -0
  141. package/lib/components/profile/SecurityTab.d.ts.map +1 -0
  142. package/lib/components/profile/SecurityTab.jsx +145 -0
  143. package/lib/components/profile/SecurityTab.jsx.map +1 -0
  144. package/lib/components/profile/index.d.ts +4 -0
  145. package/lib/components/profile/index.d.ts.map +1 -0
  146. package/lib/components/profile/index.js +4 -0
  147. package/lib/components/profile/index.js.map +1 -0
  148. package/lib/components/segmented-toggle.d.ts +28 -0
  149. package/lib/components/segmented-toggle.d.ts.map +1 -0
  150. package/lib/components/segmented-toggle.jsx +64 -0
  151. package/lib/components/segmented-toggle.jsx.map +1 -0
  152. package/lib/components/separator.d.ts +13 -0
  153. package/lib/components/separator.d.ts.map +1 -0
  154. package/lib/components/separator.jsx +16 -0
  155. package/lib/components/separator.jsx.map +1 -0
  156. package/lib/components/server-settings.d.ts +27 -0
  157. package/lib/components/server-settings.d.ts.map +1 -0
  158. package/lib/components/server-settings.jsx +307 -0
  159. package/lib/components/server-settings.jsx.map +1 -0
  160. package/lib/components/settings/AccountSection.d.ts +26 -0
  161. package/lib/components/settings/AccountSection.d.ts.map +1 -0
  162. package/lib/components/settings/AccountSection.jsx +32 -0
  163. package/lib/components/settings/AccountSection.jsx.map +1 -0
  164. package/lib/components/settings/DangerZoneSection.d.ts +13 -0
  165. package/lib/components/settings/DangerZoneSection.d.ts.map +1 -0
  166. package/lib/components/settings/DangerZoneSection.jsx +16 -0
  167. package/lib/components/settings/DangerZoneSection.jsx.map +1 -0
  168. package/lib/components/settings/GeneralSection.d.ts +11 -0
  169. package/lib/components/settings/GeneralSection.d.ts.map +1 -0
  170. package/lib/components/settings/GeneralSection.jsx +12 -0
  171. package/lib/components/settings/GeneralSection.jsx.map +1 -0
  172. package/lib/components/settings/SupportSection.d.ts +11 -0
  173. package/lib/components/settings/SupportSection.d.ts.map +1 -0
  174. package/lib/components/settings/SupportSection.jsx +22 -0
  175. package/lib/components/settings/SupportSection.jsx.map +1 -0
  176. package/lib/components/settings/index.d.ts +5 -0
  177. package/lib/components/settings/index.d.ts.map +1 -0
  178. package/lib/components/settings/index.js +5 -0
  179. package/lib/components/settings/index.js.map +1 -0
  180. package/lib/components/settings-row.d.ts +28 -0
  181. package/lib/components/settings-row.d.ts.map +1 -0
  182. package/lib/components/settings-row.jsx +68 -0
  183. package/lib/components/settings-row.jsx.map +1 -0
  184. package/lib/components/skeleton.d.ts +8 -0
  185. package/lib/components/skeleton.d.ts.map +1 -0
  186. package/lib/components/skeleton.jsx +24 -0
  187. package/lib/components/skeleton.jsx.map +1 -0
  188. package/lib/components/social-connections.d.ts +15 -0
  189. package/lib/components/social-connections.d.ts.map +1 -0
  190. package/lib/components/social-connections.jsx +31 -0
  191. package/lib/components/social-connections.jsx.map +1 -0
  192. package/lib/components/stat-card.d.ts +29 -0
  193. package/lib/components/stat-card.d.ts.map +1 -0
  194. package/lib/components/stat-card.jsx +36 -0
  195. package/lib/components/stat-card.jsx.map +1 -0
  196. package/lib/components/tag-chip.d.ts +18 -0
  197. package/lib/components/tag-chip.d.ts.map +1 -0
  198. package/lib/components/tag-chip.jsx +54 -0
  199. package/lib/components/tag-chip.jsx.map +1 -0
  200. package/lib/components/tag-multi-select.d.ts +27 -0
  201. package/lib/components/tag-multi-select.d.ts.map +1 -0
  202. package/lib/components/tag-multi-select.jsx +65 -0
  203. package/lib/components/tag-multi-select.jsx.map +1 -0
  204. package/lib/components/text.d.ts +22 -0
  205. package/lib/components/text.d.ts.map +1 -0
  206. package/lib/components/text.jsx +43 -0
  207. package/lib/components/text.jsx.map +1 -0
  208. package/lib/components/toast/ToastPortal.d.ts +11 -0
  209. package/lib/components/toast/ToastPortal.d.ts.map +1 -0
  210. package/lib/components/toast/ToastPortal.jsx +15 -0
  211. package/lib/components/toast/ToastPortal.jsx.map +1 -0
  212. package/lib/components/toast/ToastPortal.web.d.ts +14 -0
  213. package/lib/components/toast/ToastPortal.web.d.ts.map +1 -0
  214. package/lib/components/toast/ToastPortal.web.jsx +39 -0
  215. package/lib/components/toast/ToastPortal.web.jsx.map +1 -0
  216. package/lib/components/toast/index.d.ts +3 -0
  217. package/lib/components/toast/index.d.ts.map +1 -0
  218. package/lib/components/toast/index.js +3 -0
  219. package/lib/components/toast/index.js.map +1 -0
  220. package/lib/components/toast/toast-config.d.ts +14 -0
  221. package/lib/components/toast/toast-config.d.ts.map +1 -0
  222. package/lib/components/toast/toast-config.jsx +24 -0
  223. package/lib/components/toast/toast-config.jsx.map +1 -0
  224. package/lib/components/tooltip.d.ts +30 -0
  225. package/lib/components/tooltip.d.ts.map +1 -0
  226. package/lib/components/tooltip.jsx +14 -0
  227. package/lib/components/tooltip.jsx.map +1 -0
  228. package/lib/components/totp-display.d.ts +23 -0
  229. package/lib/components/totp-display.d.ts.map +1 -0
  230. package/lib/components/totp-display.jsx +132 -0
  231. package/lib/components/totp-display.jsx.map +1 -0
  232. package/lib/components/user-avatar.d.ts +18 -0
  233. package/lib/components/user-avatar.d.ts.map +1 -0
  234. package/lib/components/user-avatar.jsx +25 -0
  235. package/lib/components/user-avatar.jsx.map +1 -0
  236. package/lib/context/AppConfigContext.d.ts +56 -0
  237. package/lib/context/AppConfigContext.d.ts.map +1 -0
  238. package/lib/context/AppConfigContext.jsx +14 -0
  239. package/lib/context/AppConfigContext.jsx.map +1 -0
  240. package/lib/context/NavigationContext.d.ts +42 -0
  241. package/lib/context/NavigationContext.d.ts.map +1 -0
  242. package/lib/context/NavigationContext.jsx +53 -0
  243. package/lib/context/NavigationContext.jsx.map +1 -0
  244. package/lib/context/NotificationContext.d.ts +32 -0
  245. package/lib/context/NotificationContext.d.ts.map +1 -0
  246. package/lib/context/NotificationContext.jsx +49 -0
  247. package/lib/context/NotificationContext.jsx.map +1 -0
  248. package/lib/context/ThemeContext.d.ts +12 -0
  249. package/lib/context/ThemeContext.d.ts.map +1 -0
  250. package/lib/context/ThemeContext.jsx +66 -0
  251. package/lib/context/ThemeContext.jsx.map +1 -0
  252. package/lib/context/index.d.ts +5 -0
  253. package/lib/context/index.d.ts.map +1 -0
  254. package/lib/context/index.js +5 -0
  255. package/lib/context/index.js.map +1 -0
  256. package/lib/flags.d.ts +9 -0
  257. package/lib/flags.d.ts.map +1 -0
  258. package/lib/flags.js +37 -0
  259. package/lib/flags.js.map +1 -0
  260. package/lib/forms/form-error.d.ts +12 -0
  261. package/lib/forms/form-error.d.ts.map +1 -0
  262. package/lib/forms/form-error.jsx +14 -0
  263. package/lib/forms/form-error.jsx.map +1 -0
  264. package/lib/forms/index.d.ts +3 -0
  265. package/lib/forms/index.d.ts.map +1 -0
  266. package/lib/forms/index.js +3 -0
  267. package/lib/forms/index.js.map +1 -0
  268. package/lib/forms/shake-field.d.ts +18 -0
  269. package/lib/forms/shake-field.d.ts.map +1 -0
  270. package/lib/forms/shake-field.jsx +30 -0
  271. package/lib/forms/shake-field.jsx.map +1 -0
  272. package/lib/hooks/index.d.ts +6 -0
  273. package/lib/hooks/index.d.ts.map +1 -0
  274. package/lib/hooks/index.js +6 -0
  275. package/lib/hooks/index.js.map +1 -0
  276. package/lib/hooks/useAuth.d.ts +109 -0
  277. package/lib/hooks/useAuth.d.ts.map +1 -0
  278. package/lib/hooks/useAuth.jsx +404 -0
  279. package/lib/hooks/useAuth.jsx.map +1 -0
  280. package/lib/hooks/useAutoLock.d.ts +22 -0
  281. package/lib/hooks/useAutoLock.d.ts.map +1 -0
  282. package/lib/hooks/useAutoLock.js +85 -0
  283. package/lib/hooks/useAutoLock.js.map +1 -0
  284. package/lib/hooks/useDebouncedValue.d.ts +19 -0
  285. package/lib/hooks/useDebouncedValue.d.ts.map +1 -0
  286. package/lib/hooks/useDebouncedValue.js +29 -0
  287. package/lib/hooks/useDebouncedValue.js.map +1 -0
  288. package/lib/hooks/useShakeAnimation.d.ts +17 -0
  289. package/lib/hooks/useShakeAnimation.d.ts.map +1 -0
  290. package/lib/hooks/useShakeAnimation.js +22 -0
  291. package/lib/hooks/useShakeAnimation.js.map +1 -0
  292. package/lib/hooks/useUploadBlobUrl.d.ts +13 -0
  293. package/lib/hooks/useUploadBlobUrl.d.ts.map +1 -0
  294. package/lib/hooks/useUploadBlobUrl.js +60 -0
  295. package/lib/hooks/useUploadBlobUrl.js.map +1 -0
  296. package/lib/index.d.ts +10 -0
  297. package/lib/index.d.ts.map +1 -0
  298. package/lib/index.js +47 -0
  299. package/lib/index.js.map +1 -0
  300. package/lib/screens/auth/AccountPickerScreen.d.ts +24 -0
  301. package/lib/screens/auth/AccountPickerScreen.d.ts.map +1 -0
  302. package/lib/screens/auth/AccountPickerScreen.jsx +47 -0
  303. package/lib/screens/auth/AccountPickerScreen.jsx.map +1 -0
  304. package/lib/screens/auth/AuthScreen.d.ts +59 -0
  305. package/lib/screens/auth/AuthScreen.d.ts.map +1 -0
  306. package/lib/screens/auth/AuthScreen.jsx +594 -0
  307. package/lib/screens/auth/AuthScreen.jsx.map +1 -0
  308. package/lib/screens/auth/ConsentScreen.d.ts +27 -0
  309. package/lib/screens/auth/ConsentScreen.d.ts.map +1 -0
  310. package/lib/screens/auth/ConsentScreen.jsx +55 -0
  311. package/lib/screens/auth/ConsentScreen.jsx.map +1 -0
  312. package/lib/screens/auth/ErrorScreen.d.ts +20 -0
  313. package/lib/screens/auth/ErrorScreen.d.ts.map +1 -0
  314. package/lib/screens/auth/ErrorScreen.jsx +43 -0
  315. package/lib/screens/auth/ErrorScreen.jsx.map +1 -0
  316. package/lib/screens/auth/ExpiredLinkScreen.d.ts +19 -0
  317. package/lib/screens/auth/ExpiredLinkScreen.d.ts.map +1 -0
  318. package/lib/screens/auth/ExpiredLinkScreen.jsx +38 -0
  319. package/lib/screens/auth/ExpiredLinkScreen.jsx.map +1 -0
  320. package/lib/screens/auth/ForgotPasswordScreen.d.ts +4 -0
  321. package/lib/screens/auth/ForgotPasswordScreen.d.ts.map +1 -0
  322. package/lib/screens/auth/ForgotPasswordScreen.jsx +123 -0
  323. package/lib/screens/auth/ForgotPasswordScreen.jsx.map +1 -0
  324. package/lib/screens/auth/ImportDataScreen.d.ts +17 -0
  325. package/lib/screens/auth/ImportDataScreen.d.ts.map +1 -0
  326. package/lib/screens/auth/ImportDataScreen.jsx +73 -0
  327. package/lib/screens/auth/ImportDataScreen.jsx.map +1 -0
  328. package/lib/screens/auth/LoginScreen.d.ts +38 -0
  329. package/lib/screens/auth/LoginScreen.d.ts.map +1 -0
  330. package/lib/screens/auth/LoginScreen.jsx +326 -0
  331. package/lib/screens/auth/LoginScreen.jsx.map +1 -0
  332. package/lib/screens/auth/MfaChallengeScreen.d.ts +4 -0
  333. package/lib/screens/auth/MfaChallengeScreen.d.ts.map +1 -0
  334. package/lib/screens/auth/MfaChallengeScreen.jsx +139 -0
  335. package/lib/screens/auth/MfaChallengeScreen.jsx.map +1 -0
  336. package/lib/screens/auth/MfaEnrollScreen.d.ts +12 -0
  337. package/lib/screens/auth/MfaEnrollScreen.d.ts.map +1 -0
  338. package/lib/screens/auth/MfaEnrollScreen.jsx +267 -0
  339. package/lib/screens/auth/MfaEnrollScreen.jsx.map +1 -0
  340. package/lib/screens/auth/PasskeyEnrollScreen.d.ts +18 -0
  341. package/lib/screens/auth/PasskeyEnrollScreen.d.ts.map +1 -0
  342. package/lib/screens/auth/PasskeyEnrollScreen.jsx +51 -0
  343. package/lib/screens/auth/PasskeyEnrollScreen.jsx.map +1 -0
  344. package/lib/screens/auth/PrivacyPolicyScreen.d.ts +19 -0
  345. package/lib/screens/auth/PrivacyPolicyScreen.d.ts.map +1 -0
  346. package/lib/screens/auth/PrivacyPolicyScreen.jsx +143 -0
  347. package/lib/screens/auth/PrivacyPolicyScreen.jsx.map +1 -0
  348. package/lib/screens/auth/RegisterScreen.d.ts +14 -0
  349. package/lib/screens/auth/RegisterScreen.d.ts.map +1 -0
  350. package/lib/screens/auth/RegisterScreen.jsx +296 -0
  351. package/lib/screens/auth/RegisterScreen.jsx.map +1 -0
  352. package/lib/screens/auth/ResetPasswordScreen.d.ts +12 -0
  353. package/lib/screens/auth/ResetPasswordScreen.d.ts.map +1 -0
  354. package/lib/screens/auth/ResetPasswordScreen.jsx +276 -0
  355. package/lib/screens/auth/ResetPasswordScreen.jsx.map +1 -0
  356. package/lib/screens/auth/ServerConfigScreen.d.ts +18 -0
  357. package/lib/screens/auth/ServerConfigScreen.d.ts.map +1 -0
  358. package/lib/screens/auth/ServerConfigScreen.jsx +259 -0
  359. package/lib/screens/auth/ServerConfigScreen.jsx.map +1 -0
  360. package/lib/screens/auth/TermsOfServiceScreen.d.ts +30 -0
  361. package/lib/screens/auth/TermsOfServiceScreen.d.ts.map +1 -0
  362. package/lib/screens/auth/TermsOfServiceScreen.jsx +130 -0
  363. package/lib/screens/auth/TermsOfServiceScreen.jsx.map +1 -0
  364. package/lib/screens/auth/VerifyEmailScreen.d.ts +4 -0
  365. package/lib/screens/auth/VerifyEmailScreen.d.ts.map +1 -0
  366. package/lib/screens/auth/VerifyEmailScreen.jsx +249 -0
  367. package/lib/screens/auth/VerifyEmailScreen.jsx.map +1 -0
  368. package/lib/screens/auth/WelcomeScreen.d.ts +17 -0
  369. package/lib/screens/auth/WelcomeScreen.d.ts.map +1 -0
  370. package/lib/screens/auth/WelcomeScreen.jsx +58 -0
  371. package/lib/screens/auth/WelcomeScreen.jsx.map +1 -0
  372. package/lib/screens/auth/index.d.ts +19 -0
  373. package/lib/screens/auth/index.d.ts.map +1 -0
  374. package/lib/screens/auth/index.js +19 -0
  375. package/lib/screens/auth/index.js.map +1 -0
  376. package/lib/screens/index.d.ts +2 -0
  377. package/lib/screens/index.d.ts.map +1 -0
  378. package/lib/screens/index.js +2 -0
  379. package/lib/screens/index.js.map +1 -0
  380. package/lib/services/api-config.d.ts +87 -0
  381. package/lib/services/api-config.d.ts.map +1 -0
  382. package/lib/services/api-config.js +202 -0
  383. package/lib/services/api-config.js.map +1 -0
  384. package/lib/services/auth.service.d.ts +140 -0
  385. package/lib/services/auth.service.d.ts.map +1 -0
  386. package/lib/services/auth.service.js +376 -0
  387. package/lib/services/auth.service.js.map +1 -0
  388. package/lib/services/favicon.d.ts +46 -0
  389. package/lib/services/favicon.d.ts.map +1 -0
  390. package/lib/services/favicon.js +84 -0
  391. package/lib/services/favicon.js.map +1 -0
  392. package/lib/services/hibp-cache.d.ts +23 -0
  393. package/lib/services/hibp-cache.d.ts.map +1 -0
  394. package/lib/services/hibp-cache.js +56 -0
  395. package/lib/services/hibp-cache.js.map +1 -0
  396. package/lib/services/hibp.d.ts +28 -0
  397. package/lib/services/hibp.d.ts.map +1 -0
  398. package/lib/services/hibp.js +92 -0
  399. package/lib/services/hibp.js.map +1 -0
  400. package/lib/services/index.d.ts +8 -0
  401. package/lib/services/index.d.ts.map +1 -0
  402. package/lib/services/index.js +7 -0
  403. package/lib/services/index.js.map +1 -0
  404. package/lib/services/passkey.d.ts +10 -0
  405. package/lib/services/passkey.d.ts.map +1 -0
  406. package/lib/services/passkey.js +192 -0
  407. package/lib/services/passkey.js.map +1 -0
  408. package/lib/utils/api-fetch.d.ts +11 -0
  409. package/lib/utils/api-fetch.d.ts.map +1 -0
  410. package/lib/utils/api-fetch.js +52 -0
  411. package/lib/utils/api-fetch.js.map +1 -0
  412. package/lib/utils/clipboard.d.ts +31 -0
  413. package/lib/utils/clipboard.d.ts.map +1 -0
  414. package/lib/utils/clipboard.js +66 -0
  415. package/lib/utils/clipboard.js.map +1 -0
  416. package/lib/utils/cn.d.ts +9 -0
  417. package/lib/utils/cn.d.ts.map +1 -0
  418. package/lib/utils/cn.js +12 -0
  419. package/lib/utils/cn.js.map +1 -0
  420. package/lib/utils/debounce.d.ts +13 -0
  421. package/lib/utils/debounce.d.ts.map +1 -0
  422. package/lib/utils/debounce.js +28 -0
  423. package/lib/utils/debounce.js.map +1 -0
  424. package/lib/utils/error.d.ts +9 -0
  425. package/lib/utils/error.d.ts.map +1 -0
  426. package/lib/utils/error.js +11 -0
  427. package/lib/utils/error.js.map +1 -0
  428. package/lib/utils/fetch.d.ts +17 -0
  429. package/lib/utils/fetch.d.ts.map +1 -0
  430. package/lib/utils/fetch.js +29 -0
  431. package/lib/utils/fetch.js.map +1 -0
  432. package/lib/utils/http-error.d.ts +41 -0
  433. package/lib/utils/http-error.d.ts.map +1 -0
  434. package/lib/utils/http-error.js +63 -0
  435. package/lib/utils/http-error.js.map +1 -0
  436. package/lib/utils/index.d.ts +11 -0
  437. package/lib/utils/index.d.ts.map +1 -0
  438. package/lib/utils/index.js +11 -0
  439. package/lib/utils/index.js.map +1 -0
  440. package/lib/utils/palette.d.ts +46 -0
  441. package/lib/utils/palette.d.ts.map +1 -0
  442. package/lib/utils/palette.js +75 -0
  443. package/lib/utils/palette.js.map +1 -0
  444. package/lib/utils/password.d.ts +20 -0
  445. package/lib/utils/password.d.ts.map +1 -0
  446. package/lib/utils/password.js +51 -0
  447. package/lib/utils/password.js.map +1 -0
  448. package/lib/utils/settings-storage.d.ts +3 -0
  449. package/lib/utils/settings-storage.d.ts.map +1 -0
  450. package/lib/utils/settings-storage.js +62 -0
  451. package/lib/utils/settings-storage.js.map +1 -0
  452. package/lib/utils/validation.d.ts +29 -0
  453. package/lib/utils/validation.d.ts.map +1 -0
  454. package/lib/utils/validation.js +58 -0
  455. package/lib/utils/validation.js.map +1 -0
  456. package/package.json +204 -0
  457. package/src/components/__tests__/data-table.test.ts +94 -0
  458. package/src/components/__tests__/pagination.test.ts +73 -0
  459. package/src/components/__tests__/stat-card.test.ts +21 -0
  460. package/src/components/auth/LockOverlay.tsx +122 -0
  461. package/src/components/auth/PasswordStrengthMeter.tsx +96 -0
  462. package/src/components/auth/ProtectedRoute.tsx +91 -0
  463. package/src/components/auth/PwnedPasswordWarning.tsx +54 -0
  464. package/src/components/auth/index.ts +10 -0
  465. package/src/components/bottom-nav.tsx +99 -0
  466. package/src/components/button.tsx +144 -0
  467. package/src/components/card.tsx +82 -0
  468. package/src/components/checkbox.tsx +48 -0
  469. package/src/components/connection-settings.tsx +583 -0
  470. package/src/components/context-menu.tsx +95 -0
  471. package/src/components/data-table.tsx +223 -0
  472. package/src/components/dialog.tsx +376 -0
  473. package/src/components/dropdown-menu.tsx +120 -0
  474. package/src/components/entropy-boot-splash.tsx +114 -0
  475. package/src/components/entropy-mark-animated.tsx +260 -0
  476. package/src/components/entropy-mark-geometry.ts +11 -0
  477. package/src/components/entropy-mark-sequential.tsx +132 -0
  478. package/src/components/entropy-mark.tsx +135 -0
  479. package/src/components/error-boundary.tsx +70 -0
  480. package/src/components/index.ts +156 -0
  481. package/src/components/input.tsx +246 -0
  482. package/src/components/item-icon.tsx +111 -0
  483. package/src/components/label.tsx +25 -0
  484. package/src/components/legal-dialog.tsx +47 -0
  485. package/src/components/license-gate.tsx +28 -0
  486. package/src/components/logo.tsx +34 -0
  487. package/src/components/onboarding-wizard.tsx +353 -0
  488. package/src/components/pagination.tsx +154 -0
  489. package/src/components/password-generator.tsx +393 -0
  490. package/src/components/pin-pad.tsx +156 -0
  491. package/src/components/popover.tsx +38 -0
  492. package/src/components/profile/ProfileDialog.tsx +129 -0
  493. package/src/components/profile/ProfileTab.tsx +151 -0
  494. package/src/components/profile/SecurityTab.tsx +229 -0
  495. package/src/components/profile/index.ts +3 -0
  496. package/src/components/segmented-toggle.tsx +119 -0
  497. package/src/components/separator.tsx +41 -0
  498. package/src/components/server-settings.tsx +442 -0
  499. package/src/components/settings/AccountSection.tsx +76 -0
  500. package/src/components/settings/DangerZoneSection.tsx +37 -0
  501. package/src/components/settings/GeneralSection.tsx +39 -0
  502. package/src/components/settings/SupportSection.tsx +53 -0
  503. package/src/components/settings/index.ts +4 -0
  504. package/src/components/settings-row.tsx +126 -0
  505. package/src/components/skeleton.tsx +48 -0
  506. package/src/components/social-connections.tsx +55 -0
  507. package/src/components/stat-card.tsx +64 -0
  508. package/src/components/tag-chip.tsx +78 -0
  509. package/src/components/tag-multi-select.tsx +136 -0
  510. package/src/components/text.tsx +77 -0
  511. package/src/components/toast/ToastPortal.tsx +16 -0
  512. package/src/components/toast/ToastPortal.web.tsx +47 -0
  513. package/src/components/toast/index.ts +2 -0
  514. package/src/components/toast/toast-config.tsx +31 -0
  515. package/src/components/tooltip.tsx +30 -0
  516. package/src/components/totp-display.tsx +207 -0
  517. package/src/components/user-avatar.tsx +46 -0
  518. package/src/context/AppConfigContext.tsx +76 -0
  519. package/src/context/NavigationContext.tsx +75 -0
  520. package/src/context/NotificationContext.tsx +69 -0
  521. package/src/context/ThemeContext.tsx +83 -0
  522. package/src/context/__tests__/NavigationContext.test.tsx +53 -0
  523. package/src/context/index.ts +24 -0
  524. package/src/flags.ts +44 -0
  525. package/src/forms/form-error.tsx +20 -0
  526. package/src/forms/index.ts +2 -0
  527. package/src/forms/shake-field.tsx +52 -0
  528. package/src/hooks/__tests__/useUploadBlobUrl.test.tsx +77 -0
  529. package/src/hooks/index.ts +18 -0
  530. package/src/hooks/useAuth.tsx +567 -0
  531. package/src/hooks/useAutoLock.ts +110 -0
  532. package/src/hooks/useDebouncedValue.ts +32 -0
  533. package/src/hooks/useShakeAnimation.ts +42 -0
  534. package/src/hooks/useUploadBlobUrl.ts +75 -0
  535. package/src/index.ts +48 -0
  536. package/src/screens/auth/AccountPickerScreen.tsx +103 -0
  537. package/src/screens/auth/AuthScreen.tsx +941 -0
  538. package/src/screens/auth/ConsentScreen.tsx +118 -0
  539. package/src/screens/auth/ErrorScreen.tsx +96 -0
  540. package/src/screens/auth/ExpiredLinkScreen.tsx +87 -0
  541. package/src/screens/auth/ForgotPasswordScreen.tsx +182 -0
  542. package/src/screens/auth/ImportDataScreen.tsx +112 -0
  543. package/src/screens/auth/LoginScreen.tsx +484 -0
  544. package/src/screens/auth/MfaChallengeScreen.tsx +216 -0
  545. package/src/screens/auth/MfaEnrollScreen.tsx +388 -0
  546. package/src/screens/auth/PasskeyEnrollScreen.tsx +96 -0
  547. package/src/screens/auth/PrivacyPolicyScreen.tsx +249 -0
  548. package/src/screens/auth/RegisterScreen.tsx +430 -0
  549. package/src/screens/auth/ResetPasswordScreen.tsx +377 -0
  550. package/src/screens/auth/ServerConfigScreen.tsx +388 -0
  551. package/src/screens/auth/TermsOfServiceScreen.tsx +236 -0
  552. package/src/screens/auth/VerifyEmailScreen.tsx +310 -0
  553. package/src/screens/auth/WelcomeScreen.tsx +78 -0
  554. package/src/screens/auth/index.ts +36 -0
  555. package/src/screens/index.ts +1 -0
  556. package/src/services/__tests__/api-config.test.ts +67 -0
  557. package/src/services/__tests__/favicon.test.ts +88 -0
  558. package/src/services/__tests__/hibp-cache.test.ts +37 -0
  559. package/src/services/__tests__/hibp.test.ts +39 -0
  560. package/src/services/api-config.ts +260 -0
  561. package/src/services/auth.service.ts +498 -0
  562. package/src/services/favicon.ts +102 -0
  563. package/src/services/hibp-cache.ts +71 -0
  564. package/src/services/hibp.ts +120 -0
  565. package/src/services/index.ts +52 -0
  566. package/src/services/passkey.ts +213 -0
  567. package/src/types/external.d.ts +24 -0
  568. package/src/utils/__tests__/clipboard.test.ts +59 -0
  569. package/src/utils/__tests__/cn.test.ts +24 -0
  570. package/src/utils/__tests__/debounce.test.ts +42 -0
  571. package/src/utils/__tests__/error.test.ts +18 -0
  572. package/src/utils/__tests__/http-error.test.ts +33 -0
  573. package/src/utils/__tests__/palette.test.ts +22 -0
  574. package/src/utils/__tests__/password.test.ts +55 -0
  575. package/src/utils/__tests__/settings-storage.test.ts +15 -0
  576. package/src/utils/__tests__/validation.test.ts +52 -0
  577. package/src/utils/api-fetch.ts +61 -0
  578. package/src/utils/clipboard.ts +69 -0
  579. package/src/utils/cn.ts +12 -0
  580. package/src/utils/debounce.ts +33 -0
  581. package/src/utils/error.ts +10 -0
  582. package/src/utils/fetch.ts +33 -0
  583. package/src/utils/http-error.ts +81 -0
  584. package/src/utils/index.ts +23 -0
  585. package/src/utils/palette.ts +105 -0
  586. package/src/utils/password.ts +64 -0
  587. package/src/utils/settings-storage.ts +68 -0
  588. package/src/utils/validation.ts +71 -0
  589. package/styles/globals.css +262 -0
  590. 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&apos;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;