@oxy.so/services 6.0.0 → 6.2.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 (172) hide show
  1. package/README.md +1 -1
  2. package/lib/commonjs/index.js +32 -0
  3. package/lib/commonjs/index.js.map +1 -1
  4. package/lib/commonjs/ui/components/OxyAuthChooser.js +8 -8
  5. package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -1
  6. package/lib/commonjs/ui/components/SurfaceScreen.js +21 -12
  7. package/lib/commonjs/ui/components/SurfaceScreen.js.map +1 -1
  8. package/lib/commonjs/ui/components/authChooser/AccountsMenuView.js +1 -8
  9. package/lib/commonjs/ui/components/authChooser/AccountsMenuView.js.map +1 -1
  10. package/lib/commonjs/ui/components/authChooser/types.js.map +1 -1
  11. package/lib/commonjs/ui/components/signIn/OxyAuthScreen.js +11 -5
  12. package/lib/commonjs/ui/components/signIn/OxyAuthScreen.js.map +1 -1
  13. package/lib/commonjs/ui/components/signIn/OxyCreateAccountPanel.js +236 -0
  14. package/lib/commonjs/ui/components/signIn/OxyCreateAccountPanel.js.map +1 -0
  15. package/lib/commonjs/ui/components/signIn/OxyDeleteAccountPanel.js +114 -0
  16. package/lib/commonjs/ui/components/signIn/OxyDeleteAccountPanel.js.map +1 -0
  17. package/lib/commonjs/ui/components/signIn/OxyLinkCommonsPanel.js +275 -0
  18. package/lib/commonjs/ui/components/signIn/OxyLinkCommonsPanel.js.map +1 -0
  19. package/lib/commonjs/ui/components/signIn/OxyRecoverAccountPanel.js +170 -0
  20. package/lib/commonjs/ui/components/signIn/OxyRecoverAccountPanel.js.map +1 -0
  21. package/lib/commonjs/ui/components/signIn/OxySignInPanel.js +45 -32
  22. package/lib/commonjs/ui/components/signIn/OxySignInPanel.js.map +1 -1
  23. package/lib/commonjs/ui/components/signIn/accountFlowParts.js +244 -0
  24. package/lib/commonjs/ui/components/signIn/accountFlowParts.js.map +1 -0
  25. package/lib/commonjs/ui/components/signIn/signInMethods.js +19 -18
  26. package/lib/commonjs/ui/components/signIn/signInMethods.js.map +1 -1
  27. package/lib/commonjs/ui/context/OxyContext.js +4 -4
  28. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  29. package/lib/commonjs/ui/context/passkeyFlow.js +22 -0
  30. package/lib/commonjs/ui/context/passkeyFlow.js.map +1 -1
  31. package/lib/commonjs/ui/hooks/useSurfaceFrameWidth.js +28 -0
  32. package/lib/commonjs/ui/hooks/useSurfaceFrameWidth.js.map +1 -0
  33. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +0 -23
  34. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
  35. package/lib/commonjs/ui/screens/EditProfileScreen.js +0 -8
  36. package/lib/commonjs/ui/screens/EditProfileScreen.js.map +1 -1
  37. package/lib/commonjs/ui/screens/ManageAccountScreen.js +3 -3
  38. package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
  39. package/lib/commonjs/ui/utils/accountDeletionHandoff.js +2 -2
  40. package/lib/commonjs/ui/utils/accountDeletionHandoff.js.map +1 -1
  41. package/lib/module/index.js +7 -0
  42. package/lib/module/index.js.map +1 -1
  43. package/lib/module/ui/components/OxyAuthChooser.js +9 -9
  44. package/lib/module/ui/components/OxyAuthChooser.js.map +1 -1
  45. package/lib/module/ui/components/SurfaceScreen.js +21 -12
  46. package/lib/module/ui/components/SurfaceScreen.js.map +1 -1
  47. package/lib/module/ui/components/authChooser/AccountsMenuView.js +1 -8
  48. package/lib/module/ui/components/authChooser/AccountsMenuView.js.map +1 -1
  49. package/lib/module/ui/components/authChooser/types.js.map +1 -1
  50. package/lib/module/ui/components/signIn/OxyAuthScreen.js +11 -5
  51. package/lib/module/ui/components/signIn/OxyAuthScreen.js.map +1 -1
  52. package/lib/module/ui/components/signIn/OxyCreateAccountPanel.js +231 -0
  53. package/lib/module/ui/components/signIn/OxyCreateAccountPanel.js.map +1 -0
  54. package/lib/module/ui/components/signIn/OxyDeleteAccountPanel.js +109 -0
  55. package/lib/module/ui/components/signIn/OxyDeleteAccountPanel.js.map +1 -0
  56. package/lib/module/ui/components/signIn/OxyLinkCommonsPanel.js +270 -0
  57. package/lib/module/ui/components/signIn/OxyLinkCommonsPanel.js.map +1 -0
  58. package/lib/module/ui/components/signIn/OxyRecoverAccountPanel.js +165 -0
  59. package/lib/module/ui/components/signIn/OxyRecoverAccountPanel.js.map +1 -0
  60. package/lib/module/ui/components/signIn/OxySignInPanel.js +47 -32
  61. package/lib/module/ui/components/signIn/OxySignInPanel.js.map +1 -1
  62. package/lib/module/ui/components/signIn/accountFlowParts.js +235 -0
  63. package/lib/module/ui/components/signIn/accountFlowParts.js.map +1 -0
  64. package/lib/module/ui/components/signIn/signInMethods.js +19 -18
  65. package/lib/module/ui/components/signIn/signInMethods.js.map +1 -1
  66. package/lib/module/ui/context/OxyContext.js +4 -4
  67. package/lib/module/ui/context/OxyContext.js.map +1 -1
  68. package/lib/module/ui/context/passkeyFlow.js +21 -0
  69. package/lib/module/ui/context/passkeyFlow.js.map +1 -1
  70. package/lib/module/ui/hooks/useSurfaceFrameWidth.js +24 -0
  71. package/lib/module/ui/hooks/useSurfaceFrameWidth.js.map +1 -0
  72. package/lib/module/ui/screens/EditProfileFieldScreen.js +1 -24
  73. package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
  74. package/lib/module/ui/screens/EditProfileScreen.js +0 -8
  75. package/lib/module/ui/screens/EditProfileScreen.js.map +1 -1
  76. package/lib/module/ui/screens/ManageAccountScreen.js +3 -3
  77. package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
  78. package/lib/module/ui/utils/accountDeletionHandoff.js +2 -2
  79. package/lib/module/ui/utils/accountDeletionHandoff.js.map +1 -1
  80. package/lib/typescript/commonjs/index.d.ts +8 -0
  81. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts +3 -2
  83. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts.map +1 -1
  85. package/lib/typescript/commonjs/ui/components/authChooser/AccountsMenuView.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts +0 -5
  87. package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/ui/components/signIn/OxyAuthScreen.d.ts +7 -1
  89. package/lib/typescript/commonjs/ui/components/signIn/OxyAuthScreen.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/ui/components/signIn/OxyCreateAccountPanel.d.ts +24 -0
  91. package/lib/typescript/commonjs/ui/components/signIn/OxyCreateAccountPanel.d.ts.map +1 -0
  92. package/lib/typescript/commonjs/ui/components/signIn/OxyDeleteAccountPanel.d.ts +16 -0
  93. package/lib/typescript/commonjs/ui/components/signIn/OxyDeleteAccountPanel.d.ts.map +1 -0
  94. package/lib/typescript/commonjs/ui/components/signIn/OxyLinkCommonsPanel.d.ts +21 -0
  95. package/lib/typescript/commonjs/ui/components/signIn/OxyLinkCommonsPanel.d.ts.map +1 -0
  96. package/lib/typescript/commonjs/ui/components/signIn/OxyRecoverAccountPanel.d.ts +20 -0
  97. package/lib/typescript/commonjs/ui/components/signIn/OxyRecoverAccountPanel.d.ts.map +1 -0
  98. package/lib/typescript/commonjs/ui/components/signIn/OxySignInPanel.d.ts +12 -12
  99. package/lib/typescript/commonjs/ui/components/signIn/OxySignInPanel.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/ui/components/signIn/accountFlowParts.d.ts +60 -0
  101. package/lib/typescript/commonjs/ui/components/signIn/accountFlowParts.d.ts.map +1 -0
  102. package/lib/typescript/commonjs/ui/components/signIn/signInMethods.d.ts +18 -15
  103. package/lib/typescript/commonjs/ui/components/signIn/signInMethods.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  105. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +11 -5
  106. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/ui/context/passkeyFlow.d.ts +16 -1
  108. package/lib/typescript/commonjs/ui/context/passkeyFlow.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/ui/hooks/useSurfaceFrameWidth.d.ts +12 -0
  111. package/lib/typescript/commonjs/ui/hooks/useSurfaceFrameWidth.d.ts.map +1 -0
  112. package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts +1 -1
  113. package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/ui/screens/EditProfileScreen.d.ts.map +1 -1
  115. package/lib/typescript/module/index.d.ts +8 -0
  116. package/lib/typescript/module/index.d.ts.map +1 -1
  117. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts +3 -2
  118. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -1
  119. package/lib/typescript/module/ui/components/SurfaceScreen.d.ts.map +1 -1
  120. package/lib/typescript/module/ui/components/authChooser/AccountsMenuView.d.ts.map +1 -1
  121. package/lib/typescript/module/ui/components/authChooser/types.d.ts +0 -5
  122. package/lib/typescript/module/ui/components/authChooser/types.d.ts.map +1 -1
  123. package/lib/typescript/module/ui/components/signIn/OxyAuthScreen.d.ts +7 -1
  124. package/lib/typescript/module/ui/components/signIn/OxyAuthScreen.d.ts.map +1 -1
  125. package/lib/typescript/module/ui/components/signIn/OxyCreateAccountPanel.d.ts +24 -0
  126. package/lib/typescript/module/ui/components/signIn/OxyCreateAccountPanel.d.ts.map +1 -0
  127. package/lib/typescript/module/ui/components/signIn/OxyDeleteAccountPanel.d.ts +16 -0
  128. package/lib/typescript/module/ui/components/signIn/OxyDeleteAccountPanel.d.ts.map +1 -0
  129. package/lib/typescript/module/ui/components/signIn/OxyLinkCommonsPanel.d.ts +21 -0
  130. package/lib/typescript/module/ui/components/signIn/OxyLinkCommonsPanel.d.ts.map +1 -0
  131. package/lib/typescript/module/ui/components/signIn/OxyRecoverAccountPanel.d.ts +20 -0
  132. package/lib/typescript/module/ui/components/signIn/OxyRecoverAccountPanel.d.ts.map +1 -0
  133. package/lib/typescript/module/ui/components/signIn/OxySignInPanel.d.ts +12 -12
  134. package/lib/typescript/module/ui/components/signIn/OxySignInPanel.d.ts.map +1 -1
  135. package/lib/typescript/module/ui/components/signIn/accountFlowParts.d.ts +60 -0
  136. package/lib/typescript/module/ui/components/signIn/accountFlowParts.d.ts.map +1 -0
  137. package/lib/typescript/module/ui/components/signIn/signInMethods.d.ts +18 -15
  138. package/lib/typescript/module/ui/components/signIn/signInMethods.d.ts.map +1 -1
  139. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  140. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +11 -5
  141. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
  142. package/lib/typescript/module/ui/context/passkeyFlow.d.ts +16 -1
  143. package/lib/typescript/module/ui/context/passkeyFlow.d.ts.map +1 -1
  144. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  145. package/lib/typescript/module/ui/hooks/useSurfaceFrameWidth.d.ts +12 -0
  146. package/lib/typescript/module/ui/hooks/useSurfaceFrameWidth.d.ts.map +1 -0
  147. package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts +1 -1
  148. package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  149. package/lib/typescript/module/ui/screens/EditProfileScreen.d.ts.map +1 -1
  150. package/package.json +4 -4
  151. package/src/index.ts +11 -0
  152. package/src/ui/components/OxyAuthChooser.tsx +9 -9
  153. package/src/ui/components/SurfaceScreen.tsx +19 -11
  154. package/src/ui/components/authChooser/AccountsMenuView.tsx +0 -7
  155. package/src/ui/components/authChooser/types.ts +0 -5
  156. package/src/ui/components/signIn/OxyAuthScreen.tsx +14 -5
  157. package/src/ui/components/signIn/OxyCreateAccountPanel.tsx +207 -0
  158. package/src/ui/components/signIn/OxyDeleteAccountPanel.tsx +106 -0
  159. package/src/ui/components/signIn/OxyLinkCommonsPanel.tsx +222 -0
  160. package/src/ui/components/signIn/OxyRecoverAccountPanel.tsx +154 -0
  161. package/src/ui/components/signIn/OxySignInPanel.tsx +56 -37
  162. package/src/ui/components/signIn/accountFlowParts.tsx +204 -0
  163. package/src/ui/components/signIn/signInMethods.ts +21 -17
  164. package/src/ui/context/OxyContext.tsx +6 -5
  165. package/src/ui/context/__tests__/passkeyFlow.test.ts +59 -1
  166. package/src/ui/context/oxyContextTypes.ts +11 -5
  167. package/src/ui/context/passkeyFlow.ts +28 -0
  168. package/src/ui/hooks/useSurfaceFrameWidth.ts +21 -0
  169. package/src/ui/screens/EditProfileFieldScreen.tsx +1 -27
  170. package/src/ui/screens/EditProfileScreen.tsx +0 -6
  171. package/src/ui/screens/ManageAccountScreen.tsx +3 -3
  172. package/src/ui/utils/accountDeletionHandoff.ts +2 -2
