@oxy.so/services 1.1.0 → 2.0.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 (176) hide show
  1. package/README.md +1 -1
  2. package/lib/commonjs/ui/components/BenefitList.js +93 -0
  3. package/lib/commonjs/ui/components/BenefitList.js.map +1 -0
  4. package/lib/commonjs/ui/components/FollowTargetButton.js +1 -1
  5. package/lib/commonjs/ui/components/FollowTargetButton.js.map +1 -1
  6. package/lib/commonjs/ui/components/OxyAuthChooser.js +95 -140
  7. package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -1
  8. package/lib/commonjs/ui/components/authChooser/AccountsMenuView.js +8 -1
  9. package/lib/commonjs/ui/components/authChooser/AccountsMenuView.js.map +1 -1
  10. package/lib/commonjs/ui/components/authChooser/SignInEntryView.js +35 -25
  11. package/lib/commonjs/ui/components/authChooser/SignInEntryView.js.map +1 -1
  12. package/lib/commonjs/ui/components/authChooser/SignInRequestView.js +2 -3
  13. package/lib/commonjs/ui/components/authChooser/SignInRequestView.js.map +1 -1
  14. package/lib/commonjs/ui/components/authChooser/SignUpView.js +12 -127
  15. package/lib/commonjs/ui/components/authChooser/SignUpView.js.map +1 -1
  16. package/lib/commonjs/ui/components/authChooser/signInFailureMessage.js +40 -0
  17. package/lib/commonjs/ui/components/authChooser/signInFailureMessage.js.map +1 -0
  18. package/lib/commonjs/ui/components/authChooser/types.js +4 -6
  19. package/lib/commonjs/ui/components/authChooser/types.js.map +1 -1
  20. package/lib/commonjs/ui/context/OxyContext.js +1 -17
  21. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  22. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +15 -2
  23. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  24. package/lib/commonjs/ui/context/passkeyFlow.js +2 -28
  25. package/lib/commonjs/ui/context/passkeyFlow.js.map +1 -1
  26. package/lib/commonjs/ui/hooks/accountDialogSnapshot.js +3 -1
  27. package/lib/commonjs/ui/hooks/accountDialogSnapshot.js.map +1 -1
  28. package/lib/commonjs/ui/hooks/useProfileEditing.js +14 -0
  29. package/lib/commonjs/ui/hooks/useProfileEditing.js.map +1 -1
  30. package/lib/commonjs/ui/screens/AccountVerificationScreen.js +10 -10
  31. package/lib/commonjs/ui/screens/AccountVerificationScreen.js.map +1 -1
  32. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +225 -9
  33. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
  34. package/lib/commonjs/ui/screens/LearnMoreUsernamesScreen.js +6 -6
  35. package/lib/commonjs/ui/screens/LearnMoreUsernamesScreen.js.map +1 -1
  36. package/lib/commonjs/ui/screens/ManageAccountScreen.js +10 -0
  37. package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
  38. package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js +5 -5
  39. package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
  40. package/lib/commonjs/ui/screens/trust/TrustAboutScreen.js +13 -13
  41. package/lib/commonjs/ui/screens/trust/TrustAboutScreen.js.map +1 -1
  42. package/lib/module/ui/components/BenefitList.js +89 -0
  43. package/lib/module/ui/components/BenefitList.js.map +1 -0
  44. package/lib/module/ui/components/FollowTargetButton.js +1 -1
  45. package/lib/module/ui/components/FollowTargetButton.js.map +1 -1
  46. package/lib/module/ui/components/OxyAuthChooser.js +97 -142
  47. package/lib/module/ui/components/OxyAuthChooser.js.map +1 -1
  48. package/lib/module/ui/components/authChooser/AccountsMenuView.js +8 -1
  49. package/lib/module/ui/components/authChooser/AccountsMenuView.js.map +1 -1
  50. package/lib/module/ui/components/authChooser/SignInEntryView.js +35 -25
  51. package/lib/module/ui/components/authChooser/SignInEntryView.js.map +1 -1
  52. package/lib/module/ui/components/authChooser/SignInRequestView.js +2 -3
  53. package/lib/module/ui/components/authChooser/SignInRequestView.js.map +1 -1
  54. package/lib/module/ui/components/authChooser/SignUpView.js +14 -129
  55. package/lib/module/ui/components/authChooser/SignUpView.js.map +1 -1
  56. package/lib/module/ui/components/authChooser/signInFailureMessage.js +36 -0
  57. package/lib/module/ui/components/authChooser/signInFailureMessage.js.map +1 -0
  58. package/lib/module/ui/components/authChooser/types.js +4 -6
  59. package/lib/module/ui/components/authChooser/types.js.map +1 -1
  60. package/lib/module/ui/context/OxyContext.js +2 -18
  61. package/lib/module/ui/context/OxyContext.js.map +1 -1
  62. package/lib/module/ui/context/hooks/useAuthOperations.js +15 -2
  63. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  64. package/lib/module/ui/context/passkeyFlow.js +2 -27
  65. package/lib/module/ui/context/passkeyFlow.js.map +1 -1
  66. package/lib/module/ui/hooks/accountDialogSnapshot.js +3 -1
  67. package/lib/module/ui/hooks/accountDialogSnapshot.js.map +1 -1
  68. package/lib/module/ui/hooks/useProfileEditing.js +14 -0
  69. package/lib/module/ui/hooks/useProfileEditing.js.map +1 -1
  70. package/lib/module/ui/screens/AccountVerificationScreen.js +6 -6
  71. package/lib/module/ui/screens/AccountVerificationScreen.js.map +1 -1
  72. package/lib/module/ui/screens/EditProfileFieldScreen.js +225 -9
  73. package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
  74. package/lib/module/ui/screens/LearnMoreUsernamesScreen.js +6 -6
  75. package/lib/module/ui/screens/LearnMoreUsernamesScreen.js.map +1 -1
  76. package/lib/module/ui/screens/ManageAccountScreen.js +12 -2
  77. package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
  78. package/lib/module/ui/screens/PremiumSubscriptionScreen.js +1 -1
  79. package/lib/module/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
  80. package/lib/module/ui/screens/trust/TrustAboutScreen.js +7 -7
  81. package/lib/module/ui/screens/trust/TrustAboutScreen.js.map +1 -1
  82. package/lib/typescript/commonjs/index.d.ts +1 -1
  83. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/ui/components/BenefitList.d.ts +25 -0
  85. package/lib/typescript/commonjs/ui/components/BenefitList.d.ts.map +1 -0
  86. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts +3 -4
  87. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/ui/components/authChooser/AccountsMenuView.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/ui/components/authChooser/SignInEntryView.d.ts +11 -12
  90. package/lib/typescript/commonjs/ui/components/authChooser/SignInEntryView.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/ui/components/authChooser/SignInRequestView.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/ui/components/authChooser/SignUpView.d.ts +10 -13
  93. package/lib/typescript/commonjs/ui/components/authChooser/SignUpView.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/ui/components/authChooser/signInFailureMessage.d.ts +16 -0
  95. package/lib/typescript/commonjs/ui/components/authChooser/signInFailureMessage.d.ts.map +1 -0
  96. package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts +11 -13
  97. package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +1 -1
  99. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +3 -2
  101. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +20 -10
  103. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/ui/context/passkeyFlow.d.ts +2 -21
  105. package/lib/typescript/commonjs/ui/context/passkeyFlow.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/ui/hooks/accountDialogSnapshot.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +3 -0
  108. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/ui/hooks/useProfileEditing.d.ts +1 -0
  110. package/lib/typescript/commonjs/ui/hooks/useProfileEditing.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  113. package/lib/typescript/module/index.d.ts +1 -1
  114. package/lib/typescript/module/index.d.ts.map +1 -1
  115. package/lib/typescript/module/ui/components/BenefitList.d.ts +25 -0
  116. package/lib/typescript/module/ui/components/BenefitList.d.ts.map +1 -0
  117. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts +3 -4
  118. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -1
  119. package/lib/typescript/module/ui/components/authChooser/AccountsMenuView.d.ts.map +1 -1
  120. package/lib/typescript/module/ui/components/authChooser/SignInEntryView.d.ts +11 -12
  121. package/lib/typescript/module/ui/components/authChooser/SignInEntryView.d.ts.map +1 -1
  122. package/lib/typescript/module/ui/components/authChooser/SignInRequestView.d.ts.map +1 -1
  123. package/lib/typescript/module/ui/components/authChooser/SignUpView.d.ts +10 -13
  124. package/lib/typescript/module/ui/components/authChooser/SignUpView.d.ts.map +1 -1
  125. package/lib/typescript/module/ui/components/authChooser/signInFailureMessage.d.ts +16 -0
  126. package/lib/typescript/module/ui/components/authChooser/signInFailureMessage.d.ts.map +1 -0
  127. package/lib/typescript/module/ui/components/authChooser/types.d.ts +11 -13
  128. package/lib/typescript/module/ui/components/authChooser/types.d.ts.map +1 -1
  129. package/lib/typescript/module/ui/context/OxyContext.d.ts +1 -1
  130. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  131. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +3 -2
  132. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  133. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +20 -10
  134. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
  135. package/lib/typescript/module/ui/context/passkeyFlow.d.ts +2 -21
  136. package/lib/typescript/module/ui/context/passkeyFlow.d.ts.map +1 -1
  137. package/lib/typescript/module/ui/hooks/accountDialogSnapshot.d.ts.map +1 -1
  138. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +3 -0
  139. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  140. package/lib/typescript/module/ui/hooks/useProfileEditing.d.ts +1 -0
  141. package/lib/typescript/module/ui/hooks/useProfileEditing.d.ts.map +1 -1
  142. package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  143. package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  144. package/package.json +6 -6
  145. package/src/index.ts +1 -1
  146. package/src/ui/components/BenefitList.tsx +111 -0
  147. package/src/ui/components/FollowTargetButton.tsx +1 -1
  148. package/src/ui/components/OxyAuthChooser.tsx +106 -156
  149. package/src/ui/components/authChooser/AccountsMenuView.tsx +7 -0
  150. package/src/ui/components/authChooser/SignInEntryView.tsx +41 -44
  151. package/src/ui/components/authChooser/SignInRequestView.tsx +1 -4
  152. package/src/ui/components/authChooser/SignUpView.tsx +16 -114
  153. package/src/ui/components/authChooser/signInFailureMessage.ts +39 -0
  154. package/src/ui/components/authChooser/types.ts +11 -13
  155. package/src/ui/context/OxyContext.tsx +2 -20
  156. package/src/ui/context/__tests__/passkeyFlow.test.ts +2 -65
  157. package/src/ui/context/hooks/useAuthOperations.ts +10 -5
  158. package/src/ui/context/oxyContextTypes.ts +16 -8
  159. package/src/ui/context/passkeyFlow.ts +2 -39
  160. package/src/ui/hooks/accountDialogSnapshot.ts +2 -0
  161. package/src/ui/hooks/useProfileEditing.ts +15 -0
  162. package/src/ui/screens/AccountVerificationScreen.tsx +6 -6
  163. package/src/ui/screens/EditProfileFieldScreen.tsx +202 -7
  164. package/src/ui/screens/LearnMoreUsernamesScreen.tsx +6 -6
  165. package/src/ui/screens/ManageAccountScreen.tsx +12 -0
  166. package/src/ui/screens/PremiumSubscriptionScreen.tsx +1 -1
  167. package/src/ui/screens/trust/TrustAboutScreen.tsx +7 -7
  168. package/lib/commonjs/ui/components/authChooser/useUsernameAvailability.js +0 -50
  169. package/lib/commonjs/ui/components/authChooser/useUsernameAvailability.js.map +0 -1
  170. package/lib/module/ui/components/authChooser/useUsernameAvailability.js +0 -46
  171. package/lib/module/ui/components/authChooser/useUsernameAvailability.js.map +0 -1
  172. package/lib/typescript/commonjs/ui/components/authChooser/useUsernameAvailability.d.ts +0 -16
  173. package/lib/typescript/commonjs/ui/components/authChooser/useUsernameAvailability.d.ts.map +0 -1
  174. package/lib/typescript/module/ui/components/authChooser/useUsernameAvailability.d.ts +0 -16
  175. package/lib/typescript/module/ui/components/authChooser/useUsernameAvailability.d.ts.map +0 -1
  176. package/src/ui/components/authChooser/useUsernameAvailability.ts +0 -60
