@oxy.so/services 12.3.0 → 12.5.0

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 (187) hide show
  1. package/README.md +1 -1
  2. package/lib/commonjs/index.js +17 -2
  3. package/lib/commonjs/index.js.map +1 -1
  4. package/lib/commonjs/ui/components/authChooser/types.js +14 -8
  5. package/lib/commonjs/ui/components/authChooser/types.js.map +1 -1
  6. package/lib/commonjs/ui/components/signIn/OxyAuthenticatorPanel.js +11 -10
  7. package/lib/commonjs/ui/components/signIn/OxyAuthenticatorPanel.js.map +1 -1
  8. package/lib/commonjs/ui/components/signIn/OxyDeleteAccountPanel.js +19 -18
  9. package/lib/commonjs/ui/components/signIn/OxyDeleteAccountPanel.js.map +1 -1
  10. package/lib/commonjs/ui/components/signIn/OxyEmailPanel.js +251 -0
  11. package/lib/commonjs/ui/components/signIn/OxyEmailPanel.js.map +1 -0
  12. package/lib/commonjs/ui/components/signIn/OxyLinkCommonsPanel.js +40 -33
  13. package/lib/commonjs/ui/components/signIn/OxyLinkCommonsPanel.js.map +1 -1
  14. package/lib/commonjs/ui/components/signIn/OxyPasswordPanel.js +51 -19
  15. package/lib/commonjs/ui/components/signIn/OxyPasswordPanel.js.map +1 -1
  16. package/lib/commonjs/ui/components/signIn/OxySignInSecurityPanel.js +154 -0
  17. package/lib/commonjs/ui/components/signIn/OxySignInSecurityPanel.js.map +1 -0
  18. package/lib/commonjs/ui/components/signIn/ReauthStep.js +6 -4
  19. package/lib/commonjs/ui/components/signIn/ReauthStep.js.map +1 -1
  20. package/lib/commonjs/ui/components/signIn/SessionsSection.js +200 -0
  21. package/lib/commonjs/ui/components/signIn/SessionsSection.js.map +1 -0
  22. package/lib/commonjs/ui/components/signIn/accountFlowParts.js +6 -0
  23. package/lib/commonjs/ui/components/signIn/accountFlowParts.js.map +1 -1
  24. package/lib/commonjs/ui/components/signIn/accountSecurityState.js +74 -0
  25. package/lib/commonjs/ui/components/signIn/accountSecurityState.js.map +1 -0
  26. package/lib/commonjs/ui/navigation/routes.js +1 -1
  27. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  28. package/lib/commonjs/ui/navigation/screenComponents.js +4 -0
  29. package/lib/commonjs/ui/navigation/screenComponents.js.map +1 -1
  30. package/lib/commonjs/ui/navigation/surfaceRegistry.js.map +1 -1
  31. package/lib/commonjs/ui/screens/AccountSecurityPanelScreen.js +1 -1
  32. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +3 -12
  33. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  34. package/lib/commonjs/ui/screens/CreateAccountScreen.js +3 -12
  35. package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -1
  36. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +7 -3
  37. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
  38. package/lib/commonjs/ui/screens/ManageAccountScreen.js +8 -196
  39. package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
  40. package/lib/commonjs/ui/screens/SignInEmailScreen.js +19 -0
  41. package/lib/commonjs/ui/screens/SignInEmailScreen.js.map +1 -0
  42. package/lib/commonjs/ui/screens/SignInSecurityScreen.js +30 -0
  43. package/lib/commonjs/ui/screens/SignInSecurityScreen.js.map +1 -0
  44. package/lib/module/index.js +5 -2
  45. package/lib/module/index.js.map +1 -1
  46. package/lib/module/ui/components/authChooser/types.js +15 -8
  47. package/lib/module/ui/components/authChooser/types.js.map +1 -1
  48. package/lib/module/ui/components/signIn/OxyAuthenticatorPanel.js +12 -11
  49. package/lib/module/ui/components/signIn/OxyAuthenticatorPanel.js.map +1 -1
  50. package/lib/module/ui/components/signIn/OxyDeleteAccountPanel.js +20 -19
  51. package/lib/module/ui/components/signIn/OxyDeleteAccountPanel.js.map +1 -1
  52. package/lib/module/ui/components/signIn/OxyEmailPanel.js +246 -0
  53. package/lib/module/ui/components/signIn/OxyEmailPanel.js.map +1 -0
  54. package/lib/module/ui/components/signIn/OxyLinkCommonsPanel.js +40 -33
  55. package/lib/module/ui/components/signIn/OxyLinkCommonsPanel.js.map +1 -1
  56. package/lib/module/ui/components/signIn/OxyPasswordPanel.js +52 -20
  57. package/lib/module/ui/components/signIn/OxyPasswordPanel.js.map +1 -1
  58. package/lib/module/ui/components/signIn/OxySignInSecurityPanel.js +149 -0
  59. package/lib/module/ui/components/signIn/OxySignInSecurityPanel.js.map +1 -0
  60. package/lib/module/ui/components/signIn/ReauthStep.js +6 -4
  61. package/lib/module/ui/components/signIn/ReauthStep.js.map +1 -1
  62. package/lib/module/ui/components/signIn/SessionsSection.js +194 -0
  63. package/lib/module/ui/components/signIn/SessionsSection.js.map +1 -0
  64. package/lib/module/ui/components/signIn/accountFlowParts.js +6 -0
  65. package/lib/module/ui/components/signIn/accountFlowParts.js.map +1 -1
  66. package/lib/module/ui/components/signIn/accountSecurityState.js +69 -0
  67. package/lib/module/ui/components/signIn/accountSecurityState.js.map +1 -0
  68. package/lib/module/ui/navigation/routes.js +1 -1
  69. package/lib/module/ui/navigation/routes.js.map +1 -1
  70. package/lib/module/ui/navigation/screenComponents.js +4 -0
  71. package/lib/module/ui/navigation/screenComponents.js.map +1 -1
  72. package/lib/module/ui/navigation/surfaceRegistry.js.map +1 -1
  73. package/lib/module/ui/screens/AccountSecurityPanelScreen.js +1 -1
  74. package/lib/module/ui/screens/AccountSettingsScreen.js +4 -13
  75. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  76. package/lib/module/ui/screens/CreateAccountScreen.js +3 -13
  77. package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -1
  78. package/lib/module/ui/screens/EditProfileFieldScreen.js +8 -4
  79. package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
  80. package/lib/module/ui/screens/ManageAccountScreen.js +9 -197
  81. package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
  82. package/lib/module/ui/screens/SignInEmailScreen.js +17 -0
  83. package/lib/module/ui/screens/SignInEmailScreen.js.map +1 -0
  84. package/lib/module/ui/screens/SignInSecurityScreen.js +26 -0
  85. package/lib/module/ui/screens/SignInSecurityScreen.js.map +1 -0
  86. package/lib/typescript/commonjs/index.d.ts +4 -0
  87. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts +1 -3
  89. package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/ui/components/signIn/OxyAuthenticatorPanel.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/ui/components/signIn/OxyDeleteAccountPanel.d.ts +3 -2
  92. package/lib/typescript/commonjs/ui/components/signIn/OxyDeleteAccountPanel.d.ts.map +1 -1
  93. package/lib/typescript/commonjs/ui/components/signIn/OxyEmailPanel.d.ts +21 -0
  94. package/lib/typescript/commonjs/ui/components/signIn/OxyEmailPanel.d.ts.map +1 -0
  95. package/lib/typescript/commonjs/ui/components/signIn/OxyLinkCommonsPanel.d.ts +2 -1
  96. package/lib/typescript/commonjs/ui/components/signIn/OxyLinkCommonsPanel.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/ui/components/signIn/OxyPasswordPanel.d.ts +5 -4
  98. package/lib/typescript/commonjs/ui/components/signIn/OxyPasswordPanel.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/ui/components/signIn/OxySignInSecurityPanel.d.ts +25 -0
  100. package/lib/typescript/commonjs/ui/components/signIn/OxySignInSecurityPanel.d.ts.map +1 -0
  101. package/lib/typescript/commonjs/ui/components/signIn/ReauthStep.d.ts +8 -2
  102. package/lib/typescript/commonjs/ui/components/signIn/ReauthStep.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/ui/components/signIn/SessionsSection.d.ts +9 -0
  104. package/lib/typescript/commonjs/ui/components/signIn/SessionsSection.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/ui/components/signIn/accountFlowParts.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/ui/components/signIn/accountSecurityState.d.ts +26 -0
  107. package/lib/typescript/commonjs/ui/components/signIn/accountSecurityState.d.ts.map +1 -0
  108. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +3 -2
  109. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
  111. package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/ui/navigation/screenComponents.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts +2 -0
  114. package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts.map +1 -1
  115. package/lib/typescript/commonjs/ui/screens/AccountSecurityPanelScreen.d.ts +1 -1
  116. package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  117. package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  118. package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts +3 -2
  120. package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  121. package/lib/typescript/commonjs/ui/screens/SignInEmailScreen.d.ts +6 -0
  122. package/lib/typescript/commonjs/ui/screens/SignInEmailScreen.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/ui/screens/SignInSecurityScreen.d.ts +6 -0
  124. package/lib/typescript/commonjs/ui/screens/SignInSecurityScreen.d.ts.map +1 -0
  125. package/lib/typescript/module/index.d.ts +4 -0
  126. package/lib/typescript/module/index.d.ts.map +1 -1
  127. package/lib/typescript/module/ui/components/authChooser/types.d.ts +1 -3
  128. package/lib/typescript/module/ui/components/authChooser/types.d.ts.map +1 -1
  129. package/lib/typescript/module/ui/components/signIn/OxyAuthenticatorPanel.d.ts.map +1 -1
  130. package/lib/typescript/module/ui/components/signIn/OxyDeleteAccountPanel.d.ts +3 -2
  131. package/lib/typescript/module/ui/components/signIn/OxyDeleteAccountPanel.d.ts.map +1 -1
  132. package/lib/typescript/module/ui/components/signIn/OxyEmailPanel.d.ts +21 -0
  133. package/lib/typescript/module/ui/components/signIn/OxyEmailPanel.d.ts.map +1 -0
  134. package/lib/typescript/module/ui/components/signIn/OxyLinkCommonsPanel.d.ts +2 -1
  135. package/lib/typescript/module/ui/components/signIn/OxyLinkCommonsPanel.d.ts.map +1 -1
  136. package/lib/typescript/module/ui/components/signIn/OxyPasswordPanel.d.ts +5 -4
  137. package/lib/typescript/module/ui/components/signIn/OxyPasswordPanel.d.ts.map +1 -1
  138. package/lib/typescript/module/ui/components/signIn/OxySignInSecurityPanel.d.ts +25 -0
  139. package/lib/typescript/module/ui/components/signIn/OxySignInSecurityPanel.d.ts.map +1 -0
  140. package/lib/typescript/module/ui/components/signIn/ReauthStep.d.ts +8 -2
  141. package/lib/typescript/module/ui/components/signIn/ReauthStep.d.ts.map +1 -1
  142. package/lib/typescript/module/ui/components/signIn/SessionsSection.d.ts +9 -0
  143. package/lib/typescript/module/ui/components/signIn/SessionsSection.d.ts.map +1 -0
  144. package/lib/typescript/module/ui/components/signIn/accountFlowParts.d.ts.map +1 -1
  145. package/lib/typescript/module/ui/components/signIn/accountSecurityState.d.ts +26 -0
  146. package/lib/typescript/module/ui/components/signIn/accountSecurityState.d.ts.map +1 -0
  147. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +3 -2
  148. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  149. package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
  150. package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
  151. package/lib/typescript/module/ui/navigation/screenComponents.d.ts.map +1 -1
  152. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts +2 -0
  153. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts.map +1 -1
  154. package/lib/typescript/module/ui/screens/AccountSecurityPanelScreen.d.ts +1 -1
  155. package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  156. package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  157. package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  158. package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts +3 -2
  159. package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  160. package/lib/typescript/module/ui/screens/SignInEmailScreen.d.ts +6 -0
  161. package/lib/typescript/module/ui/screens/SignInEmailScreen.d.ts.map +1 -0
  162. package/lib/typescript/module/ui/screens/SignInSecurityScreen.d.ts +6 -0
  163. package/lib/typescript/module/ui/screens/SignInSecurityScreen.d.ts.map +1 -0
  164. package/package.json +6 -6
  165. package/src/index.ts +7 -2
  166. package/src/ui/components/authChooser/__tests__/profileColors.test.ts +13 -0
  167. package/src/ui/components/authChooser/types.ts +16 -15
  168. package/src/ui/components/signIn/OxyAuthenticatorPanel.tsx +11 -8
  169. package/src/ui/components/signIn/OxyDeleteAccountPanel.tsx +15 -16
  170. package/src/ui/components/signIn/OxyEmailPanel.tsx +258 -0
  171. package/src/ui/components/signIn/OxyLinkCommonsPanel.tsx +27 -15
  172. package/src/ui/components/signIn/OxyPasswordPanel.tsx +49 -14
  173. package/src/ui/components/signIn/OxySignInSecurityPanel.tsx +147 -0
  174. package/src/ui/components/signIn/ReauthStep.tsx +13 -4
  175. package/src/ui/components/signIn/SessionsSection.tsx +218 -0
  176. package/src/ui/components/signIn/accountFlowParts.tsx +6 -0
  177. package/src/ui/components/signIn/accountSecurityState.tsx +75 -0
  178. package/src/ui/navigation/routes.ts +5 -1
  179. package/src/ui/navigation/screenComponents.ts +4 -0
  180. package/src/ui/navigation/surfaceRegistry.ts +2 -0
  181. package/src/ui/screens/AccountSecurityPanelScreen.tsx +1 -1
  182. package/src/ui/screens/AccountSettingsScreen.tsx +10 -29
  183. package/src/ui/screens/CreateAccountScreen.tsx +9 -27
  184. package/src/ui/screens/EditProfileFieldScreen.tsx +10 -8
  185. package/src/ui/screens/ManageAccountScreen.tsx +8 -241
  186. package/src/ui/screens/SignInEmailScreen.tsx +13 -0
  187. package/src/ui/screens/SignInSecurityScreen.tsx +19 -0