@@ -0,0 +1,204 @@
1
+ /**
2
+ * The pieces auth.oxy.so's account pages share (ADR 0029 D3): creating a
3
+ * passkey account, recovering one, deleting one. Each is a page of the IdP
4
+ * built from the same sign-in shell as `OxySignInPanel`, so they read as one
5
+ * product; the passkey belongs to auth.oxy.so, so none of them runs anywhere
6
+ * else.
7
+ */
8
+
9
+ import type React from 'react';
10
+ import { useState } from 'react';
11
+ import { StyleSheet, View } from 'react-native';
12
+ import { Button } from '@oxy.so/bloom/button';
13
+ import { useTheme } from '@oxy.so/bloom/theme';
14
+ import { TextField, TextFieldHint, TextFieldInput, TextFieldLabel } from '@oxy.so/bloom/text-field';
15
+ import { Text } from '@oxy.so/bloom/typography';
16
+ import { EMAIL_CODE_LENGTH, EMAIL_VERIFICATION_ERROR_CODES, type EmailVerificationConfirmResponse } from '@oxy.so/contracts';
17
+ import { useOxy } from '../../context/OxyContext';
18
+ import { useI18n } from '../../hooks/useI18n';
19
+ import type { Translate } from '../authChooser/types';
20
+ import { SubtleLink } from '../authChooser/primitives';
21
+ import { OxyAuthScreen, OxyAuthScreenHeader } from './OxyAuthScreen';
22
+ import { describePasskeyError, isRateLimited } from './passkeyError';
23
+
24
+ /** The API error code a thrown SDK error carries (`error.code`), if any. */
25
+ function errorCode(error: unknown): string | undefined {
26
+ const code = (error as { code?: unknown } | null)?.code;
27
+ return typeof code === 'string' ? code : undefined;
28
+ }
29
+
30
+ /** A failure of an account page, in the person's language. */
31
+ export function describeAccountFlowError(error: unknown, t: Translate): string {
32
+ switch (errorCode(error)) {
33
+ case EMAIL_VERIFICATION_ERROR_CODES.codeInvalid:
34
+ return t('emailCode.errors.codeInvalid');
35
+ case EMAIL_VERIFICATION_ERROR_CODES.tooManyAttempts:
36
+ return t('emailCode.errors.tooManyAttempts');
37
+ case EMAIL_VERIFICATION_ERROR_CODES.ticketInvalid:
38
+ case EMAIL_VERIFICATION_ERROR_CODES.ticketRequired:
39
+ return t('emailCode.errors.expired');
40
+ case EMAIL_VERIFICATION_ERROR_CODES.unavailable:
41
+ return t('emailCode.errors.unavailable');
42
+ default:
43
+ if (isRateLimited(error)) return t('emailCode.errors.rateLimited');
44
+ return describePasskeyError(error, t);
45
+ }
46
+ }
47
+
48
+ /** Whether the confirmation this flow holds is gone and it must start again. */
49
+ export function isTicketExpired(error: unknown): boolean {
50
+ const code = errorCode(error);
51
+ return code === EMAIL_VERIFICATION_ERROR_CODES.ticketInvalid || code === EMAIL_VERIFICATION_ERROR_CODES.ticketRequired;
52
+ }
53
+
54
+ /** One labelled input, the way the sign-in screen draws its username. */
55
+ export const AccountFlowField: React.FC<{
56
+ label: string;
57
+ value: string;
58
+ onChange: (value: string) => void;
59
+ onSubmit: () => void;
60
+ error: string | null;
61
+ disabled?: boolean;
62
+ placeholder?: string;
63
+ autoComplete?: 'username' | 'email' | 'one-time-code' | 'off';
64
+ keyboardType?: 'default' | 'email-address' | 'number-pad';
65
+ maxLength?: number;
66
+ testID: string;
67
+ }> = ({ label, value, onChange, onSubmit, error, disabled, placeholder, autoComplete, keyboardType, maxLength, testID }) => (
68
+ <View style={styles.field}>
69
+ <TextFieldLabel>{label}</TextFieldLabel>
70
+ <TextField invalid={error !== null} disabled={disabled} radius={999} style={styles.input}>
71
+ <TextFieldInput
72
+ testID={testID}
73
+ label={label}
74
+ value={value}
75
+ onValueChange={onChange}
76
+ placeholder={placeholder}
77
+ autoComplete={autoComplete}
78
+ keyboardType={keyboardType}
79
+ maxLength={maxLength}
80
+ autoCapitalize="none"
81
+ autoCorrect={false}
82
+ autoFocus
83
+ returnKeyType="go"
84
+ onSubmitEditing={onSubmit}
85
+ aria-required
86
+ />
87
+ </TextField>
88
+ {error ? <TextFieldHint invalid>{error}</TextFieldHint> : null}
89
+ </View>
90
+ );
91
+
92
+ /** The screen's one primary action. */
93
+ export const AccountFlowAction: React.FC<{
94
+ label: string;
95
+ onPress: () => void;
96
+ pending: boolean;
97
+ disabled?: boolean;
98
+ testID: string;
99
+ }> = ({ label, onPress, pending, disabled, testID }) => (
100
+ <Button appearance="solid" tone="action" size="lg" fullWidth loading={pending} disabled={pending || disabled} onPress={onPress} testID={testID}>
101
+ {label}
102
+ </Button>
103
+ );
104
+
105
+ /** A line of body copy under the header. */
106
+ export const AccountFlowNote: React.FC<{ children: React.ReactNode }> = ({ children }) => {
107
+ const theme = useTheme();
108
+ return <Text style={[styles.note, { color: theme.colors.textSecondary }]}>{children}</Text>;
109
+ };
110
+
111
+ /** Why a step failed, where it happened. */
112
+ export const AccountFlowErrorLine: React.FC<{ message: string }> = ({ message }) => {
113
+ const theme = useTheme();
114
+ return (
115
+ <Text accessibilityRole="alert" style={[styles.note, { color: theme.colors.error }]}>
116
+ {message}
117
+ </Text>
118
+ );
119
+ };
120
+
121
+ export interface EmailCodeStepProps {
122
+ /** What was sent, and where (or, for a recovery, where it may have gone). */
123
+ description: string;
124
+ verificationId: string;
125
+ onConfirmed: (confirmed: EmailVerificationConfirmResponse) => void;
126
+ /** Send a new code; resolves once it is on its way. */
127
+ onResend: () => Promise<void>;
128
+ /** "Use another email" / "Back" — the step before. */
129
+ back: { label: string; onPress: () => void };
130
+ }
131
+
132
+ /** "Check your email": the 6-digit code a verification sent. */
133
+ export const EmailCodeStep: React.FC<EmailCodeStepProps> = ({ description, verificationId, onConfirmed, onResend, back }) => {
134
+ const theme = useTheme();
135
+ const { t } = useI18n();
136
+ const { oxyServices } = useOxy();
137
+ const [code, setCode] = useState('');
138
+ const [pending, setPending] = useState(false);
139
+ const [error, setError] = useState<string | null>(null);
140
+ const [notice, setNotice] = useState<string | null>(null);
141
+
142
+ const confirm = () => {
143
+ const digits = code.replace(/\D/g, '');
144
+ if (digits.length !== EMAIL_CODE_LENGTH || pending) {
145
+ setError(t('emailCode.errors.codeInvalid'));
146
+ return;
147
+ }
148
+ setError(null);
149
+ setPending(true);
150
+ oxyServices
151
+ .confirmEmailVerification(verificationId, digits)
152
+ .then(onConfirmed)
153
+ .catch((reason: unknown) => setError(describeAccountFlowError(reason, t)))
154
+ .finally(() => setPending(false));
155
+ };
156
+
157
+ const resend = () => {
158
+ setError(null);
159
+ setNotice(null);
160
+ setCode('');
161
+ onResend()
162
+ .then(() => setNotice(t('emailCode.resent')))
163
+ .catch((reason: unknown) => setError(describeAccountFlowError(reason, t)));
164
+ };
165
+
166
+ return (
167
+ <OxyAuthScreen>
168
+ <OxyAuthScreenHeader title={t('emailCode.title')} description={description} />
169
+ <AccountFlowField
170
+ label={t('emailCode.label')}
171
+ value={code}
172
+ onChange={(value) => {
173
+ setCode(value);
174
+ if (error) setError(null);
175
+ }}
176
+ onSubmit={confirm}
177
+ error={error}
178
+ disabled={pending}
179
+ placeholder="000000"
180
+ autoComplete="one-time-code"
181
+ keyboardType="number-pad"
182
+ maxLength={EMAIL_CODE_LENGTH}
183
+ testID="email-code"
184
+ />
185
+ {notice ? <Text style={[styles.note, { color: theme.colors.textSecondary }]}>{notice}</Text> : null}
186
+ <AccountFlowAction label={t('signin.actions.continue')} onPress={confirm} pending={pending} testID="email-code-continue" />
187
+ <SubtleLink label={t('emailCode.resend')} theme={theme} onPress={resend} disabled={pending} testID="email-code-resend" />
188
+ <SubtleLink label={back.label} theme={theme} onPress={back.onPress} disabled={pending} testID="email-code-back" />
189
+ </OxyAuthScreen>
190
+ );
191
+ };
192
+
193
+ const styles = StyleSheet.create({
194
+ field: {
195
+ gap: 6,
196
+ },
197
+ input: {
198
+ height: 40,
199
+ },
200
+ note: {
201
+ fontSize: 16,
202
+ lineHeight: 24,
203
+ },
204
+ });
@@ -1,34 +1,38 @@
1
1
  /**
2
2
  * Which sign-in methods a surface offers, from where it runs.
3
3
  *
4
- * One screen everywhere; only the transport behind each block changes:
4
+ * One screen everywhere, and sign-in happens IN it (ADR 0029 D1):
5
5
  *
6
- * - auth.oxy.so (`page`) is where the web signs in: the embedded Commons QR
7
- * (the split card's right column from `md`, "Continue with Oxy" below it),
8
- * the username with its Continue, and the passkey — asserted right here,
9
- * on the one origin every Oxy passkey belongs to;
10
- * - an app's account dialog on the web (`dialog`) opens that screen in a
11
- * window over the app ("Continue with Oxy"), like "Sign in with Google":
12
- * the browser's session lives on auth.oxy.so, so every Oxy app shares it;
13
- * - native signs in with Commons: "Continue with Oxy", or "Get Commons" on a
14
- * device that has no Commons to continue with.
6
+ * - the COMMONS way in — on the web the embedded QR (the split card's right
7
+ * column from `md`, with "Continue with Oxy" below `md`, where the screen
8
+ * is the phone that would scan it); on native "Continue with Oxy", and
9
+ * "Get Commons" on a device that has no Commons to continue with;
10
+ * - the PASSKEY — it belongs to `oxy.so`, so it runs right here on auth.oxy.so
11
+ * (`page`: the username with its Continue, and the discoverable passkey), and
12
+ * in an app's dialog on the web it opens auth.oxy.so's window for that one
13
+ * step (with account creation); native has none, Commons holds the identity.
15
14
  */