@@ -1,37 +1,31 @@
1
1
  /**
2
2
  * Account creation (`signup` view).
3
3
  *
4
- * Commons is ALWAYS the priority path (owner mandate) and leads everywhere it
5
- * appears: native shows only "Create your identity in Commons" (or "Get Commons"
6
- * first, when it is not installed); web on a first-party Oxy origin leads with
7
- * the SAME "Get Commons" CTA, with inline passkey creation offered UNDERNEATH as
8
- * the de-emphasized "don't want to install anything" alternative — never
9
- * co-equal, never a competing button. Anywhere else (a non-Oxy web origin,
10
- * before the b2 hub-relay ships) shows an honest "not available here yet"
11
- * handoff instead of a broken button.
4
+ * Web: the account AND its self-custody identity are created at the identity
5
+ * origin (`id.oxy.so`), in the same window a passkey sign-in uses — never on
6
+ * this page, whatever its origin (one identity, two carriers). The sign-in
7
+ * entry's "create an account" link opens that window directly; this view is
8
+ * what `openAccountDialog('signup')` lands on, and offers the same one action.
9
+ *
10
+ * Native: Commons owns identity creation. Deep-link straight in when Commons is
11
+ * installed, else lead with the "Get Commons" acquisition CTA.
12
12
  */