@@ -0,0 +1,13 @@
1
+ import { resolveAccentHex } from '../types';
2
+ import { APP_COLOR_PRESETS } from '@oxy.so/bloom/color-presets';
3
+
4
+ jest.mock('@oxy.so/bloom/theme', () => jest.requireActual('@oxy.so/bloom/color-presets'));
5
+
6
+ it('preserves the account identity for every app using the shared chooser', () => {
7
+ expect(resolveAccentHex('grove', '#fff')).toBe(APP_COLOR_PRESETS.grove.hex);
8
+ expect(resolveAccentHex('amber', '#fff')).toBe(APP_COLOR_PRESETS.yellow.hex);
9
+ expect(resolveAccentHex('#ABC', '#fff')).toBe('#aabbcc');
10
+ expect(resolveAccentHex('#123456', '#fff')).toBe('#123456');
11
+ expect(resolveAccentHex(null, '#fff')).toBe('#fff');
12
+ expect(resolveAccentHex('unknown', '#fff')).toBe('#fff');
13
+ });
@@ -1,3 +1,4 @@
1
+ import { APP_COLOR_NAMES } from '@oxy.so/bloom/color-presets';
1
2
  /**
2
3
  * Shared types + per-account color resolution for the auth chooser's views.
3
4
  *
@@ -8,12 +9,7 @@
8
9
  */