16
15
 
17
16
  import type { CommonsAvailability } from '@oxy.so/core';
18
17
 
19
18
  /**
20
- * `qr` — auth.oxy.so: the embedded QR from `md`, "Continue with Oxy" below.
21
- * `window` — an app on the web: "Continue with Oxy" opens auth.oxy.so's window.
19
+ * `qr` — the web: the embedded QR from `md`, "Continue with Oxy" below.
22
20
  * `continue` — native "Continue with Oxy": Oxy picks the route (shared
23
21
  * keychain, Commons on this device, a push, or the QR view).
24
22
  * `get-commons` — native, and Commons is not installed here.
25
23
  */
26
- export type CommonsEntry = 'qr' | 'window' | 'continue' | 'get-commons';
24
+ export type CommonsEntry = 'qr' | 'continue' | 'get-commons';
25
+
26
+ /**
27
+ * `here` — auth.oxy.so: the username and the passkey run on this page.
28
+ * `window` — an app's dialog on the web: the passkey opens auth.oxy.so's window.
29
+ * `none` — native.
30
+ */
31
+ export type PasskeyEntry = 'here' | 'window' | 'none';
27
32
 
28
33
  export interface SignInMethods {
29
34
  commons: CommonsEntry;
30
- /** The username and passkey block: only on auth.oxy.so. */
31
- passkey: boolean;
35
+ passkey: PasskeyEntry;
32
36
  }