13
13
 
14
14
  import type React from 'react';
15
- import { useState } from 'react';
16
- import { TextInput, View } from 'react-native';
15
+ import { View } from 'react-native';
17
16
  import { Button } from '@oxy.so/bloom/button';
18
17
  import { Text } from '@oxy.so/bloom/typography';
19
- import type { AccountDialogSnapshot, OxyServices } from '@oxy.so/core';
20
- import { isWebBrowser } from '../../utils/isWebBrowser';
21
- import { Dividerish, SubtleLink } from './primitives';
18
+ import type { AccountDialogSnapshot } from '@oxy.so/core';
19
+ import { SubtleLink } from './primitives';
22
20
  import { authChooserStyles as styles } from './styles';
23
21
  import type { PasskeyMode, Theme, Translate } from './types';
24
- import { useUsernameAvailability, type UsernameStatus } from './useUsernameAvailability';
25
22
 
26
23
  interface SignUpViewProps {
27
24
  snapshot: AccountDialogSnapshot;
28
25
  theme: Theme;
29
26
  t: Translate;
30
- oxyServices: OxyServices;
31
27
  passkeyMode: PasskeyMode;
32
- onCreateWithPasskey: (username: string) => void;
33
- createPending: boolean;
34
- /** Open the auth.oxy.so hub popup (b2) — the 'hub' mode's only path. */
28
+ /** Open the identity-origin window — web's only creation path. */
35
29
  onOpenHub: () => void;
36
30
  /** Deep-link into Commons' own identity-creation screen (installed only). */
37
31
  onCreateIdentityInCommons: () => void;
@@ -44,22 +38,13 @@ const SignUpView: React.FC<SignUpViewProps> = ({
44
38
  snapshot,
45
39
  theme,
46
40
  t,
47
- oxyServices,
48
41
  passkeyMode,
49
- onCreateWithPasskey,
50
- createPending,
51
42
  onOpenHub,
52
43
  onCreateIdentityInCommons,
53
44
  onGetCommons,
54
45
  onBackToSignIn,
55
46
  }) => {
56
- const [username, setUsername] = useState('');
57
- const { status: usernameStatus, check: checkUsername } = useUsernameAvailability(oxyServices, t);
58
- const canSubmit = username.trim().length >= 3 && usernameStatus === 'available' && !createPending;
59
-
60
- // Native — Commons owns identity creation. Deep-link straight in when it is
61
- // installed, else lead with the same "Get Commons" acquisition CTA.
62
- if (!isWebBrowser()) {
47
+ if (passkeyMode === 'none') {
63
48
  const commonsInstalled = snapshot.commonsAvailability === 'available';
64
49
  return (
65
50
  <View style={styles.centeredBlock}>
@@ -75,100 +60,17 @@ const SignUpView: React.FC<SignUpViewProps> = ({
75
60
  );
76
61
  }
77
62
 
78
- // Web, first-party Oxy origin. Commons leads with a "Get Commons" CTA; passkey
79
- // creation is the de-emphasized alternative underneath for someone who doesn't
80
- // want to install anything — a secondary button introduced by its own "No
81
- // Commons?" framing, the same subordination the sign-in surfaces use.
82
- if (passkeyMode === 'direct') {
83
- return (
84
- <View style={styles.signInBlock}>
85
- <Text style={[styles.qrHeadline, { color: theme.colors.text }]}>
86
- {t('signup.commonsHeadline')}
87
- </Text>
88
- <Text style={[styles.mutedText, { color: theme.colors.textSecondary }]}>
89
- {t('signup.commonsExplainer')}
90
- </Text>
91
- <Button variant="primary" onPress={onGetCommons} style={styles.primaryButton}>
92
- {t('accountSwitcher.getCommons')}
93
- </Button>
94
-
95
- <Dividerish theme={theme} label={t('signin.or')} />
96
-
97
- <Text style={[styles.mutedText, { color: theme.colors.textSecondary }]}>
98
- {t('signup.passkeyAlternative')}
99
- </Text>
100
- <TextInput
101
- style={[styles.usernameInput, { borderColor: theme.colors.border, color: theme.colors.text }]}
102
- placeholder={t('signup.usernamePlaceholder')}
103
- placeholderTextColor={theme.colors.textSecondary}
104
- value={username}
105
- autoCapitalize="none"
106
- autoCorrect={false}
107
- autoComplete="username"
108
- onChangeText={(value) => {
109
- setUsername(value);
110
- checkUsername(value.trim());
111
- }}
112
- testID="signup-username-input"
113
- />
114
- <UsernameStatusText status={usernameStatus} theme={theme} t={t} />
115
- <Button
116
- variant="secondary"
117
- onPress={() => onCreateWithPasskey(username.trim())}
118
- disabled={!canSubmit}
119
- style={styles.secondaryButton}
120
- testID="signup-create-button"
121
- >
122
- {createPending ? t('signup.creating') : t('signup.createAccount')}
123
- </Button>
124
- <SubtleLink label={t('signup.backToSignInLink')} theme={theme} onPress={onBackToSignIn} />
125
- </View>
126
- );
127
- }
128
-
129
- // Web, non-Oxy origin (b2): a passkey ceremony cannot run locally here — the
130
- // same WebAuthn RP-ID boundary the sign-in surfaces hit — so this opens the
131
- // auth.oxy.so hub popup. Completing EITHER a sign-in OR a fresh sign-up there
132
- // relays the resulting session back through the SAME poll/socket engine.
133
63
  return (
134
64
  <View style={styles.centeredBlock}>
135
65
  <Text style={[styles.mutedText, { color: theme.colors.textSecondary }]}>
136
- {t('signup.hubExplainer')}
66
+ {t('accountSwitcher.passkeyHint')}
137
67
  </Text>
138
- <Button variant="primary" onPress={onOpenHub} style={styles.primaryButton}>
139
- {t('signup.continueInWindow')}
68
+ <Button variant="primary" onPress={onOpenHub} style={styles.primaryButton} testID="signup-open-identity">
69
+ {t('signup.createAccount')}
140
70
  </Button>
141
71
  <SubtleLink label={t('signup.backToSignInLink')} theme={theme} onPress={onBackToSignIn} />
142
72
  </View>
143
73
  );
144
74
  };
145
75
 
146
- /** The inline availability verdict. Network FAILURES are toasted, never shown here. */
147
- const UsernameStatusText: React.FC<{ status: UsernameStatus; theme: Theme; t: Translate }> = ({
148
- status,
149
- theme,
150
- t,
151
- }) => {
152
- if (status === 'idle') return null;
153
- if (status === 'checking') {
154
- return (
155
- <Text style={[styles.mutedText, { color: theme.colors.textSecondary }]}>
156
- {t('signup.usernameChecking')}
157
- </Text>
158
- );
159
- }
160
- if (status === 'available') {
161
- return (
162
- <Text style={[styles.mutedText, { color: theme.colors.success }]}>
163
- {t('signup.usernameAvailable')}
164
- </Text>
165
- );
166
- }
167
- return (
168
- <Text style={[styles.mutedText, { color: theme.colors.error }]}>
169
- {t('signup.usernameTaken')}
170
- </Text>
171
- );
172
- };
173
-
174
76
  export default SignUpView;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The localized toast for a failed sign-in attempt.
3
+ *
4
+ * One responsibility: turn the controller's machine-readable
5
+ * `signIn.failure` into copy. The controller ships no user-facing prose — its
6
+ * raw `signIn.error` is English or a server string, kept for diagnostics only.
7
+ */
8
+
9
+ import type { SignInFailureReason } from '@oxy.so/core';
10
+ import type { Translate } from './types';
11
+
12
+ /**
13
+ * The toast for `failure`, or `null` when there is nothing to report: a
14
+ * `'cancelled'` attempt is the user's own choice (they closed the window), and
15
+ * the surface already offers "Try again" without scolding them for it.
16
+ */
17
+ export function signInFailureMessage(
18
+ failure: SignInFailureReason | null,
19
+ t: Translate,
20
+ ): string | null {
21
+ switch (failure) {
22
+ case 'cancelled':
23
+ return null;
24
+ case 'denied':
25
+ return t('accountSwitcher.signInFailures.denied');
26
+ case 'expired':
27
+ return t('accountSwitcher.signInFailures.expired');
28
+ case 'network':
29
+ return t('accountSwitcher.signInFailures.network');
30
+ case 'not-configured':
31
+ return t('accountSwitcher.signInFailures.notConfigured');
32
+ case 'unsupported-flow':
33
+ return t('accountSwitcher.signInFailures.unsupportedFlow');
34
+ case 'claim-failed':
35
+ return t('accountSwitcher.signInFailures.claimFailed');
36
+ default:
37
+ return t('accountSwitcher.signInFailures.generic');
38
+ }
39
+ }
@@ -95,6 +95,11 @@ export interface AccountsMenuActions {
95
95
  onPrivacy: () => void;
96
96
  onTerms: () => void;
97
97
  onSignOut: () => void;
98
+ /**
99
+ * Open the person's identity at the identity origin (recovery phrase,
100
+ * recovery, account deletion). Web only — `undefined` hides the row.
101
+ */
102
+ onOpenIdentity?: () => void;
98
103
  customItems: readonly {
99
104
  key: string;
100
105
  label: string;
@@ -104,15 +109,13 @@ export interface AccountsMenuActions {
104
109
  }
105
110
 
106
111
  /**
107
- * Where a WebAuthn ceremony can run from the current origin.
112
+ * Where a passkey sign-in or sign-up can run from the current surface.
108
113
  *
109
- * A credential minted for `oxy.so` can only be ASSERTED there (or a loopback
110
- * dev host) — a hard, browser-enforced RP-ID boundary, not feature detection.
111
- * `'direct'` = a first-party Oxy web origin, the ceremony runs here.
112
- * `'hub'` = any other web origin; it runs in the auth.oxy.so popup instead.
113
- * `'none'` = native, where Commons owns identity.
114
+ * `'hub'` = web: it runs at the identity origin (`id.oxy.so`) in a popup, on
115
+ * every web origin, first-party or not.
116
+ * `'none'` = native, where Commons owns identity.
114
117
  */
115
- export type PasskeyMode = 'direct' | 'hub' | 'none';
118
+ export type PasskeyMode = 'hub' | 'none';
116
119
 
117
120
  /**
118
121
  * Everything that is NOT the sign-in surface's one primary action.
@@ -125,17 +128,12 @@ export type PasskeyMode = 'direct' | 'hub' | 'none';
125
128
  export interface SignInAlternatives {
126
129
  /** Whether a WebAuthn ceremony is reachable from this origin at all. */
127
130
  passkeyAvailable: boolean;
128
- /**
129
- * `true` while a DIRECT ceremony is in flight. The hub-popup variant reports
130
- * its progress through `snapshot.signIn` instead, so it never sets this.
131
- */
132
- passkeyPending: boolean;
133
131
  onSignInWithPasskey: () => void;
134
132
  /** Fall back to the cross-device QR handoff (restarts the request). */
135
133
  onShowQr: () => void;
136
134
  /** Open the Commons store listing / landing page for this platform. */
137
135
  onGetCommons: () => void;
138
- /** Enter the account-creation view. */
136
+ /** Create an account: the identity-origin window on web, the Commons flow on native. */
139
137
  onCreateAccount: () => void;
140
138
  }
141
139
 
@@ -95,7 +95,7 @@ import { DEFAULT_SESSION_VALIDITY_MS } from './oxyContextHelpers';
95
95
  // never dereferenced at module scope here.
96
96
  import { useFollow } from '../hooks/useFollow';
97
97
  import { commitDeviceSetAndResolve } from './commitSessionFlow';
98
- import { runPasskeyLogin, runPasskeyRegister, runPasskeyAdd } from './passkeyFlow';
98
+ import { runPasskeyLogin, runPasskeyAdd } from './passkeyFlow';
99
99
  import {
100
100
  isPasskeySupported,
101
101
  runRegistrationCeremony,
@@ -104,7 +104,7 @@ import {
104
104
  import { queryKeys } from '../hooks/queries/queryKeys';
105
105
  import { useOxyAccountGraph } from './useOxyAccountGraph';
106
106
 
107
- export type { OxyContextState, OxyRuntimeProviderProps } from './oxyContextTypes';
107
+ export type { LogoutResult, OxyContextState, OxyRuntimeProviderProps } from './oxyContextTypes';
108
108
 
109
109
  const OxyRuntimeContext = createContext<OxyContextState | null>(null);
110
110
 
@@ -959,22 +959,6 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
959
959
  [oxyServices, authStore, commitSession],
960
960
  );
961
961
 
962
- // Create a brand-new account whose first auth method is a passkey.
963
- const registerWithPasskey = useCallback(
964
- async (params: { username: string; deviceName?: string }): Promise<void> => {
965
- await runPasskeyRegister({
966
- isSupported: isPasskeySupported,
967
- getRegisterOptions: (username) => oxyServices.webauthnRegisterOptions(username),
968
- runCeremony: runRegistrationCeremony,
969
- registerVerify: (response, envelope) => oxyServices.webauthnRegisterVerify(response, envelope),
970
- commit: (input) => commitSession(input, { activate: true }),
971
- username: params.username,
972
- deviceName: params.deviceName,
973
- });
974
- },
975
- [oxyServices, commitSession],
976
- );
977
-
978
962
  // Add a passkey to the already-signed-in account (bearer present). No new
979
963
  // session is committed — just refresh the linked auth-methods list.
980
964
  const addPasskey = useCallback(
@@ -1265,7 +1249,6 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
1265
1249
  getPublicKey,
1266
1250
  signIn,
1267
1251
  signInWithPasskey,
1268
- registerWithPasskey,
1269
1252
  addPasskey,
1270
1253
  removePasskey,
1271
1254
  revokeSuspiciousSignIn,
@@ -1323,7 +1306,6 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
1323
1306
  getPublicKey,
1324
1307
  signIn,
1325
1308
  signInWithPasskey,
1326
- registerWithPasskey,
1327
1309
  addPasskey,
1328
1310
  removePasskey,
1329
1311
  revokeSuspiciousSignIn,
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `passkeyFlow` — the pure, deps-injected passkey (WebAuthn) orchestration that
3
- * backs `useOxy().signInWithPasskey` / `registerWithPasskey` / `addPasskey`.
3
+ * backs `useOxy().signInWithPasskey` / `addPasskey`.
4
4
  *
5
5
  * These tests assert the fixed `options → ceremony → verify → commit` ordering,
6
6
  * the exact `commitSession` input projected from a session-arm login result, the
@@ -12,11 +12,9 @@
12
12
  import type { LoginSessionResult } from '@oxy.so/contracts';
13
13
  import {
14
14
  runPasskeyLogin,
15
- runPasskeyRegister,
16
- runPasskeyAdd,
15
+ runPasskeyAdd,
17
16
  PASSKEY_UNSUPPORTED_MESSAGE,
18
17
  type RunPasskeyLoginDeps,
19
- type RunPasskeyRegisterDeps,
20
18
  type RunPasskeyAddDeps,
21
19
  type PasskeyRegisterVerifyResult,
22
20
  } from '../passkeyFlow';
@@ -122,67 +120,6 @@ describe('runPasskeyLogin', () => {
122
120
  });
123
121
  });
124
122
 
125
- describe('runPasskeyRegister', () => {
126
- const buildDeps = (
127
- order: string[],
128
- overrides: Partial<RunPasskeyRegisterDeps> = {},
129
- ): RunPasskeyRegisterDeps => ({
130
- isSupported: jest.fn(() => true),
131
- getRegisterOptions: jest.fn(async (username) => {
132
- order.push('getRegisterOptions');
133
- expect(username).toBe('newuser');
134
- return { challenge: 'reg-opts' };
135
- }),
136
- runCeremony: jest.fn(async () => {
137
- order.push('runCeremony');
138
- return { id: 'attestation' };
139
- }),
140
- registerVerify: jest.fn(async () => {
141
- order.push('registerVerify');
142
- return sessionResult;
143
- }),
144
- commit: jest.fn(async () => {
145
- order.push('commit');
146
- }),
147
- username: 'newuser',
148
- deviceName: 'My Laptop',
149
- ...overrides,
150
- });
151
-
152
- it('creates the account (signup branch) and commits the minted session', async () => {
153
- const order: string[] = [];
154
- const deps = buildDeps(order);
155
-
156
- await runPasskeyRegister(deps);
157
-
158
- expect(order).toEqual(['getRegisterOptions', 'runCeremony', 'registerVerify', 'commit']);
159
- expect(deps.registerVerify).toHaveBeenCalledWith(
160
- { id: 'attestation' },
161
- { username: 'newuser', deviceName: 'My Laptop' },
162
- );
163
- expect(deps.commit).toHaveBeenCalledWith(expectedCommitInput);
164
- });
165
-
166
- it('throws (no commit) when verify returns a link branch instead of a session', async () => {
167
- const order: string[] = [];
168
- const deps = buildDeps(order, {
169
- registerVerify: jest.fn(async () => linkResult),
170
- });
171
-
172
- await expect(runPasskeyRegister(deps)).rejects.toThrow(/did not establish a session/i);
173
- expect(deps.commit).not.toHaveBeenCalled();
174
- });
175
-
176
- it('throws and touches nothing when passkeys are unsupported', async () => {
177
- const order: string[] = [];
178
- const deps = buildDeps(order, { isSupported: jest.fn(() => false) });
179
-
180
- await expect(runPasskeyRegister(deps)).rejects.toThrow(PASSKEY_UNSUPPORTED_MESSAGE);
181
- expect(deps.getRegisterOptions).not.toHaveBeenCalled();
182
- expect(deps.commit).not.toHaveBeenCalled();
183
- });
184
- });
185
-
186
123
  describe('runPasskeyAdd', () => {
187
124
  const buildDeps = (
188
125
  order: string[],
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
2
2
  import type { ApiError, AuthStateStore, IdentityBinding, SessionClient, User } from '@oxy.so/core';
3
3
  import type { ClientSession, SessionLoginResponse } from '@oxy.so/core';
4
4
  import type { OxyRuntime } from '../../runtime';
5
+ import type { LogoutResult } from '../oxyContextTypes';
5
6
  import { DeviceManager } from '@oxy.so/core';
6
7
  import { fetchSessionsWithFallback } from '../../utils/sessionHelpers';
7
8
  import { handleAuthError, isInvalidSessionError } from '../../utils/errorHandlers';
@@ -56,8 +57,8 @@ export interface UseAuthOperationsOptions {
56
57
  export interface UseAuthOperationsResult {
57
58
  /** Sign in with existing public key */
58
59
  signIn: (publicKey: string, deviceName?: string) => Promise<User>;
59
- /** Logout from current session */
60
- logout: (targetSessionId?: string) => Promise<void>;
60
+ /** Logout from current session. Never rejects — see {@link LogoutResult}. */
61
+ logout: (targetSessionId?: string) => Promise<LogoutResult>;
61
62
  /** Logout from all sessions */
62
63
  logoutAll: () => Promise<void>;
63
64
  }
@@ -292,9 +293,9 @@ export const useAuthOperations = ({
292
293
  * Logout from session
293
294
  */
294
295
  const logout = useCallback(
295
- async (targetSessionId?: string): Promise<void> => {
296
+ async (targetSessionId?: string): Promise<LogoutResult> => {
296
297
  const activeSessionId = runtime.getSnapshot().activeSessionId;
297
- if (!activeSessionId) return;
298
+ if (!activeSessionId) return { status: 'signed-out' };
298
299
 
299
300
  const sessionToLogout = targetSessionId || activeSessionId;
300
301
 
@@ -326,6 +327,7 @@ export const useAuthOperations = ({
326
327
  clearPersistedAuthSafe(store, logger);
327
328
  await clearSessionState();
328
329
  }
330
+ return { status: 'signed-out' };
329
331
  } catch (error) {
330
332
  const isInvalid = isInvalidSessionError(error);
331
333
 
@@ -337,7 +339,7 @@ export const useAuthOperations = ({
337
339
  // The active session is invalid → full sign-out; clear persisted state.
338
340
  clearPersistedAuthSafe(store, logger);
339
341
  await clearSessionState();
340
- return;
342
+ return { status: 'signed-out' };
341
343
  }
342
344
 
343
345
  handleAuthError(error, {
@@ -348,6 +350,9 @@ export const useAuthOperations = ({
348
350
  logger,
349
351
  status: isInvalid ? 401 : undefined,
350
352
  });
353
+ // Nothing was revoked and the session is still installed here. Say so,
354
+ // rather than letting a caller close its surface as if it had worked.
355
+ return { status: 'failed', error };
351
356
  }
352
357
  },
353
358
  [
@@ -11,6 +11,19 @@ import type {
11
11
  import type { WebAuthMode, WebOAuthSignInResult } from '../oauth/types';
12
12
  import type { StorageInterface } from '../utils/storageHelpers';
13
13
 
14
+ /**
15
+ * The outcome of `logout()`. It never rejects: a caller reads this instead.
16
+ *
17
+ * - `signed-out` — the server confirmed the sign-out, or the session was
18
+ * already invalid and was cleaned up locally. Either way, this device no
19
+ * longer holds it.
20
+ * - `failed` — the revocation did not go through (typically no connection).
21
+ * The session is still installed on this device, and nothing about the
22
+ * outcome may be presented as revoked. The error is also reported through
23
+ * the provider's `onError`, as before.
24
+ */
25
+ export type LogoutResult = { status: 'signed-out' } | { status: 'failed'; error: unknown };
26
+
14
27
  export interface OxyContextState {
15
28
  user: User | null;
16
29
  sessions: ClientSession[];
@@ -75,12 +88,6 @@ export interface OxyContextState {
75
88
  deviceFingerprint?: string;
76
89
  }) => Promise<void>;
77
90
 
78
- /**
79
- * Create a brand-new account whose first authentication method is a passkey.
80
- * WEB-ONLY — throws on native or an unsupported browser.
81
- */
82
- registerWithPasskey: (params: { username: string; deviceName?: string }) => Promise<void>;
83
-
84
91
  /**
85
92
  * Add a passkey to the already-signed-in account (bearer present). Does NOT
86
93
  * commit a new session; refreshes the linked auth-methods list on success.
@@ -125,10 +132,11 @@ export interface OxyContextState {
125
132
  */
126
133
  requestOAuthConsent: (options: RequestOAuthConsentOptions) => Promise<OAuthConsentResult>;
127
134
 
128
- logout: (targetSessionId?: string) => Promise<void>;
135
+ logout: (targetSessionId?: string) => Promise<LogoutResult>;
129
136
  logoutAll: () => Promise<void>;
130
137
  switchSession: (sessionId: string) => Promise<User>;
131
- removeSession: (sessionId: string) => Promise<void>;
138
+ /** The same operation as `logout(sessionId)`, with the same {@link LogoutResult}. */
139
+ removeSession: (sessionId: string) => Promise<LogoutResult>;
132
140
  refreshSessions: () => Promise<void>;
133
141
  setLanguage: (languageId: string) => Promise<void>;
134
142
  getDeviceSessions: () => Promise<
@@ -4,11 +4,11 @@
4
4
  * Extracted from `OxyContext` so the fixed `options → ceremony → verify → commit`
5
5
  * ordering is unit-testable with injected deps — the exact pattern
6
6
  * `commitSessionFlow.ts` (`commitDeviceSetAndResolve`) uses. The context methods
7
- * (`signInWithPasskey` / `registerWithPasskey` / `addPasskey`) are thin wrappers
7
+ * (`signInWithPasskey` / `addPasskey`) are thin wrappers
8
8
  * that supply the real deps: the core `webauthn*` methods, the platform ceremony
9
9
  * client (`webauthn/passkeyClient`), and the internal `commitSession` funnel.
10
10
  *
11
- * All three GATE on `isSupported()` first (the platform client returns `false`
11
+ * Both GATE on `isSupported()` first (the platform client returns `false`
12
12
  * off the web) so an unsupported surface fails loudly before touching a ceremony.
13
13
  */
14
14
 
@@ -84,43 +84,6 @@ export async function runPasskeyLogin(deps: RunPasskeyLoginDeps): Promise<void>
84
84
  await deps.commit(toCommitInput(result));
85
85
  }
86
86
 
87
- /** Injected dependencies for {@link runPasskeyRegister}. */
88
- export interface RunPasskeyRegisterDeps {
89
- isSupported: () => boolean;
90
- getRegisterOptions: (username: string) => Promise<unknown>;
91
- runCeremony: (optionsJSON: unknown) => Promise<unknown>;
92
- registerVerify: (
93
- response: unknown,
94
- envelope: { username: string; deviceName?: string },
95
- ) => Promise<PasskeyRegisterVerifyResult>;
96
- commit: (input: CommitInput) => Promise<void>;
97
- username: string;
98
- deviceName?: string;
99
- }
100
-
101
- /**
102
- * Passkey SIGNUP: create a brand-new account whose first auth method is a
103
- * passkey. The verify signup branch mints a session (a {@link LoginSessionResult}
104
- * carrying `sessionId`), which is committed exactly like a password signup.
105
- */
106
- export async function runPasskeyRegister(deps: RunPasskeyRegisterDeps): Promise<void> {
107
- if (!deps.isSupported()) {
108
- throw new Error(PASSKEY_UNSUPPORTED_MESSAGE);
109
- }
110
- const options = await deps.getRegisterOptions(deps.username);
111
- const response = await deps.runCeremony(options);
112
- const result = await deps.registerVerify(response, {
113
- username: deps.username,
114
- deviceName: deps.deviceName,
115
- });
116
- // Only the signup session arm carries `sessionId`; the link branch
117
- // (`{ success, message }`) does not.
118
- if (!('sessionId' in result)) {
119
- throw new Error('Passkey registration did not establish a session.');
120
- }
121
- await deps.commit(toCommitInput(result));
122
- }
123
-
124
87
  /** Injected dependencies for {@link runPasskeyAdd}. */
125
88
  export interface RunPasskeyAddDeps {
126
89
  isSupported: () => boolean;
@@ -27,11 +27,13 @@ export const EMPTY_ACCOUNT_DIALOG_SNAPSHOT: AccountDialogSnapshot = {
27
27
  qrPayload: null,
28
28
  expiresAt: null,
29
29
  error: null,
30
+ failure: null,
30
31
  route: null,
31
32
  routeFailed: false,
32
33
  pushSentAt: null,
33
34
  openedAt: null,
34
35
  progress: 'idle',
36
+ attempt: 0,
35
37
  },
36
38
  commonsAvailability: 'unknown',
37
39
  };
@@ -31,6 +31,7 @@ export interface ProfileUpdateData {
31
31
  phone?: string;
32
32
  address?: string;
33
33
  birthday?: string;
34
+ dateOfBirth?: string;
34
35
  }
35
36
 
36
37
  type ProfileFieldValue = string | ProfileLocation[] | ProfileLinkMetadata[];
@@ -87,6 +88,13 @@ export const useProfileEditing = () => {
87
88
  if (updates.birthday !== undefined) {
88
89
  updateData.birthday = updates.birthday;
89
90
  }
91
+ if (updates.dateOfBirth !== undefined) {
92
+ // '' clears it — `updateData.dateOfBirth` is `string | null`
93
+ // per the contract, and there is no separate blank sentinel for
94
+ // a structured date the way `blankToNull` gives every free-text
95
+ // field.
96
+ updateData.dateOfBirth = updates.dateOfBirth === '' ? null : updates.dateOfBirth;
97
+ }
90
98
 
91
99
  // Handle name field
92
100
  if (updates.firstName !== undefined || updates.lastName !== undefined) {
@@ -142,6 +150,13 @@ export const useProfileEditing = () => {
142
150
  if (typeof value !== 'string') return false;
143
151
  updates.birthday = value;
144
152
  break;
153
+ case 'dateOfBirth':
154
+ // '' clears it (the picker's "no complete date selected"
155
+ // state) — `updateUserProfile`'s `dateOfBirth` block treats
156
+ // an empty string the same as `null`.
157
+ if (typeof value !== 'string') return false;
158
+ updates.dateOfBirth = value;
159
+ break;
145
160
  case 'location':
146
161
  if (!isProfileLocationArray(value)) return false;
147
162
  updates.locations = value;