9
10
 
10
11
  import type { AccountStorageUsageResponse } from '@oxy.so/core';
11
- import {
12
- APP_COLOR_NAMES,
13
- APP_COLOR_PRESETS,
14
- type AppColorName,
15
- type useTheme,
16
- } from '@oxy.so/bloom/theme';
12
+ import { APP_COLOR_PRESETS, type useTheme } from '@oxy.so/bloom/theme';
17
13
  import type { AccountDialogMenuSection } from '../../navigation/accountDialogManager';
18
14
  import type { useI18n } from '../../hooks/useI18n';
19
15
 
@@ -132,14 +128,19 @@ export interface SignInAlternatives {
132
128
  * renders in EVERY consumer regardless of NativeWind availability.
133
129
  */
134
130
  export function resolveAccentHex(color: string | null, fallback: string): string {
135
- const preset = toPreset(color);
136
- return preset ? APP_COLOR_PRESETS[preset].hex : fallback;
137
- }
131
+ const normalized = color?.trim().toLowerCase();
132
+ if (!normalized) return fallback;
133
+
134
+ if (/^#[0-9a-f]{3}$/i.test(normalized)) {
135
+ return `#${normalized[1]}${normalized[1]}${normalized[2]}${normalized[2]}${normalized[3]}${normalized[3]}`;
136
+ }
137
+ if (/^#[0-9a-f]{6}$/i.test(normalized)) return normalized;
138
138
 
139
- /** Narrow a stored color string to a known `AppColorName`, or `undefined`. */
140
- export function toPreset(color: string | null): AppColorName | undefined {
141
- if (!color) return undefined;
142
- return (APP_COLOR_NAMES as readonly string[]).includes(color)
143
- ? (color as AppColorName)
144
- : undefined;
139
+ // `amber` was the retired name for `yellow`; existing user records are
140
+ // migrated, but inboxes can still render cached account data during rollout.
141
+ const preset = normalized === 'amber' ? 'yellow' : normalized;
142
+ if (APP_COLOR_NAMES.includes(preset as (typeof APP_COLOR_NAMES)[number])) {
143
+ return APP_COLOR_PRESETS[preset as (typeof APP_COLOR_NAMES)[number]].hex;
144
+ }
145
+ return fallback;
145
146
  }
@@ -23,12 +23,11 @@ import type { TotpEnrollResponse } from '@oxy.so/contracts';
23
23
  import { TOTP_DIGITS } from '@oxy.so/contracts/client';
24
24
  import { useQueryClient } from '@tanstack/react-query';
25
25
  import { useOxy } from '../../context/oxyRuntimeContext';
26
- import { useSignInMethods } from '../../hooks/queries/useAuthMethods';
27
26
  import { queryKeys } from '../../hooks/queries/queryKeys';
28
27
  import { useI18n } from '../../hooks/useI18n';
29
28
  import { copyText } from '../../utils/clipboard';
30
29
  import { SubtleLink } from '../authChooser/primitives';
31
- import { OxyAuthLoading, OxyAuthScreen, OxyAuthScreenHeader } from './OxyAuthScreen';
30
+ import { OxyAuthScreen, OxyAuthScreenHeader } from './OxyAuthScreen';
32
31
  import { ReauthStep } from './ReauthStep';
33
32
  import {
34
33
  AccountFlowAction,
@@ -38,6 +37,7 @@ import {
38
37
  AccountFlowProgress,
39
38
  describeSignInError,
40
39
  } from './accountFlowParts';
40
+ import { accountSecurityFallback, useAccountSecurityState } from './accountSecurityState';
41
41
 
42
42
  /** High-contrast, un-themed on purpose: scan reliability. */
43
43
  const QR_PLATE_BG = '#FFFFFF';
@@ -60,10 +60,9 @@ export interface OxyAuthenticatorPanelProps {
60
60
  export const OxyAuthenticatorPanel: React.FC<OxyAuthenticatorPanelProps> = ({ onDone }) => {
61
61
  const theme = useTheme();
62
62
  const { t } = useI18n();
63
- const { oxyServices, user } = useOxy();
63
+ const { oxyServices } = useOxy();
64
64
  const queryClient = useQueryClient();
65
- const keyed = Boolean(user?.publicKey);
66
- const methods = useSignInMethods({ enabled: !keyed });
65
+ const security = useAccountSecurityState();
67
66
  const [step, setStep] = useState<Step>({ name: 'status' });
68
67
  const [code, setCode] = useState('');
69
68
  const [pending, setPending] = useState(false);
@@ -72,7 +71,7 @@ export const OxyAuthenticatorPanel: React.FC<OxyAuthenticatorPanelProps> = ({ on
72
71
  const refresh = () =>
73
72
  void queryClient.invalidateQueries({ queryKey: queryKeys.signInMethods.all });
74
73
 
75
- if (keyed) {
74
+ if (security.keyed) {
76
75
  return (
77
76
  <OxyAuthScreen>
78
77
  <OxyAuthScreenHeader
@@ -82,8 +81,9 @@ export const OxyAuthenticatorPanel: React.FC<OxyAuthenticatorPanelProps> = ({ on
82
81
  </OxyAuthScreen>
83
82
  );
84
83
  }
85
- if (!methods.data) return <OxyAuthLoading />;
86
- const { hasPassword, totpEnabled, backupCodesRemaining } = methods.data;
84
+ const fallback = accountSecurityFallback(security, t('signInSecurity.totp.title'), t);
85
+ if (fallback || !security.methods.data) return fallback;
86
+ const { hasEmail, hasPassword, totpEnabled, backupCodesRemaining } = security.methods.data;
87
87
 
88
88
  const setUp = () => {
89
89
  if (pending) return;
@@ -157,6 +157,7 @@ export const OxyAuthenticatorPanel: React.FC<OxyAuthenticatorPanelProps> = ({ on
157
157
  description={t('signInSecurity.totp.scan')}
158
158
  action="totp"
159
159
  allowPassword={hasPassword}
160
+ allowEmail={hasEmail}
160
161
  totpEnabled={false}
161
162
  submitLabel={t('signInSecurity.totp.enable')}
162
163
  validate={() =>
@@ -213,6 +214,7 @@ export const OxyAuthenticatorPanel: React.FC<OxyAuthenticatorPanelProps> = ({ on
213
214
  description={t('signInSecurity.totp.regenerateDescription')}
214
215
  action="totp"
215
216
  allowPassword={hasPassword}
217
+ allowEmail={hasEmail}
216
218
  totpEnabled
217
219
  submitLabel={t('signInSecurity.totp.regenerate')}
218
220
  onSubmit={async (reauth) => {
@@ -230,6 +232,7 @@ export const OxyAuthenticatorPanel: React.FC<OxyAuthenticatorPanelProps> = ({ on
230
232
  description={t('signInSecurity.totp.description')}
231
233
  action="totp"
232
234
  allowPassword={hasPassword}
235
+ allowEmail={hasEmail}
233
236
  totpEnabled
234
237
  submitLabel={t('signInSecurity.totp.disable')}
235
238
  destructive
@@ -2,8 +2,9 @@
2
2
  * Deleting an account that has no key of its own, from the account's settings
3
3
  * (the account dialog's "Manage your account", or the Accounts app):
4
4
  *
5
- * type the username → a code sent to the account's email (+ the
6
- * authenticator's code when it has one) → deleted, and signed out
5
+ * type the username → a code sent to the account's email, or its password
6
+ * when it has no email (+ the authenticator's code when it has one) →
7
+ * deleted, and signed out
7
8
  *
8
9
  * An account with a Commons key is deleted with that key — in Commons, or in
9
10
  * an app that holds it (`ManageAccountScreen`); this panel says so instead.
@@ -14,11 +15,11 @@ import type React from 'react';
14
15
  import { useState } from 'react';
15
16
  import { surfaces } from '@oxy.so/bloom/surfaces';
16
17
  import { useOxy } from '../../context/oxyRuntimeContext';
17
- import { useSignInMethods } from '../../hooks/queries/useAuthMethods';
18
18
  import { useI18n } from '../../hooks/useI18n';
19
- import { OxyAuthLoading, OxyAuthScreen, OxyAuthScreenHeader } from './OxyAuthScreen';
19
+ import { OxyAuthScreen, OxyAuthScreenHeader } from './OxyAuthScreen';
20
20
  import { ReauthStep } from './ReauthStep';
21
21
  import { AccountFlowField, AccountFlowNotice } from './accountFlowParts';
22
+ import { accountSecurityFallback, useAccountSecurityState } from './accountSecurityState';
22
23
 
23
24
  export interface OxyDeleteAccountPanelProps {
24
25
  /** The account is gone and this origin signed out of it. */
@@ -33,8 +34,7 @@ export const OxyDeleteAccountPanel: React.FC<OxyDeleteAccountPanelProps> = ({
33
34
  }) => {
34
35
  const { t } = useI18n();
35
36
  const { user, oxyServices, logout } = useOxy();
36
- const keyed = Boolean(user?.publicKey);
37
- const methods = useSignInMethods({ enabled: !keyed });
37
+ const security = useAccountSecurityState();
38
38
  const [confirmText, setConfirmText] = useState('');
39
39
  const [deleted, setDeleted] = useState(false);
40
40
 
@@ -49,7 +49,7 @@ export const OxyDeleteAccountPanel: React.FC<OxyDeleteAccountPanelProps> = ({
49
49
  );
50
50
  }
51
51
 
52
- if (keyed) {
52
+ if (security.keyed) {
53
53
  return (
54
54
  <OxyAuthScreen>
55
55
  <OxyAuthScreenHeader title={t('deleteAccount.handoff.commonsTitle')} />
@@ -60,7 +60,9 @@ export const OxyDeleteAccountPanel: React.FC<OxyDeleteAccountPanelProps> = ({
60
60
  );
61
61
  }
62
62
 
63
- if (!methods.data) return <OxyAuthLoading />;
63
+ const fallback = accountSecurityFallback(security, t('deleteAccount.title'), t);
64
+ if (fallback || !security.methods.data) return fallback;
65
+ const { hasEmail, totpEnabled } = security.methods.data;
64
66
 
65
67
  const username = user?.username ?? '';
66
68
 
@@ -69,14 +71,16 @@ export const OxyDeleteAccountPanel: React.FC<OxyDeleteAccountPanelProps> = ({
69
71
  title={t('deleteAccount.title')}
70
72
  description={t('deleteAccount.keyless.subtitle', { username })}
71
73
  action="delete_account"
72
- totpEnabled={methods.data.totpEnabled}
74
+ // The mailbox proves it when there is one; the password only without.
75
+ allowPassword={!hasEmail}
76
+ allowEmail={hasEmail}
77
+ totpEnabled={totpEnabled}
73
78
  submitLabel={t('deleteAccount.keyless.action')}
74
79
  destructive
75
80
  validate={() =>
76
81
  confirmText.trim() === username ? null : t('deleteAccount.confirmLabel', { username })
77
82
  }
78
83
  onSubmit={async (proof) => {
79
- if (!proof.emailCode) throw new Error(t('reauth.errors.invalid'));
80
84
  // The last word before it is gone: the proof is ready, and nothing has
81
85
  // been sent yet. Cancelling keeps the screen as it is, code and all.
82
86
  const confirmed = await surfaces.confirm({
@@ -87,12 +91,7 @@ export const OxyDeleteAccountPanel: React.FC<OxyDeleteAccountPanelProps> = ({
87
91
  destructive: true,
88
92
  });
89
93
  if (!confirmed) return;
90
- await oxyServices.users.deleteMe(confirmText.trim(), {
91
- reauth: {
92
- emailCode: proof.emailCode,
93
- ...(proof.totpCode ? { totpCode: proof.totpCode } : {}),
94
- },
95
- });
94
+ await oxyServices.users.deleteMe(confirmText.trim(), { reauth: proof });
96
95
  await logout().catch(() => undefined);
97
96
  setDeleted(true);
98
97
  onDeleted?.();
@@ -0,0 +1,258 @@
1
+ /**
2
+ * The account's email, from its settings: add one, change it, or remove it.
3
+ *
4
+ * add / change: the new address → a code sent to IT, plus the person's
5
+ * proof (the password, or a code to the current email; + the
6
+ * authenticator's) → saved, and every other session signed out
7
+ *
8
+ * remove: only with a password set, confirmed with that password (+ the
9
+ * authenticator's) → the account signs in, and is recovered, with its
10
+ * password alone
11
+ *
12
+ * Sign-in codes, links and security notices go to this address. An account
13
+ * with a Commons key has no email; this panel says so instead.
14
+ */
15
+
16
+ import type React from 'react';
17
+ import { useState } from 'react';
18
+ import { toast } from '@oxy.so/bloom/toast';
19
+ import { EMAIL_CODE_LENGTH, checkContract, emailAddressCheck } from '@oxy.so/contracts/client';
20
+ import { useQueryClient } from '@tanstack/react-query';
21
+ import { useOxy } from '../../context/oxyRuntimeContext';
22
+ import { queryKeys } from '../../hooks/queries/queryKeys';
23
+ import { useI18n } from '../../hooks/useI18n';
24
+ import { SubtleLink } from '../authChooser/primitives';
25
+ import { OxyAuthScreen, OxyAuthScreenHeader } from './OxyAuthScreen';
26
+ import { ReauthStep } from './ReauthStep';
27
+ import {
28
+ AccountFlowAction,
29
+ AccountFlowCodeField,
30
+ AccountFlowField,
31
+ AccountFlowNote,
32
+ AccountFlowNotice,
33
+ describeSignInError,
34
+ } from './accountFlowParts';
35
+ import { accountSecurityFallback, useAccountSecurityState } from './accountSecurityState';
36
+
37
+ type Step =
38
+ | { name: 'status' }
39
+ | { name: 'address' }
40
+ | { name: 'confirm'; email: string; verificationId: string }
41
+ | { name: 'remove' };
42
+
43
+ export interface OxyEmailPanelProps {
44
+ /** The email was added, changed or removed. */
45
+ onDone?: () => void;
46
+ }
47
+
48
+ export const OxyEmailPanel: React.FC<OxyEmailPanelProps> = ({ onDone }) => {
49
+ const { t } = useI18n();
50
+ const { oxyServices, user } = useOxy();
51
+ const queryClient = useQueryClient();
52
+ const security = useAccountSecurityState();
53
+ const [step, setStep] = useState<Step>({ name: 'status' });
54
+ const [address, setAddress] = useState('');
55
+ const [code, setCode] = useState('');
56
+ const [pending, setPending] = useState(false);
57
+ const [error, setError] = useState<string | null>(null);
58
+
59
+ if (security.keyed) {
60
+ return (
61
+ <OxyAuthScreen>
62
+ <OxyAuthScreenHeader
63
+ title={t('signInSecurity.email.title')}
64
+ description={t('linkCommons.already')}
65
+ />
66
+ </OxyAuthScreen>
67
+ );
68
+ }
69
+ const fallback = accountSecurityFallback(security, t('signInSecurity.email.title'), t);
70
+ if (fallback || !security.methods.data) return fallback;
71
+ const { hasEmail, hasPassword, totpEnabled } = security.methods.data;
72
+ const current = hasEmail ? (user?.email ?? null) : null;
73
+
74
+ const finish = (message: string) => {
75
+ void queryClient.invalidateQueries({ queryKey: queryKeys.signInMethods.all });
76
+ void queryClient.invalidateQueries({ queryKey: queryKeys.accounts.all });
77
+ toast.success(message);
78
+ setStep({ name: 'status' });
79
+ setAddress('');
80
+ setCode('');
81
+ onDone?.();
82
+ };
83
+
84
+ const sendCode = (email: string) => {
85
+ if (pending) return;
86
+ setError(null);
87
+ setPending(true);
88
+ oxyServices.auth.email
89
+ .startChange(email)
90
+ .then((started) => {
91
+ setCode('');
92
+ setStep({ name: 'confirm', email, verificationId: started.verificationId });
93
+ })
94
+ .catch((reason: unknown) => setError(describeSignInError(reason, t)))
95
+ .finally(() => setPending(false));
96
+ };
97
+
98
+ const submitAddress = () => {
99
+ const email = checkContract(emailAddressCheck, address);
100
+ if (!email) {
101
+ setError(t('signInSecurity.email.invalid'));
102
+ return;
103
+ }
104
+ if (email === current) {
105
+ setError(t('signInSecurity.errors.emailUnchanged'));
106
+ return;
107
+ }
108
+ sendCode(email);
109
+ };
110
+
111
+ switch (step.name) {
112
+ case 'address':
113
+ return (
114
+ <OxyAuthScreen>
115
+ <OxyAuthScreenHeader
116
+ title={hasEmail ? t('signInSecurity.email.change') : t('signInSecurity.email.add')}
117
+ description={t('signInSecurity.email.newDescription')}
118
+ />
119
+ <AccountFlowField
120
+ label={t('signInSecurity.email.newLabel')}
121
+ value={address}
122
+ onChange={(value) => {
123
+ setAddress(value);
124
+ if (error) setError(null);
125
+ }}
126
+ onSubmit={submitAddress}
127
+ error={error}
128
+ disabled={pending}
129
+ autoComplete="email"
130
+ keyboardType="email-address"
131
+ testID="email-new"
132
+ />
133
+ <AccountFlowAction
134
+ label={t('signInSecurity.email.sendCode')}
135
+ onPress={submitAddress}
136
+ pending={pending}
137
+ testID="email-send-code"
138
+ />
139
+ <SubtleLink
140
+ label={t('common.cancel')}
141
+ onPress={() => {
142
+ setError(null);
143
+ setStep({ name: 'status' });
144
+ }}
145
+ disabled={pending}
146
+ testID="email-cancel"
147
+ />
148
+ </OxyAuthScreen>
149
+ );
150
+ case 'confirm':
151
+ return (
152
+ <ReauthStep
153
+ title={t('signInSecurity.email.confirmTitle')}
154
+ description={t('signInSecurity.email.confirmDescription', { email: step.email })}
155
+ action="change_email"
156
+ allowPassword={hasPassword}
157
+ allowEmail={hasEmail}
158
+ totpEnabled={totpEnabled}
159
+ submitLabel={t('signInSecurity.email.save')}
160
+ validate={() =>
161
+ code.replace(/\D/g, '').length === EMAIL_CODE_LENGTH
162
+ ? null
163
+ : t('emailCode.errors.codeInvalid')
164
+ }
165
+ onSubmit={async (reauth) => {
166
+ await oxyServices.auth.email.change({
167
+ email: step.email,
168
+ verificationId: step.verificationId,
169
+ code: code.replace(/\D/g, ''),
170
+ reauth,
171
+ });
172
+ finish(t('signInSecurity.email.saved'));
173
+ }}
174
+ secondary={{ label: t('common.cancel'), onPress: () => setStep({ name: 'status' }) }}
175
+ >
176
+ <AccountFlowCodeField
177
+ label={t('signInSecurity.email.codeLabel')}
178
+ value={code}
179
+ onChange={setCode}
180
+ error={null}
181
+ length={EMAIL_CODE_LENGTH}
182
+ testID="email-new-code"
183
+ />
184
+ <SubtleLink
185
+ label={t('emailCode.resend')}
186
+ onPress={() => sendCode(step.email)}
187
+ disabled={pending}
188
+ testID="email-new-code-resend"
189
+ />
190
+ {error ? (
191
+ <AccountFlowNotice type="error" testID="email-new-code-error">
192
+ {error}
193
+ </AccountFlowNotice>
194
+ ) : null}
195
+ </ReauthStep>
196
+ );
197
+ case 'remove':
198
+ return (
199
+ <ReauthStep
200
+ title={t('signInSecurity.email.removeTitle')}
201
+ description={t('signInSecurity.email.removeDescription')}
202
+ action="change_email"
203
+ // Confirmed with the password it will rely on, never the mailbox it loses.
204
+ allowPassword
205
+ allowEmail={false}
206
+ totpEnabled={totpEnabled}
207
+ submitLabel={t('signInSecurity.email.remove')}
208
+ destructive
209
+ onSubmit={async (reauth) => {
210
+ await oxyServices.auth.email.remove(reauth);
211
+ finish(t('signInSecurity.email.removed'));
212
+ }}
213
+ secondary={{ label: t('common.cancel'), onPress: () => setStep({ name: 'status' }) }}
214
+ />
215
+ );
216
+ default:
217
+ return (
218
+ <OxyAuthScreen>
219
+ <OxyAuthScreenHeader
220
+ title={t('signInSecurity.email.title')}
221
+ description={
222
+ hasEmail
223
+ ? t('signInSecurity.email.description')
224
+ : t('signInSecurity.email.noneDescription')
225
+ }
226
+ />
227
+ {current ? <AccountFlowNote testID="email-current">{current}</AccountFlowNote> : null}
228
+ {!hasEmail ? (
229
+ <AccountFlowNotice type="warning" testID="email-none-warning">
230
+ {t('signInSecurity.noEmailWarning')}
231
+ </AccountFlowNotice>
232
+ ) : null}
233
+ <AccountFlowAction
234
+ label={hasEmail ? t('signInSecurity.email.change') : t('signInSecurity.email.add')}
235
+ onPress={() => {
236
+ setError(null);
237
+ setAddress('');
238
+ setStep({ name: 'address' });
239
+ }}
240
+ pending={false}
241
+ testID="email-change"
242
+ />
243
+ {hasEmail && hasPassword ? (
244
+ <SubtleLink
245
+ label={t('signInSecurity.email.remove')}
246
+ onPress={() => setStep({ name: 'remove' })}
247
+ testID="remove-email-link"
248
+ />
249
+ ) : null}
250
+ {hasEmail && !hasPassword ? (
251
+ <AccountFlowNote testID="email-remove-needs-password">
252
+ {t('signInSecurity.email.removeNeedsPassword')}
253
+ </AccountFlowNote>
254
+ ) : null}
255
+ </OxyAuthScreen>
256
+ );
257
+ }
258
+ };
@@ -4,7 +4,8 @@
4
4
  *
5
5
  * QR → Commons scans and signs with its key → both screens show the same
6
6
  * code → the person confirms it's them with a code sent to the account's
7
- * email (+ the authenticator's code) → linked
7
+ * email, or its password when it has none (+ the authenticator's code) →
8
+ * linked
8
9
  *
9
10
  * Afterwards the account is self-custodied: its root is the key Commons holds,
10
11
  * its email, password and authenticator are deleted, and its phrase in Commons
@@ -22,7 +23,6 @@ import { Text } from '@oxy.so/bloom/typography';
22
23
  import type { IdentityLinkCreateResponse } from '@oxy.so/contracts';
23
24
  import { useOxy } from '../../context/oxyRuntimeContext';
24
25
  import { loadIdentityCrypto } from '../../utils/identityCrypto';
25
- import { useSignInMethods } from '../../hooks/queries/useAuthMethods';
26
26
  import { useI18n } from '../../hooks/useI18n';
27
27
  import { OxyAuthScreen, OxyAuthScreenHeader } from './OxyAuthScreen';
28
28
  import { ReauthStep } from './ReauthStep';
@@ -32,6 +32,7 @@ import {
32
32
  AccountFlowNote,
33
33
  describeSignInError,
34
34
  } from './accountFlowParts';
35
+ import { accountSecurityFallback, useAccountSecurityState } from './accountSecurityState';
35
36
 
36
37
  /** How often the page asks whether Commons has signed. */
37
38
  export const IDENTITY_LINK_POLL_MS = 2000;
@@ -57,11 +58,11 @@ export interface OxyLinkCommonsPanelProps {
57
58
  export const OxyLinkCommonsPanel: React.FC<OxyLinkCommonsPanelProps> = ({ onLinked }) => {
58
59
  const theme = useTheme();
59
60
  const { t } = useI18n();
60
- const { user, oxyServices } = useOxy();
61
+ const { oxyServices } = useOxy();
61
62
  const [step, setStep] = useState<Step>({ name: 'opening' });
62
63
  const [error, setError] = useState<string | null>(null);
63
- const alreadyLinked = Boolean(user?.publicKey);
64
- const methods = useSignInMethods({ enabled: !alreadyLinked });
64
+ const security = useAccountSecurityState();
65
+ const alreadyLinked = security.keyed;
65
66
 
66
67
  const open = useCallback(() => {
67
68
  setError(null);
@@ -78,10 +79,10 @@ export const OxyLinkCommonsPanel: React.FC<OxyLinkCommonsPanelProps> = ({ onLink
78
79
  // One request per visit, opened once the page knows the account has no root.
79
80
  const opened = useRef(false);
80
81
  useEffect(() => {
81
- if (alreadyLinked || opened.current) return;
82
+ if (alreadyLinked || !security.personal || opened.current) return;
82
83
  opened.current = true;
83
84
  open();
84
- }, [alreadyLinked, open]);
85
+ }, [alreadyLinked, security.personal, open]);
85
86
 
86
87
  // While the QR shows, ask whether Commons has signed; a request only lives a
87
88
  // few minutes, and an expired one says so instead of waiting forever.
@@ -140,27 +141,38 @@ export const OxyLinkCommonsPanel: React.FC<OxyLinkCommonsPanelProps> = ({ onLink
140
141
  );
141
142
  }
142
143
 
144
+ if (!security.personal) return accountSecurityFallback(security, t('linkCommons.title'), t);
145
+
143
146
  switch (step.name) {
144
- case 'confirm':
147
+ case 'confirm': {
148
+ // The proof depends on what the account has: wait for it rather than
149
+ // guess (an authenticator left out would only fail the confirmation).
150
+ const fallback = accountSecurityFallback(security, t('linkCommons.confirmTitle'), t);
151
+ if (fallback || !security.methods.data) return fallback;
152
+ const { hasEmail, totpEnabled } = security.methods.data;
145
153
  return (
146
154
  <ReauthStep
147
155
  title={t('linkCommons.confirmTitle')}
148
- description={t('linkCommons.confirmDescription')}
156
+ description={
157
+ hasEmail
158
+ ? t('linkCommons.confirmDescription')
159
+ : t('linkCommons.confirmPasswordDescription')
160
+ }
149
161
  action="link_commons"
150
- totpEnabled={methods.data?.totpEnabled ?? false}
162
+ // The mailbox proves it when there is one; the password only without.
163
+ allowPassword={!hasEmail}
164
+ allowEmail={hasEmail}
165
+ totpEnabled={totpEnabled}
151
166
  submitLabel={t('linkCommons.confirm')}
152
167
  onSubmit={async (proof) => {
153
- if (!proof.emailCode) throw new Error(t('reauth.errors.invalid'));
154
- await oxyServices.identity.links.complete(step.link.linkId, {
155
- emailCode: proof.emailCode,
156
- ...(proof.totpCode ? { totpCode: proof.totpCode } : {}),
157
- });
168
+ await oxyServices.identity.links.complete(step.link.linkId, proof);
158
169
  setStep({ name: 'done' });
159
170
  onLinked?.();
160
171
  }}
161
172
  secondary={{ label: t('linkCommons.cancel'), onPress: () => cancel(step.link) }}
162
173
  />
163
174
  );
175
+ }
164
176
  case 'done':
165
177
  return (
166
178
  <OxyAuthScreen>