@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,941 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { KeyboardAvoidingView, Platform, ScrollView, View } from "react-native";
3
+ import { SafeAreaView } from "react-native-safe-area-context";
4
+ import Animated, {
5
+ Easing,
6
+ FadeInDown,
7
+ FadeOutUp,
8
+ LinearTransition,
9
+ runOnJS,
10
+ useAnimatedStyle,
11
+ useSharedValue,
12
+ withTiming,
13
+ } from "react-native-reanimated";
14
+ import { type Href } from "expo-router";
15
+ import { StatusBar } from "expo-status-bar";
16
+ import { Ionicons } from "@expo/vector-icons";
17
+ import { useColorScheme } from "nativewind";
18
+ import * as Haptics from "expo-haptics";
19
+ import { Heading, MutedText, Text } from "../../components/text";
20
+ import { Button, IconButton } from "../../components/button";
21
+ import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "../../components/card";
22
+ import { EmailInput, Input, PasswordInput } from "../../components/input";
23
+ import { Dialog, DialogButton, DialogFooter } from "../../components/dialog";
24
+ import { SeparatorWithText } from "../../components/separator";
25
+ import { LegalDialog } from "../../components/legal-dialog";
26
+ import { SegmentedToggle } from "../../components/segmented-toggle";
27
+ import PasswordStrengthMeter from "../../components/auth/PasswordStrengthMeter";
28
+ import PwnedPasswordWarning from "../../components/auth/PwnedPasswordWarning";
29
+ import ShakeField from "../../forms/shake-field";
30
+ import FormError from "../../forms/form-error";
31
+ import { useAuth } from "../../hooks/useAuth";
32
+ import { useShakeAnimation } from "../../hooks/useShakeAnimation";
33
+ import { ApiError } from "../../utils/http-error";
34
+ import { validateEmail } from "../../utils/validation";
35
+ import AuthService from "../../services/auth.service";
36
+ import { checkPasswordPwned, debounce, getPwnedMessage } from "../../services/hibp";
37
+ import { getCachedPolicy } from "../../services/hibp-cache";
38
+ import { useAppConfig } from "../../context/AppConfigContext";
39
+ import { useNavigation } from "../../context/NavigationContext";
40
+
41
+ const SCROLL_CONTENT_STYLE = { flexGrow: 1 } as const;
42
+ const DEFAULT_POLICY = { enabled: false, enforce: false, block_threshold: 0 };
43
+
44
+ type Mode = "signin" | "signup";
45
+ // welcome — logo + decoration + Get Started (the entry view)
46
+ // auth — the Sign In / Sign Up form
47
+ // forgot — inline password-reset request
48
+ type Stage = "welcome" | "auth" | "forgot";
49
+
50
+ type AnimatedViewProps = React.ComponentProps<typeof Animated.View>;
51
+
52
+ /**
53
+ * Reanimated config for the FIELD-level Sign In ↔ Sign Up toggle. (The
54
+ * welcome ↔ form stage swap is a fixed, shared-value fade-through — see the
55
+ * morph effect in the component.)
56
+ * • `layout` — eases the card height as fields appear/disappear.
57
+ * • `entering`/`exiting` — fields that mount/unmount (Sign In ↔ Sign Up).
58
+ */
59
+ export interface AuthFormTransition {
60
+ layout?: AnimatedViewProps["layout"];
61
+ entering?: AnimatedViewProps["entering"];
62
+ exiting?: AnimatedViewProps["exiting"];
63
+ }
64
+
65
+ const DEFAULT_TRANSITION: Required<AuthFormTransition> = {
66
+ layout: LinearTransition.duration(280),
67
+ entering: FadeInDown.duration(240),
68
+ exiting: FadeOutUp.duration(160),
69
+ };
70
+
71
+ // Stage morph timing — fade/scale out, swap the stage at zero opacity (so the
72
+ // centered→centered swap is invisible, no overlap/teleport), then fade/scale in.
73
+ const STAGE_OUT_MS = 200;
74
+ const STAGE_IN_MS = 360;
75
+
76
+ export interface AuthScreenProps {
77
+ /** Master-password field label. */
78
+ passwordLabel?: string;
79
+ /** Master-password field placeholder. */
80
+ passwordPlaceholder?: string;
81
+ /** Optional social-auth block, rendered under the primary CTA. */
82
+ socialAuth?: React.ReactNode;
83
+ /**
84
+ * Render the "Sign in with passkey" button + ceremony flow. Pass the
85
+ * result of `isPasskeySupported()` (only meaningful in sign-in mode).
86
+ */
87
+ passkeyEnabled?: boolean;
88
+ passkeyUnlockPlaceholder?: string;
89
+ passkeyUnlockTitle?: string;
90
+ passkeyUnlockDescription?: string;
91
+ /**
92
+ * Connection / server-settings dialog, folded into this single page (no
93
+ * route). When provided, a gear button is shown top-right that toggles it.
94
+ */
95
+ settings?: (props: { visible: boolean; onClose: () => void }) => React.ReactNode;
96
+ /** Override the animations. Merged over the defaults. */
97
+ transition?: AuthFormTransition;
98
+ /**
99
+ * App-specific hero rendered on the welcome view, under the logo (the home
100
+ * for each app's old welcome-screen animation — vault's PasswordDemoAnimation,
101
+ * simple-finance's AssetCarousel, …). Omit for none.
102
+ */
103
+ decoration?: React.ReactNode;
104
+ /** Welcome-view tagline/subtitle. Defaults to AppConfig `tagline`. */
105
+ welcomeCtaLabel?: string;
106
+ }
107
+
108
+ /**
109
+ * Single consolidated unauthenticated screen with three animated stages:
110
+ * a welcome view (logo + app decoration + Get Started), the Sign In / Sign Up
111
+ * form (with an animated mode toggle), and inline forgot-password. Terms/Privacy
112
+ * are themed popups and a connection-settings dialog can be folded in. Preserves
113
+ * the passkey ceremony + non-PRF unlock and the HIBP/strength checks.
114
+ */
115
+ export function AuthScreen({
116
+ passwordLabel = "Password",
117
+ passwordPlaceholder = "Enter your password",
118
+ socialAuth,
119
+ passkeyEnabled = false,
120
+ passkeyUnlockPlaceholder = "Enter your master password",
121
+ passkeyUnlockTitle = "Unlock your vault",
122
+ passkeyUnlockDescription = "Signed in successfully. This passkey can't unlock the vault on its own — enter your master password to decrypt your data.",
123
+ settings,
124
+ transition,
125
+ decoration,
126
+ welcomeCtaLabel = "Get Started",
127
+ }: AuthScreenProps = {}) {
128
+ const {
129
+ login: authLogin,
130
+ register: authRegister,
131
+ loginWithPasskey,
132
+ unlockWithPassword,
133
+ } = useAuth();
134
+ const router = useNavigation();
135
+ const { appName, Logo, tagline, routes, termsText, privacyText } = useAppConfig();
136
+
137
+ const { colorScheme } = useColorScheme();
138
+ const isDark = colorScheme === "dark";
139
+ const t = useMemo(() => ({ ...DEFAULT_TRANSITION, ...transition }), [transition]);
140
+
141
+ const [mode, setMode] = useState<Mode>("signin");
142
+ const [stage, setStage] = useState<Stage>("welcome");
143
+ const isSignup = mode === "signup";
144
+
145
+ // Stage morph: `stage` is the target; `displayStage` is what's rendered.
146
+ // On a stage change we fade/scale the content out, swap displayStage at zero
147
+ // opacity (so the centered-welcome → top-anchored-form layout change is
148
+ // invisible — no teleport, no overlap of the two states), then fade in.
149
+ const [displayStage, setDisplayStage] = useState<Stage>("welcome");
150
+ // Fade-through: `stage` is the target, `displayStage` is what's rendered. On
151
+ // a change the content fades/scales out, swaps at zero opacity (so the
152
+ // position change between stages is never seen), then fades back in.
153
+ const morph = useSharedValue(1);
154
+ useEffect(() => {
155
+ if (stage === displayStage) {
156
+ return;
157
+ }
158
+ morph.value = withTiming(
159
+ 0,
160
+ { duration: STAGE_OUT_MS, easing: Easing.in(Easing.cubic) },
161
+ (done) => {
162
+ if (done) {
163
+ runOnJS(setDisplayStage)(stage);
164
+ morph.value = withTiming(1, { duration: STAGE_IN_MS, easing: Easing.out(Easing.cubic) });
165
+ }
166
+ }
167
+ );
168
+ }, [stage, displayStage, morph]);
169
+ const morphStyle = useAnimatedStyle(() => ({
170
+ opacity: morph.value,
171
+ transform: [{ scale: 0.94 + 0.06 * morph.value }, { translateY: (1 - morph.value) * 8 }],
172
+ }));
173
+ const onWelcome = displayStage === "welcome";
174
+
175
+ const [email, setEmail] = useState("");
176
+ const [displayName, setDisplayName] = useState("");
177
+ const [password, setPassword] = useState("");
178
+ const [confirmPassword, setConfirmPassword] = useState("");
179
+ const [isLoading, setIsLoading] = useState(false);
180
+ const [errors, setErrors] = useState<{
181
+ email?: string;
182
+ displayName?: string;
183
+ password?: string;
184
+ confirmPassword?: string;
185
+ }>({});
186
+
187
+ const [legal, setLegal] = useState<null | "terms" | "privacy">(null);
188
+ const [settingsOpen, setSettingsOpen] = useState(false);
189
+
190
+ const [pwnedInfo, setPwnedInfo] = useState<{
191
+ title: string;
192
+ message: string;
193
+ severity: "error" | "warning" | "info";
194
+ count: number;
195
+ } | null>(null);
196
+
197
+ const [passkeyStep, setPasskeyStep] = useState<"idle" | "passkey-loading" | "passkey-unlock">(
198
+ "idle"
199
+ );
200
+ const [unlockPassword, setUnlockPassword] = useState("");
201
+ const [unlockError, setUnlockError] = useState("");
202
+ const [pendingOnboardingStep, setPendingOnboardingStep] = useState<string | null>(null);
203
+ const passkeyAbortRef = useRef<AbortController | null>(null);
204
+
205
+ const [forgotSent, setForgotSent] = useState(false);
206
+
207
+ const emailShake = useShakeAnimation();
208
+ const displayNameShake = useShakeAnimation();
209
+ const passwordShake = useShakeAnimation();
210
+ const confirmShake = useShakeAnimation();
211
+ const firstError =
212
+ errors.email ?? errors.displayName ?? errors.password ?? errors.confirmPassword;
213
+
214
+ useEffect(() => () => passkeyAbortRef.current?.abort(), []);
215
+
216
+ const checkPasswordDebounced = useMemo(
217
+ () =>
218
+ debounce(async (pass: string) => {
219
+ const policy = getCachedPolicy() ?? DEFAULT_POLICY;
220
+ if (!policy.enabled || pass.length < 8) {
221
+ setPwnedInfo(null);
222
+ return;
223
+ }
224
+ const result = await checkPasswordPwned(pass);
225
+ if (result.isPwned) {
226
+ setPwnedInfo({ ...getPwnedMessage(result), count: result.count });
227
+ } else {
228
+ setPwnedInfo(null);
229
+ }
230
+ }, 1000),
231
+ []
232
+ );
233
+ useEffect(() => () => checkPasswordDebounced.cancel(), [checkPasswordDebounced]);
234
+
235
+ const switchMode = useCallback((next: Mode) => {
236
+ setMode(next);
237
+ setErrors({});
238
+ setPwnedInfo(null);
239
+ }, []);
240
+
241
+ const clearField = (field: keyof typeof errors) => {
242
+ if (errors[field]) {
243
+ setErrors((e) => ({ ...e, [field]: undefined }));
244
+ }
245
+ };
246
+
247
+ const validate = () => {
248
+ const next: typeof errors = {};
249
+ if (!email) {
250
+ next.email = "Email is required";
251
+ } else if (!validateEmail(email)) {
252
+ next.email = "Please enter a valid email";
253
+ }
254
+
255
+ if (!password) {
256
+ next.password = "Password is required";
257
+ } else if (password.length < 8) {
258
+ next.password = "Password must be at least 8 characters";
259
+ }
260
+
261
+ if (isSignup) {
262
+ if (!displayName) {
263
+ next.displayName = "Display name is required";
264
+ } else if (displayName.length > 100) {
265
+ next.displayName = "Display name must be 100 characters or less";
266
+ }
267
+ if (!confirmPassword) {
268
+ next.confirmPassword = "Please confirm your password";
269
+ } else if (password !== confirmPassword) {
270
+ next.confirmPassword = "Passwords do not match";
271
+ }
272
+ }
273
+ return next;
274
+ };
275
+
276
+ const shakeFor = (next: typeof errors) => {
277
+ if (next.email) {
278
+ emailShake.shake();
279
+ }
280
+ if (next.displayName) {
281
+ displayNameShake.shake();
282
+ }
283
+ if (next.password) {
284
+ passwordShake.shake();
285
+ }
286
+ if (next.confirmPassword) {
287
+ confirmShake.shake();
288
+ }
289
+ };
290
+
291
+ const performLogin = async () => {
292
+ setIsLoading(true);
293
+ try {
294
+ await authLogin(email.trim().toLowerCase(), password);
295
+ setPassword("");
296
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
297
+ } catch (error) {
298
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
299
+ const message = error instanceof Error ? error.message : "Login failed";
300
+ const code = error instanceof ApiError ? error.code : undefined;
301
+ if (code === "INVALID_CREDENTIALS") {
302
+ setErrors({ password: "Invalid email or password" });
303
+ emailShake.shake();
304
+ passwordShake.shake();
305
+ } else if (code === "ACCOUNT_LOCKED") {
306
+ setErrors({ email: "Account is locked. Please contact support." });
307
+ emailShake.shake();
308
+ } else {
309
+ setErrors({ password: message });
310
+ passwordShake.shake();
311
+ }
312
+ } finally {
313
+ setIsLoading(false);
314
+ }
315
+ };
316
+
317
+ const performRegister = async () => {
318
+ setIsLoading(true);
319
+ const policy = getCachedPolicy() ?? DEFAULT_POLICY;
320
+ if (policy.enabled) {
321
+ const pwned = await checkPasswordPwned(password);
322
+ if (pwned.isPwned) {
323
+ setPwnedInfo({ ...getPwnedMessage(pwned), count: pwned.count });
324
+ if (policy.enforce && pwned.count > policy.block_threshold) {
325
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
326
+ passwordShake.shake();
327
+ confirmShake.shake();
328
+ setIsLoading(false);
329
+ return;
330
+ }
331
+ } else {
332
+ setPwnedInfo(null);
333
+ }
334
+ }
335
+ try {
336
+ const username = (email.split("@")[0] || "user").replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase();
337
+ await authRegister(email.trim().toLowerCase(), username, password, displayName.trim());
338
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
339
+ } catch (error) {
340
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
341
+ const message = error instanceof Error ? error.message : "Registration failed";
342
+ if (message.includes("email") && message.includes("exists")) {
343
+ setErrors({ email: "This email is already registered" });
344
+ emailShake.shake();
345
+ } else if (message.includes("username") && message.includes("exists")) {
346
+ setErrors({ email: "An account with this email already exists" });
347
+ emailShake.shake();
348
+ } else if (message.includes("password")) {
349
+ setErrors({ password: message });
350
+ passwordShake.shake();
351
+ } else {
352
+ setErrors({ email: message });
353
+ emailShake.shake();
354
+ }
355
+ } finally {
356
+ setIsLoading(false);
357
+ }
358
+ };
359
+
360
+ const handleSubmit = async () => {
361
+ if (isLoading) {
362
+ return;
363
+ }
364
+ const next = validate();
365
+ if (Object.keys(next).length > 0) {
366
+ setErrors(next);
367
+ shakeFor(next);
368
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
369
+ return;
370
+ }
371
+ setErrors({});
372
+ if (isSignup) {
373
+ await performRegister();
374
+ } else {
375
+ await performLogin();
376
+ }
377
+ };
378
+
379
+ const routeAfterUnlock = (onboardingStep?: string | null) => {
380
+ if (onboardingStep === "email_verification") {
381
+ router.replace({ pathname: routes.verifyEmail, params: { email } } as Href);
382
+ } else if (onboardingStep === "mfa_enrollment") {
383
+ router.replace(routes.mfaEnroll);
384
+ } else if (onboardingStep === "import") {
385
+ router.replace(routes.importData);
386
+ } else {
387
+ router.replace(routes.home);
388
+ }
389
+ };
390
+
391
+ const handlePasskeyLogin = async () => {
392
+ setPasskeyStep("passkey-loading");
393
+ setErrors({});
394
+ passkeyAbortRef.current?.abort();
395
+ const controller = new AbortController();
396
+ passkeyAbortRef.current = controller;
397
+ try {
398
+ const trimmed = email.trim().toLowerCase();
399
+ const result = await loginWithPasskey(
400
+ trimmed.length > 0 ? trimmed : undefined,
401
+ controller.signal
402
+ );
403
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
404
+ if (result.unlocked) {
405
+ setPasskeyStep("idle");
406
+ routeAfterUnlock(result.user.onboarding_step);
407
+ return;
408
+ }
409
+ setPendingOnboardingStep(result.user.onboarding_step ?? null);
410
+ setPasskeyStep("passkey-unlock");
411
+ } catch (error) {
412
+ const name = error instanceof Error ? error.name : undefined;
413
+ const benign = name === "NotAllowedError" || name === "AbortError";
414
+ if (!benign) {
415
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
416
+ setErrors({ password: error instanceof Error ? error.message : "Passkey sign-in failed" });
417
+ }
418
+ setPasskeyStep("idle");
419
+ }
420
+ };
421
+
422
+ const handleUnlockAfterPasskey = async () => {
423
+ if (!unlockPassword) {
424
+ setUnlockError("Master password is required");
425
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
426
+ return;
427
+ }
428
+ setIsLoading(true);
429
+ try {
430
+ await unlockWithPassword(unlockPassword);
431
+ setUnlockPassword("");
432
+ setUnlockError("");
433
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
434
+ routeAfterUnlock(pendingOnboardingStep ?? "complete");
435
+ setPasskeyStep("idle");
436
+ } catch (error) {
437
+ setUnlockError(
438
+ error instanceof Error ? error.message : "Failed to unlock the vault. Check your password."
439
+ );
440
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
441
+ } finally {
442
+ setIsLoading(false);
443
+ }
444
+ };
445
+
446
+ const handleForgot = async () => {
447
+ if (isLoading) {
448
+ return;
449
+ }
450
+ if (!email || !validateEmail(email)) {
451
+ setErrors({ email: "Please enter a valid email" });
452
+ emailShake.shake();
453
+ return;
454
+ }
455
+ setErrors({});
456
+ setIsLoading(true);
457
+ try {
458
+ await AuthService.forgotPassword(email.trim().toLowerCase());
459
+ setForgotSent(true);
460
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
461
+ } catch {
462
+ // Don't leak account existence — always show the same confirmation.
463
+ setForgotSent(true);
464
+ } finally {
465
+ setIsLoading(false);
466
+ }
467
+ };
468
+
469
+ const goAuth = () => {
470
+ setStage("auth");
471
+ setForgotSent(false);
472
+ setErrors({});
473
+ };
474
+
475
+ return (
476
+ <>
477
+ <SafeAreaView className="flex-1 bg-background">
478
+ <StatusBar style={isDark ? "light" : "dark"} />
479
+ <KeyboardAvoidingView
480
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
481
+ className="flex-1"
482
+ >
483
+ <ScrollView
484
+ className="flex-1"
485
+ contentContainerStyle={SCROLL_CONTENT_STYLE}
486
+ keyboardShouldPersistTaps="handled"
487
+ showsVerticalScrollIndicator={false}
488
+ >
489
+ <View className="flex-1 justify-between px-6">
490
+ {/* top bar: settings gear only — back lives inside the cards
491
+ (thumb-reachable) so users don't stretch to the corner. */}
492
+ <View className="mt-4 h-10 flex-row items-center justify-between">
493
+ <View />
494
+ {settings ? (
495
+ <IconButton
496
+ variant="ghost"
497
+ size="sm"
498
+ onPress={() => setSettingsOpen(true)}
499
+ accessibilityLabel="Connection settings"
500
+ accessibilityRole="button"
501
+ >
502
+ <Ionicons
503
+ name="settings-outline"
504
+ size={22}
505
+ color={isDark ? "#A3A3A3" : "#737373"}
506
+ />
507
+ </IconButton>
508
+ ) : (
509
+ <View />
510
+ )}
511
+ </View>
512
+
513
+ {/* One morph view driven by `morph`: dematerialize → swap
514
+ layout+content at zero opacity → materialize. Welcome is
515
+ centered; the form is top-anchored. Because the swap happens
516
+ while invisible, the position change is never seen (no
517
+ teleport) and the two stages never overlap. */}
518
+ <Animated.View
519
+ style={[
520
+ { flex: 1, width: "100%", alignItems: "center", justifyContent: "center" },
521
+ morphStyle,
522
+ ]}
523
+ >
524
+ <View className="mb-6 items-center">
525
+ <View className="flex-row items-center">
526
+ <Logo width={72} height={72} style={{ marginRight: -2 }} />
527
+ <Heading className="text-4xl">{appName}</Heading>
528
+ </View>
529
+ {tagline ? (
530
+ <Text className="mt-1 font-mono text-base text-muted-foreground">
531
+ {tagline}
532
+ </Text>
533
+ ) : null}
534
+ </View>
535
+
536
+ {onWelcome ? (
537
+ <View style={{ width: "100%", alignItems: "center" }}>
538
+ {decoration ? (
539
+ <View className="mb-6 w-full items-center">{decoration}</View>
540
+ ) : null}
541
+ <View style={{ width: "100%", maxWidth: 320 }}>
542
+ <Button
543
+ className="w-full"
544
+ onPress={() => setStage("auth")}
545
+ accessibilityLabel={welcomeCtaLabel}
546
+ accessibilityRole="button"
547
+ testID="welcome-get-started"
548
+ >
549
+ {welcomeCtaLabel}
550
+ </Button>
551
+ </View>
552
+ </View>
553
+ ) : (
554
+ <View style={{ width: "100%", alignItems: "center" }}>
555
+ <Card className="w-full max-w-[360px]">
556
+ {displayStage === "forgot" ? (
557
+ <>
558
+ <CardHeader>
559
+ <CardTitle>Reset password</CardTitle>
560
+ </CardHeader>
561
+ <CardContent className="gap-4">
562
+ {forgotSent ? (
563
+ <MutedText className="text-sm leading-5">
564
+ If an account exists for that email, we&apos;ve sent a link to reset
565
+ your password. Check your inbox.
566
+ </MutedText>
567
+ ) : (
568
+ <>
569
+ <MutedText className="text-sm leading-5">
570
+ Enter your email and we&apos;ll send you a link to reset your
571
+ password.
572
+ </MutedText>
573
+ <ShakeField shake={emailShake} label="Email">
574
+ <EmailInput
575
+ value={email}
576
+ onChangeText={(text) => {
577
+ setEmail(text);
578
+ clearField("email");
579
+ }}
580
+ autoFocus
581
+ placeholder="you@example.com"
582
+ autoComplete="email"
583
+ invalid={!!errors.email}
584
+ returnKeyType="done"
585
+ onSubmitEditing={handleForgot}
586
+ accessibilityLabel="Email address"
587
+ />
588
+ </ShakeField>
589
+ <FormError message={errors.email} />
590
+ </>
591
+ )}
592
+ </CardContent>
593
+ <CardFooter className="flex-col gap-3 border-t-0 pt-0">
594
+ {forgotSent ? (
595
+ <Button
596
+ className="w-full"
597
+ onPress={goAuth}
598
+ accessibilityLabel="Back to sign in"
599
+ >
600
+ Back to Sign In
601
+ </Button>
602
+ ) : (
603
+ <>
604
+ <Button
605
+ className="w-full"
606
+ onPress={handleForgot}
607
+ isLoading={isLoading}
608
+ accessibilityLabel="Send reset link"
609
+ >
610
+ Send reset link
611
+ </Button>
612
+ <Text
613
+ className="mt-1 text-center font-mono text-sm text-muted-foreground"
614
+ onPress={goAuth}
615
+ accessibilityRole="button"
616
+ accessibilityLabel="Back to sign in"
617
+ >
618
+ ← Back
619
+ </Text>
620
+ </>
621
+ )}
622
+ </CardFooter>
623
+ </>
624
+ ) : (
625
+ <>
626
+ <CardHeader className="gap-4">
627
+ <SegmentedToggle<Mode>
628
+ value={mode}
629
+ onChange={switchMode}
630
+ options={[
631
+ { key: "signin", label: "Sign In" },
632
+ { key: "signup", label: "Sign Up" },
633
+ ]}
634
+ />
635
+ <CardTitle>
636
+ {isSignup ? "Create your account" : `Sign in to ${appName}`}
637
+ </CardTitle>
638
+ </CardHeader>
639
+
640
+ {/* Card height eases as fields appear/disappear. */}
641
+ <Animated.View layout={t.layout}>
642
+ <CardContent className="gap-3 pb-3">
643
+ {!isSignup && passkeyEnabled ? (
644
+ <Animated.View
645
+ entering={t.entering}
646
+ exiting={t.exiting}
647
+ layout={t.layout}
648
+ >
649
+ <Button
650
+ variant="outline"
651
+ className="w-full"
652
+ onPress={handlePasskeyLogin}
653
+ isLoading={passkeyStep === "passkey-loading"}
654
+ accessibilityLabel="Sign in with passkey"
655
+ accessibilityRole="button"
656
+ testID="login-passkey-button"
657
+ >
658
+ <View className="flex-row items-center justify-center">
659
+ <Ionicons
660
+ name="key-outline"
661
+ size={20}
662
+ color={isDark ? "#FFFFFF" : "#000000"}
663
+ className="mr-2"
664
+ />
665
+ <Text className="text-base font-medium">
666
+ Sign in with passkey
667
+ </Text>
668
+ </View>
669
+ </Button>
670
+ <View className="mt-3">
671
+ <SeparatorWithText />
672
+ </View>
673
+ </Animated.View>
674
+ ) : null}
675
+
676
+ <ShakeField shake={emailShake} label="Email">
677
+ <EmailInput
678
+ value={email}
679
+ onChangeText={(text) => {
680
+ setEmail(text);
681
+ clearField("email");
682
+ }}
683
+ autoFocus
684
+ placeholder="you@example.com"
685
+ autoComplete="email"
686
+ invalid={!!errors.email}
687
+ accessibilityLabel="Email address"
688
+ testID="auth-email-input"
689
+ />
690
+ </ShakeField>
691
+
692
+ {isSignup ? (
693
+ <Animated.View
694
+ key="displayName"
695
+ entering={t.entering}
696
+ exiting={t.exiting}
697
+ layout={t.layout}
698
+ >
699
+ <ShakeField shake={displayNameShake} label="Display Name">
700
+ <Input
701
+ value={displayName}
702
+ onChangeText={(text) => {
703
+ setDisplayName(text);
704
+ clearField("displayName");
705
+ }}
706
+ placeholder="Your display name"
707
+ leftIcon={
708
+ <Ionicons
709
+ name="person-outline"
710
+ size={20}
711
+ color={isDark ? "#A3A3A3" : "#737373"}
712
+ />
713
+ }
714
+ autoCapitalize="words"
715
+ autoCorrect={false}
716
+ maxLength={100}
717
+ invalid={!!errors.displayName}
718
+ accessibilityLabel="Display name"
719
+ testID="auth-displayname-input"
720
+ />
721
+ </ShakeField>
722
+ </Animated.View>
723
+ ) : null}
724
+
725
+ <ShakeField shake={passwordShake} label={passwordLabel}>
726
+ <PasswordInput
727
+ value={password}
728
+ onChangeText={(text) => {
729
+ setPassword(text);
730
+ clearField("password");
731
+ if (isSignup) {
732
+ checkPasswordDebounced(text);
733
+ }
734
+ }}
735
+ placeholder={passwordPlaceholder}
736
+ autoComplete={isSignup ? "password-new" : "password"}
737
+ invalid={!!errors.password}
738
+ returnKeyType={isSignup ? "next" : "done"}
739
+ onSubmitEditing={isSignup ? undefined : handleSubmit}
740
+ accessibilityLabel={passwordLabel}
741
+ testID="auth-password-input"
742
+ />
743
+ </ShakeField>
744
+
745
+ {isSignup ? (
746
+ <Animated.View
747
+ key="strength"
748
+ entering={t.entering}
749
+ exiting={t.exiting}
750
+ layout={t.layout}
751
+ >
752
+ <PasswordStrengthMeter password={password} />
753
+ <PwnedPasswordWarning pwnedInfo={pwnedInfo} />
754
+ </Animated.View>
755
+ ) : null}
756
+
757
+ {isSignup ? (
758
+ <Animated.View
759
+ key="confirm"
760
+ entering={t.entering}
761
+ exiting={t.exiting}
762
+ layout={t.layout}
763
+ >
764
+ <ShakeField shake={confirmShake} label="Confirm Password">
765
+ <PasswordInput
766
+ value={confirmPassword}
767
+ onChangeText={(text) => {
768
+ setConfirmPassword(text);
769
+ clearField("confirmPassword");
770
+ }}
771
+ placeholder="Re-enter your password"
772
+ autoComplete="password-new"
773
+ returnKeyType="done"
774
+ onSubmitEditing={handleSubmit}
775
+ invalid={!!errors.confirmPassword}
776
+ accessibilityLabel="Confirm password"
777
+ testID="auth-confirm-password-input"
778
+ />
779
+ </ShakeField>
780
+ </Animated.View>
781
+ ) : (
782
+ <Animated.View
783
+ key="forgot"
784
+ entering={t.entering}
785
+ exiting={t.exiting}
786
+ layout={t.layout}
787
+ className="items-end"
788
+ >
789
+ <Text
790
+ className="font-mono text-sm underline"
791
+ onPress={() => {
792
+ setStage("forgot");
793
+ setForgotSent(false);
794
+ setErrors({});
795
+ }}
796
+ accessibilityRole="link"
797
+ accessibilityLabel="Forgot password"
798
+ >
799
+ Forgot password?
800
+ </Text>
801
+ </Animated.View>
802
+ )}
803
+ </CardContent>
804
+
805
+ <CardFooter className="flex-col gap-3 border-t-0 pt-0">
806
+ <FormError message={firstError} />
807
+
808
+ <Button
809
+ className="w-full"
810
+ onPress={handleSubmit}
811
+ isLoading={isLoading}
812
+ accessibilityLabel={isSignup ? "Create account" : "Sign in"}
813
+ accessibilityRole="button"
814
+ testID="auth-submit"
815
+ >
816
+ {isSignup ? "Create Account" : "Sign In"}
817
+ </Button>
818
+
819
+ {socialAuth ? (
820
+ <>
821
+ <SeparatorWithText />
822
+ {socialAuth}
823
+ </>
824
+ ) : null}
825
+
826
+ {/* In-card back — thumb-reachable, so users don't
827
+ have to stretch to the top-left corner arrow. */}
828
+ <Text
829
+ className="mt-1 text-center font-mono text-sm text-muted-foreground"
830
+ onPress={() => setStage("welcome")}
831
+ accessibilityRole="button"
832
+ accessibilityLabel="Back"
833
+ >
834
+ ← Back
835
+ </Text>
836
+ </CardFooter>
837
+ </Animated.View>
838
+ </>
839
+ )}
840
+ </Card>
841
+ </View>
842
+ )}
843
+ </Animated.View>
844
+
845
+ {/* legal footer — Terms/Privacy as popups, no routes */}
846
+ <View className="mb-6 mt-4">
847
+ <MutedText className="text-center text-xs leading-5">
848
+ By continuing, you agree to our{" "}
849
+ <Text
850
+ className="text-xs text-foreground underline"
851
+ onPress={() => setLegal("terms")}
852
+ accessibilityRole="link"
853
+ accessibilityLabel="Terms of Service"
854
+ >
855
+ Terms of Service
856
+ </Text>{" "}
857
+ and{" "}
858
+ <Text
859
+ className="text-xs text-foreground underline"
860
+ onPress={() => setLegal("privacy")}
861
+ accessibilityRole="link"
862
+ accessibilityLabel="Privacy Policy"
863
+ >
864
+ Privacy Policy
865
+ </Text>
866
+ </MutedText>
867
+ </View>
868
+ </View>
869
+ </ScrollView>
870
+ </KeyboardAvoidingView>
871
+ </SafeAreaView>
872
+
873
+ {/* Passkey unlock dialog — non-PRF authenticators. */}
874
+ <Dialog
875
+ visible={passkeyStep === "passkey-unlock"}
876
+ onClose={() => {
877
+ setPasskeyStep("idle");
878
+ setUnlockPassword("");
879
+ setUnlockError("");
880
+ }}
881
+ draggable
882
+ dismissOnBackdrop={false}
883
+ >
884
+ <View className="mb-5 items-center">
885
+ <View
886
+ className={`mb-4 h-14 w-14 items-center justify-center rounded-full ${isDark ? "bg-white/10" : "bg-black/5"}`}
887
+ >
888
+ <Ionicons name="lock-open-outline" size={28} color={isDark ? "#FFFFFF" : "#000000"} />
889
+ </View>
890
+ <Heading className="mb-2 text-xl">{passkeyUnlockTitle}</Heading>
891
+ <Text className="mb-3 text-center text-sm leading-5">{passkeyUnlockDescription}</Text>
892
+ <PasswordInput
893
+ value={unlockPassword}
894
+ onChangeText={(text) => {
895
+ setUnlockPassword(text);
896
+ if (unlockError) {
897
+ setUnlockError("");
898
+ }
899
+ }}
900
+ placeholder={passkeyUnlockPlaceholder}
901
+ error={unlockError}
902
+ autoComplete="password"
903
+ returnKeyType="done"
904
+ onSubmitEditing={handleUnlockAfterPasskey}
905
+ accessibilityLabel="Master password"
906
+ testID="login-passkey-unlock-input"
907
+ />
908
+ </View>
909
+ <DialogFooter style={{ justifyContent: "center" }}>
910
+ <DialogButton
911
+ variant="primary"
912
+ onPress={isLoading ? () => {} : handleUnlockAfterPasskey}
913
+ accessibilityLabel="Unlock"
914
+ accessibilityRole="button"
915
+ >
916
+ {isLoading ? "Unlocking…" : "Unlock"}
917
+ </DialogButton>
918
+ </DialogFooter>
919
+ </Dialog>
920
+
921
+ {/* Folded-in connection/server settings dialog. */}
922
+ {settings?.({ visible: settingsOpen, onClose: () => setSettingsOpen(false) })}
923
+
924
+ {/* Terms / Privacy popups. */}
925
+ <LegalDialog
926
+ visible={legal === "terms"}
927
+ onClose={() => setLegal(null)}
928
+ title="Terms of Service"
929
+ content={termsText}
930
+ />
931
+ <LegalDialog
932
+ visible={legal === "privacy"}
933
+ onClose={() => setLegal(null)}
934
+ title="Privacy Policy"
935
+ content={privacyText}
936
+ />
937
+ </>
938
+ );
939
+ }
940
+
941
+ export default AuthScreen;