33
37
 
34
38
  export interface SignInSurfaceFacts {
@@ -42,10 +46,10 @@ export interface SignInSurfaceFacts {
42
46
 
43
47
  export function resolveSignInMethods(facts: SignInSurfaceFacts): SignInMethods {
44
48
  if (facts.web) {
45
- return facts.host === 'page' ? { commons: 'qr', passkey: true } : { commons: 'window', passkey: false };
49
+ return { commons: 'qr', passkey: facts.host === 'page' ? 'here' : 'window' };
46
50
  }
47
51
  return {
48
52
  commons: facts.commonsAvailability === 'unavailable' ? 'get-commons' : 'continue',
49
- passkey: false,
53
+ passkey: 'none',
50
54
  };
51
55
  }
@@ -10,6 +10,7 @@ import {
10
10
  } from 'react';
11
11
  import { Linking, Platform } from 'react-native';
12
12
  import { DeviceManager, OxyServices, oxyClient, type OxyAuthScreen } from '@oxy.so/core';
13
+ import type { LoginSessionResult } from '@oxy.so/contracts';
13
14
  import type {
14
15
  User,
15
16
  SessionLoginResponse,
@@ -720,7 +721,7 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
720
721
  // Public `handleWebSession`: commit a session from the QR device flow. It is a
721
722
  // deliberate sign-in on THIS device, so it activates the account.
722
723
  const handleWebSession = useCallback(
723
- async (session: SessionLoginResponse): Promise<void> => {
724
+ async (session: SessionLoginResponse | LoginSessionResult): Promise<void> => {
724
725
  if (!session?.user || !session?.sessionId || !session.accessToken) {
725
726
  throw new Error('Session response did not include a usable session');
726
727
  }
@@ -784,10 +785,10 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
784
785
  [webAuthMode, oxyServices, clientId, authorizeBaseUrl, isIdentityBound, user?.id],
785
786
  );
786
787
 
787
- // On the web an Oxy app signs in and creates accounts in auth.oxy.so's
788
- // window over the app, like "Sign in with Google": the passkey belongs to
789
- // that origin, and the browser's session lives there, shared by every Oxy
790
- // app. A blocked window falls back to the same page in this tab.
788
+ // What only auth.oxy.so can do for an Oxy app on the web — assert the
789
+ // `oxy.so` passkey, create or recover a passkey account — runs in its window
790
+ // over the app, for that one step. A blocked window falls back to the same
791
+ // page in this tab.
791
792
  const continueOnAuth = useCallback(
792
793
  (screen: OxyAuthScreen): Promise<WebOAuthSignInResult> =>
793
794
  startWebOAuthSignInForContext({
@@ -12,7 +12,8 @@
12
12
  import type { LoginSessionResult } from '@oxy.so/contracts';
13
13
  import {
14
14
  runPasskeyLogin,
15
- runPasskeyAdd,
15
+ runPasskeyAdd,
16
+ runPasskeyRegisterSignIn,
16
17
  PASSKEY_UNSUPPORTED_MESSAGE,
17
18
  type RunPasskeyLoginDeps,
18
19
  type RunPasskeyAddDeps,
@@ -179,3 +180,60 @@ describe('runPasskeyAdd', () => {
179
180
  expect(deps.onLinked).not.toHaveBeenCalled();
180
181
  });
181
182
  });
183
+
184
+ describe('runPasskeyRegisterSignIn', () => {
185
+ it('runs options → ceremony → verify → commit, and commits the session it minted', async () => {
186
+ const order: string[] = [];
187
+ const commit = jest.fn(async (result: LoginSessionResult) => {
188
+ order.push('commit');
189
+ expect(result).toBe(sessionResult);
190
+ });
191
+ await runPasskeyRegisterSignIn({
192
+ isSupported: () => true,
193
+ getRegisterOptions: async () => {
194
+ order.push('register-options');
195
+ return { challenge: 'c' };
196
+ },
197
+ runCeremony: async (options) => {
198
+ order.push('ceremony');
199
+ expect(options).toEqual({ challenge: 'c' });
200
+ return { id: 'cred' };
201
+ },
202
+ registerVerify: async (response) => {
203
+ order.push('verify');
204
+ expect(response).toEqual({ id: 'cred' });
205
+ return sessionResult;
206
+ },
207
+ commit,
208
+ });
209
+ expect(order).toEqual(['register-options', 'ceremony', 'verify', 'commit']);
210
+ });
211
+
212
+ it('refuses a verify that linked instead of signing in', async () => {
213
+ const commit = jest.fn();
214
+ await expect(
215
+ runPasskeyRegisterSignIn({
216
+ isSupported: () => true,
217
+ getRegisterOptions: async () => ({}),
218
+ runCeremony: async () => ({}),
219
+ registerVerify: async () => linkResult,
220
+ commit,
221
+ }),
222
+ ).rejects.toThrow('without signing in');
223
+ expect(commit).not.toHaveBeenCalled();
224
+ });
225
+
226
+ it('fails before any ceremony where passkeys are unsupported', async () => {
227
+ const getRegisterOptions = jest.fn();
228
+ await expect(
229
+ runPasskeyRegisterSignIn({
230
+ isSupported: () => false,
231
+ getRegisterOptions,
232
+ runCeremony: jest.fn(),
233
+ registerVerify: jest.fn(),
234
+ commit: jest.fn(),
235
+ }),
236
+ ).rejects.toThrow(PASSKEY_UNSUPPORTED_MESSAGE);
237
+ expect(getRegisterOptions).not.toHaveBeenCalled();
238
+ });
239
+ });
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
+ import type { LoginSessionResult } from '@oxy.so/contracts';
2
3
  import type { OxyServices, User, SessionLoginResponse, AccountNode, CreateAccountInput, ClientSession, AccountDialogController, AccountDialogView, ApiError, SessionClient, SessionMode, OxyAuthScreen } from '@oxy.so/core';
3
4
  import type { UseFollowHook } from '../hooks/useFollow.types';
4
5
  import type { useLanguageManagement } from '../hooks/useLanguageManagement';
@@ -104,7 +105,12 @@ export interface OxyContextState {
104
105
  removePasskey: (credentialId: string) => Promise<void>;
105
106
 
106
107
  revokeSuspiciousSignIn: () => Promise<void>;
107
- handleWebSession: (session: SessionLoginResponse) => Promise<void>;
108
+ /**
109
+ * Commit a session a first-party sign-in minted on THIS origin — an OAuth
110
+ * code exchange, or a passkey registration on auth.oxy.so (sign-up,
111
+ * recovery) — as the active one.
112
+ */
113
+ handleWebSession: (session: SessionLoginResponse | LoginSessionResult) => Promise<void>;
108
114
 
109
115
  /**
110
116
  * Run a WEB third-party OAuth sign-in (authorization code + PKCE) end to end
@@ -125,10 +131,10 @@ export interface OxyContextState {
125
131
  startWebOAuthSignIn: (options: StartWebOAuthSignInOptions) => Promise<WebOAuthSignInResult>;
126
132
 
127
133
  /**
128
- * Sign in (`signin`), create an account (`signup`) or recover one
129
- * (`recover`) in auth.oxy.so's window over the app, where the browser's
130
- * shared Oxy session lives. Web only; call it from the press, so the window
131
- * opens with the gesture.
134
+ * The passkey sign-in (`signin`), account creation (`signup`) or recovery
135
+ * (`recover`) in auth.oxy.so's window over the app — the one step the
136
+ * account dialog cannot run itself, since the passkey belongs to `oxy.so`.
137
+ * Web only; call it from the press, so the window opens with the gesture.
132
138
  */
133
139
  continueOnAuth: (screen: OxyAuthScreen) => Promise<WebOAuthSignInResult>;
134
140
 
@@ -115,3 +115,31 @@ export async function runPasskeyAdd(deps: RunPasskeyAddDeps): Promise<void> {
115
115
  }
116
116
  deps.onLinked();
117
117
  }
118
+
119
+ /** Injected dependencies for {@link runPasskeyRegisterSignIn}. */
120
+ export interface RunPasskeyRegisterSignInDeps {
121
+ isSupported: () => boolean;
122
+ getRegisterOptions: () => Promise<unknown>;
123
+ runCeremony: (optionsJSON: unknown) => Promise<unknown>;
124
+ registerVerify: (response: unknown) => Promise<PasskeyRegisterVerifyResult>;
125
+ commit: (result: LoginSessionResult) => Promise<void>;
126
+ }
127
+
128
+ /**
129
+ * Register a passkey SIGNED OUT and sign in with the account it belongs to —
130
+ * a new passkey account (username + confirmed recovery email), or a recovery's
131
+ * new passkey (ADR 0029 D3). Options, ceremony, verify, then commit; the verify
132
+ * must mint a session, since there is no bearer to link to.
133
+ */
134
+ export async function runPasskeyRegisterSignIn(deps: RunPasskeyRegisterSignInDeps): Promise<void> {
135
+ if (!deps.isSupported()) {
136
+ throw new Error(PASSKEY_UNSUPPORTED_MESSAGE);
137
+ }
138
+ const options = await deps.getRegisterOptions();
139
+ const response = await deps.runCeremony(options);
140
+ const result = await deps.registerVerify(response);
141
+ if (!('sessionId' in result)) {
142
+ throw new Error('The passkey was registered without signing in.');
143
+ }
144
+ await deps.commit(result);
145
+ }
@@ -0,0 +1,21 @@
1
+ import { createContext, useContext, useLayoutEffect } from 'react';
2
+
3
+ /**
4
+ * A screen's say in how wide its surface is.
5
+ *
6
+ * `SurfaceScreen` is the ONE host that declares the surface's dialog frame, so
7
+ * a screen whose views need different widths (the account dialog: the 420 menu,
8
+ * the sign-in's 880 split card) hands its width to that host rather than
9
+ * declaring a second frame. A change morphs the centered card to it.
10
+ */
11
+ export const SurfaceFrameWidthContext = createContext<((maxWidth: number | null) => void) | null>(null);
12
+
13
+ /** Ask the surface for `maxWidth` (px) while this is mounted; `null` keeps the surface's own. */
14
+ export function useSurfaceFrameWidth(maxWidth: number | null): void {
15
+ const setWidth = useContext(SurfaceFrameWidthContext);
16
+ useLayoutEffect(() => {
17
+ if (!setWidth) return;
18
+ setWidth(maxWidth);
19
+ return () => setWidth(null);
20
+ }, [setWidth, maxWidth]);
21
+ }
@@ -30,7 +30,7 @@ import { SurfaceHeaderAction } from '../components/SurfaceHeaderAction';
30
30
  import { useOxy } from '../context/OxyContext';
31
31
  import { useProfileEditing } from '../hooks/useProfileEditing';
32
32
  import { toast } from '@oxy.so/bloom/toast';
33
- import { EMAIL_REGEX, DISPLAY_NAME_INVALID_MESSAGE, isValidDisplayName } from '@oxy.so/core';
33
+ import { DISPLAY_NAME_INVALID_MESSAGE, isValidDisplayName } from '@oxy.so/core';
34
34
  import { getLinkTitle, getLinkDescription, linksToListItems } from './linkFormat';
35
35
 
36
36
  /**
@@ -39,7 +39,6 @@ import { getLinkTitle, getLinkDescription, linksToListItems } from './linkFormat
39
39
  export type ProfileFieldType =
40
40
  | 'displayName'
41
41
  | 'username'
42
- | 'email'
43
42
  | 'bio'
44
43
  | 'phone'
45
44
  | 'address'
@@ -214,7 +213,6 @@ function buildInitialProfileState(
214
213
  fieldValues.address = String(userData.address || '');
215
214
  break;
216
215
  case 'username':
217
- case 'email':
218
216
  case 'bio':
219
217
  case 'phone':
220
218
  fieldValues[fieldType] = String(userData[fieldType] || '');
@@ -318,30 +316,6 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
318
316
  },
319
317
  ],
320
318
  };
321
- case 'email':
322
- return {
323
- title: t('editProfile.items.email.title') || 'Email',
324
- subtitle: t('editProfile.items.email.subtitle') || 'Your primary email address',
325
- fields: [
326
- {
327
- key: 'email',
328
- label: t('editProfile.items.email.label') || 'Email Address',
329
- placeholder: t('editProfile.items.email.placeholder') || 'Enter your email address',
330
- type: 'email',
331
- validation: (value) => {
332
- if (!EMAIL_REGEX.test(value)) {
333
- return t('editProfile.items.email.invalid') || 'Please enter a valid email address';
334
- }
335
- return undefined;
336
- },
337
- inputProps: {
338
- keyboardType: 'email-address',
339
- autoCapitalize: 'none',
340
- autoCorrect: false,
341
- },
342
- },
343
- ],
344
- };
345
319
  case 'bio':
346
320
  return {
347
321
  title: t('editProfile.items.bio.title') || 'Bio',
@@ -89,12 +89,6 @@ const EditProfileScreen: React.FC<BaseScreenProps> = ({ navigate }) => {
89
89
  description={user?.username ? `@${user.username}` : notSet}
90
90
  onPress={() => goToField('username')}
91
91
  />
92
- <SettingsListItem
93
- icon={<SettingsIcon name="email" color={bloomTheme.colors.success} />}
94
- title={t('editProfile.items.email.title') || 'Email'}
95
- description={user?.email || notSet}
96
- onPress={() => goToField('email')}
97
- />
98
92
  <SettingsListItem
99
93
  icon={<SettingsIcon name="phone" color={bloomTheme.colors.warning} />}
100
94
  title={t('editProfile.items.phone.title') || 'Phone Number'}
@@ -323,10 +323,10 @@ const ManageAccountScreen: React.FC<BaseScreenProps> = ({
323
323
  );
324
324
  return;
325
325
  }
326
- // Web: deleting an account is signed with its identity key, which only
327
- // auth.oxy.so can unseal — the deletion happens there, never on this page.
326
+ // Web: a passkey account confirms its deletion with its passkey, which
327
+ // only auth.oxy.so asserts — the deletion happens there, never on this page.
328
328
  if (isWebBrowser()) {
329
- await Linking.openURL(`${AUTH_WEB_ORIGIN}/identity`).catch(() => {
329
+ await Linking.openURL(`${AUTH_WEB_ORIGIN}/delete-account`).catch(() => {
330
330
  toast.error(t('accountSwitcher.linkOpenFailed'));
331
331
  });
332
332
  return;
@@ -100,12 +100,12 @@ export async function runAccountDeletionHandoff(
100
100
  return 'handled';
101
101
  }
102
102
 
103
- const site = `${AUTH_WEB_ORIGIN.replace(/^https?:\/\//, '')}/identity`;
103
+ const site = `${AUTH_WEB_ORIGIN.replace(/^https?:\/\//, '')}/delete-account`;
104
104
  await surfaces.confirm({
105
105
  title: text('deleteAccount.handoff.elsewhereTitle', 'Delete your account where your identity is'),
106
106
  description: text(
107
107
  'deleteAccount.handoff.elsewhereMessage',
108
- `Deleting your account needs your identity key, and it isn't on this device. Open Oxy Commons on the device that holds your identity and go to Settings > Delete account, or open ${site} in a browser that holds it.`,
108
+ `Deleting your account needs your identity key, and it isn't on this device. Open Oxy Commons on the device that holds it and go to Settings > Delete account. An account that uses a passkey is deleted at ${site}.`,
109
109
  { site },
110
110
  ),
111
111
  confirmLabel: text('deleteAccount.handoff.gotIt', 'Got it'),