@oxy.so/services 12.3.0 → 12.4.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.
- package/README.md +1 -1
- package/lib/commonjs/index.js +17 -2
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/OxyAuthenticatorPanel.js +11 -10
- package/lib/commonjs/ui/components/signIn/OxyAuthenticatorPanel.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/OxyDeleteAccountPanel.js +19 -18
- package/lib/commonjs/ui/components/signIn/OxyDeleteAccountPanel.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/OxyEmailPanel.js +251 -0
- package/lib/commonjs/ui/components/signIn/OxyEmailPanel.js.map +1 -0
- package/lib/commonjs/ui/components/signIn/OxyLinkCommonsPanel.js +40 -33
- package/lib/commonjs/ui/components/signIn/OxyLinkCommonsPanel.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/OxyPasswordPanel.js +51 -19
- package/lib/commonjs/ui/components/signIn/OxyPasswordPanel.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/OxySignInSecurityPanel.js +154 -0
- package/lib/commonjs/ui/components/signIn/OxySignInSecurityPanel.js.map +1 -0
- package/lib/commonjs/ui/components/signIn/ReauthStep.js +6 -4
- package/lib/commonjs/ui/components/signIn/ReauthStep.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/SessionsSection.js +200 -0
- package/lib/commonjs/ui/components/signIn/SessionsSection.js.map +1 -0
- package/lib/commonjs/ui/components/signIn/accountFlowParts.js +6 -0
- package/lib/commonjs/ui/components/signIn/accountFlowParts.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/accountSecurityState.js +74 -0
- package/lib/commonjs/ui/components/signIn/accountSecurityState.js.map +1 -0
- package/lib/commonjs/ui/navigation/routes.js +1 -1
- package/lib/commonjs/ui/navigation/routes.js.map +1 -1
- package/lib/commonjs/ui/navigation/screenComponents.js +4 -0
- package/lib/commonjs/ui/navigation/screenComponents.js.map +1 -1
- package/lib/commonjs/ui/navigation/surfaceRegistry.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSecurityPanelScreen.js +1 -1
- package/lib/commonjs/ui/screens/ManageAccountScreen.js +8 -196
- package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/SignInEmailScreen.js +19 -0
- package/lib/commonjs/ui/screens/SignInEmailScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/SignInSecurityScreen.js +30 -0
- package/lib/commonjs/ui/screens/SignInSecurityScreen.js.map +1 -0
- package/lib/module/index.js +5 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/signIn/OxyAuthenticatorPanel.js +12 -11
- package/lib/module/ui/components/signIn/OxyAuthenticatorPanel.js.map +1 -1
- package/lib/module/ui/components/signIn/OxyDeleteAccountPanel.js +20 -19
- package/lib/module/ui/components/signIn/OxyDeleteAccountPanel.js.map +1 -1
- package/lib/module/ui/components/signIn/OxyEmailPanel.js +246 -0
- package/lib/module/ui/components/signIn/OxyEmailPanel.js.map +1 -0
- package/lib/module/ui/components/signIn/OxyLinkCommonsPanel.js +40 -33
- package/lib/module/ui/components/signIn/OxyLinkCommonsPanel.js.map +1 -1
- package/lib/module/ui/components/signIn/OxyPasswordPanel.js +52 -20
- package/lib/module/ui/components/signIn/OxyPasswordPanel.js.map +1 -1
- package/lib/module/ui/components/signIn/OxySignInSecurityPanel.js +149 -0
- package/lib/module/ui/components/signIn/OxySignInSecurityPanel.js.map +1 -0
- package/lib/module/ui/components/signIn/ReauthStep.js +6 -4
- package/lib/module/ui/components/signIn/ReauthStep.js.map +1 -1
- package/lib/module/ui/components/signIn/SessionsSection.js +194 -0
- package/lib/module/ui/components/signIn/SessionsSection.js.map +1 -0
- package/lib/module/ui/components/signIn/accountFlowParts.js +6 -0
- package/lib/module/ui/components/signIn/accountFlowParts.js.map +1 -1
- package/lib/module/ui/components/signIn/accountSecurityState.js +69 -0
- package/lib/module/ui/components/signIn/accountSecurityState.js.map +1 -0
- package/lib/module/ui/navigation/routes.js +1 -1
- package/lib/module/ui/navigation/routes.js.map +1 -1
- package/lib/module/ui/navigation/screenComponents.js +4 -0
- package/lib/module/ui/navigation/screenComponents.js.map +1 -1
- package/lib/module/ui/navigation/surfaceRegistry.js.map +1 -1
- package/lib/module/ui/screens/AccountSecurityPanelScreen.js +1 -1
- package/lib/module/ui/screens/ManageAccountScreen.js +9 -197
- package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/module/ui/screens/SignInEmailScreen.js +17 -0
- package/lib/module/ui/screens/SignInEmailScreen.js.map +1 -0
- package/lib/module/ui/screens/SignInSecurityScreen.js +26 -0
- package/lib/module/ui/screens/SignInSecurityScreen.js.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +4 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxyAuthenticatorPanel.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxyDeleteAccountPanel.d.ts +3 -2
- package/lib/typescript/commonjs/ui/components/signIn/OxyDeleteAccountPanel.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxyEmailPanel.d.ts +21 -0
- package/lib/typescript/commonjs/ui/components/signIn/OxyEmailPanel.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/signIn/OxyLinkCommonsPanel.d.ts +2 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxyLinkCommonsPanel.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxyPasswordPanel.d.ts +5 -4
- package/lib/typescript/commonjs/ui/components/signIn/OxyPasswordPanel.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxySignInSecurityPanel.d.ts +25 -0
- package/lib/typescript/commonjs/ui/components/signIn/OxySignInSecurityPanel.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/signIn/ReauthStep.d.ts +8 -2
- package/lib/typescript/commonjs/ui/components/signIn/ReauthStep.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/SessionsSection.d.ts +9 -0
- package/lib/typescript/commonjs/ui/components/signIn/SessionsSection.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/signIn/accountFlowParts.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/accountSecurityState.d.ts +26 -0
- package/lib/typescript/commonjs/ui/components/signIn/accountSecurityState.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/screenComponents.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts +2 -0
- package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/AccountSecurityPanelScreen.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts +3 -2
- package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/SignInEmailScreen.d.ts +6 -0
- package/lib/typescript/commonjs/ui/screens/SignInEmailScreen.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/SignInSecurityScreen.d.ts +6 -0
- package/lib/typescript/commonjs/ui/screens/SignInSecurityScreen.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +4 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/OxyAuthenticatorPanel.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/OxyDeleteAccountPanel.d.ts +3 -2
- package/lib/typescript/module/ui/components/signIn/OxyDeleteAccountPanel.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/OxyEmailPanel.d.ts +21 -0
- package/lib/typescript/module/ui/components/signIn/OxyEmailPanel.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/signIn/OxyLinkCommonsPanel.d.ts +2 -1
- package/lib/typescript/module/ui/components/signIn/OxyLinkCommonsPanel.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/OxyPasswordPanel.d.ts +5 -4
- package/lib/typescript/module/ui/components/signIn/OxyPasswordPanel.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/OxySignInSecurityPanel.d.ts +25 -0
- package/lib/typescript/module/ui/components/signIn/OxySignInSecurityPanel.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/signIn/ReauthStep.d.ts +8 -2
- package/lib/typescript/module/ui/components/signIn/ReauthStep.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/SessionsSection.d.ts +9 -0
- package/lib/typescript/module/ui/components/signIn/SessionsSection.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/signIn/accountFlowParts.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/accountSecurityState.d.ts +26 -0
- package/lib/typescript/module/ui/components/signIn/accountSecurityState.d.ts.map +1 -0
- package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
- package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/screenComponents.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts +2 -0
- package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/AccountSecurityPanelScreen.d.ts +1 -1
- package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts +3 -2
- package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/SignInEmailScreen.d.ts +6 -0
- package/lib/typescript/module/ui/screens/SignInEmailScreen.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/SignInSecurityScreen.d.ts +6 -0
- package/lib/typescript/module/ui/screens/SignInSecurityScreen.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/index.ts +7 -2
- package/src/ui/components/signIn/OxyAuthenticatorPanel.tsx +11 -8
- package/src/ui/components/signIn/OxyDeleteAccountPanel.tsx +15 -16
- package/src/ui/components/signIn/OxyEmailPanel.tsx +258 -0
- package/src/ui/components/signIn/OxyLinkCommonsPanel.tsx +27 -15
- package/src/ui/components/signIn/OxyPasswordPanel.tsx +49 -14
- package/src/ui/components/signIn/OxySignInSecurityPanel.tsx +147 -0
- package/src/ui/components/signIn/ReauthStep.tsx +13 -4
- package/src/ui/components/signIn/SessionsSection.tsx +218 -0
- package/src/ui/components/signIn/accountFlowParts.tsx +6 -0
- package/src/ui/components/signIn/accountSecurityState.tsx +75 -0
- package/src/ui/navigation/routes.ts +5 -1
- package/src/ui/navigation/screenComponents.ts +4 -0
- package/src/ui/navigation/surfaceRegistry.ts +2 -0
- package/src/ui/screens/AccountSecurityPanelScreen.tsx +1 -1
- package/src/ui/screens/ManageAccountScreen.tsx +8 -241
- package/src/ui/screens/SignInEmailScreen.tsx +13 -0
- package/src/ui/screens/SignInSecurityScreen.tsx +19 -0
|
@@ -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) →
|
|
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 {
|
|
61
|
+
const { oxyServices } = useOxy();
|
|
61
62
|
const [step, setStep] = useState<Step>({ name: 'opening' });
|
|
62
63
|
const [error, setError] = useState<string | null>(null);
|
|
63
|
-
const
|
|
64
|
-
const
|
|
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={
|
|
156
|
+
description={
|
|
157
|
+
hasEmail
|
|
158
|
+
? t('linkCommons.confirmDescription')
|
|
159
|
+
: t('linkCommons.confirmPasswordDescription')
|
|
160
|
+
}
|
|
149
161
|
action="link_commons"
|
|
150
|
-
|
|
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
|
-
|
|
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>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Setting or
|
|
3
|
-
* is optional: the account signs in with a code by email without
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Setting, changing or removing the account's password, from its settings. A
|
|
3
|
+
* password is optional: the account signs in with a code by email without
|
|
4
|
+
* one, and it can be removed while the account has an email. Each change is
|
|
5
|
+
* confirmed with the current password (when there is one) or a code sent to
|
|
6
|
+
* the account's email, plus the authenticator's code when it has one.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type React from 'react';
|
|
@@ -14,12 +15,13 @@ import { toast } from '@oxy.so/bloom/toast';
|
|
|
14
15
|
import { PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH } from '@oxy.so/contracts/client';
|
|
15
16
|
import { useQueryClient } from '@tanstack/react-query';
|
|
16
17
|
import { useOxy } from '../../context/oxyRuntimeContext';
|
|
17
|
-
import { useSignInMethods } from '../../hooks/queries/useAuthMethods';
|
|
18
18
|
import { queryKeys } from '../../hooks/queries/queryKeys';
|
|
19
19
|
import { useI18n } from '../../hooks/useI18n';
|
|
20
|
-
import {
|
|
20
|
+
import { SubtleLink } from '../authChooser/primitives';
|
|
21
|
+
import { OxyAuthScreen, OxyAuthScreenHeader } from './OxyAuthScreen';
|
|
21
22
|
import { ReauthStep } from './ReauthStep';
|
|
22
23
|
import { AccountFlowField } from './accountFlowParts';
|
|
24
|
+
import { accountSecurityFallback, useAccountSecurityState } from './accountSecurityState';
|
|
23
25
|
|
|
24
26
|
export interface OxyPasswordPanelProps {
|
|
25
27
|
/** The password is saved. */
|
|
@@ -30,15 +32,15 @@ export interface OxyPasswordPanelProps {
|
|
|
30
32
|
|
|
31
33
|
export const OxyPasswordPanel: React.FC<OxyPasswordPanelProps> = ({ onDone, onCancel }) => {
|
|
32
34
|
const { t } = useI18n();
|
|
33
|
-
const { oxyServices
|
|
35
|
+
const { oxyServices } = useOxy();
|
|
34
36
|
const queryClient = useQueryClient();
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
+
const state = useAccountSecurityState();
|
|
38
|
+
const [removing, setRemoving] = useState(false);
|
|
37
39
|
const [password, setPassword] = useState('');
|
|
38
40
|
const [repeat, setRepeat] = useState('');
|
|
39
41
|
const [signOutOthers, setSignOutOthers] = useState(false);
|
|
40
42
|
|
|
41
|
-
if (keyed) {
|
|
43
|
+
if (state.keyed) {
|
|
42
44
|
return (
|
|
43
45
|
<OxyAuthScreen>
|
|
44
46
|
<OxyAuthScreenHeader
|
|
@@ -48,9 +50,33 @@ export const OxyPasswordPanel: React.FC<OxyPasswordPanelProps> = ({ onDone, onCa
|
|
|
48
50
|
</OxyAuthScreen>
|
|
49
51
|
);
|
|
50
52
|
}
|
|
51
|
-
|
|
53
|
+
const fallback = accountSecurityFallback(state, t('signInSecurity.password.title'), t);
|
|
54
|
+
if (fallback || !state.methods.data) return fallback;
|
|
55
|
+
const { hasEmail, hasPassword, totpEnabled } = state.methods.data;
|
|
56
|
+
const refresh = () =>
|
|
57
|
+
void queryClient.invalidateQueries({ queryKey: queryKeys.signInMethods.all });
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
if (removing) {
|
|
60
|
+
return (
|
|
61
|
+
<ReauthStep
|
|
62
|
+
title={t('signInSecurity.password.removeTitle')}
|
|
63
|
+
description={t('signInSecurity.password.removeDescription')}
|
|
64
|
+
action="change_password"
|
|
65
|
+
allowPassword
|
|
66
|
+
totpEnabled={totpEnabled}
|
|
67
|
+
submitLabel={t('signInSecurity.password.remove')}
|
|
68
|
+
destructive
|
|
69
|
+
onSubmit={async (reauth) => {
|
|
70
|
+
await oxyServices.auth.password.remove(reauth);
|
|
71
|
+
refresh();
|
|
72
|
+
toast.success(t('signInSecurity.password.removed'));
|
|
73
|
+
setRemoving(false);
|
|
74
|
+
onDone?.();
|
|
75
|
+
}}
|
|
76
|
+
secondary={{ label: t('common.cancel'), onPress: () => setRemoving(false) }}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
54
80
|
|
|
55
81
|
return (
|
|
56
82
|
<ReauthStep
|
|
@@ -62,7 +88,8 @@ export const OxyPasswordPanel: React.FC<OxyPasswordPanelProps> = ({ onDone, onCa
|
|
|
62
88
|
description={t('signInSecurity.password.description', { min: PASSWORD_MIN_LENGTH })}
|
|
63
89
|
action="change_password"
|
|
64
90
|
allowPassword={hasPassword}
|
|
65
|
-
|
|
91
|
+
allowEmail={hasEmail}
|
|
92
|
+
totpEnabled={totpEnabled}
|
|
66
93
|
submitLabel={t('signInSecurity.password.save')}
|
|
67
94
|
validate={() => {
|
|
68
95
|
if (password.length < PASSWORD_MIN_LENGTH || password.length > PASSWORD_MAX_LENGTH) {
|
|
@@ -76,7 +103,7 @@ export const OxyPasswordPanel: React.FC<OxyPasswordPanelProps> = ({ onDone, onCa
|
|
|
76
103
|
reauth,
|
|
77
104
|
revokeOtherSessions: signOutOthers,
|
|
78
105
|
});
|
|
79
|
-
|
|
106
|
+
refresh();
|
|
80
107
|
toast.success(t('signInSecurity.password.saved'));
|
|
81
108
|
onDone?.();
|
|
82
109
|
}}
|
|
@@ -112,6 +139,14 @@ export const OxyPasswordPanel: React.FC<OxyPasswordPanelProps> = ({ onDone, onCa
|
|
|
112
139
|
testID="password-sign-out-others"
|
|
113
140
|
/>
|
|
114
141
|
</Field>
|
|
142
|
+
{/* Without an email the password is the one way in: it stays. */}
|
|
143
|
+
{hasPassword && hasEmail ? (
|
|
144
|
+
<SubtleLink
|
|
145
|
+
label={t('signInSecurity.password.remove')}
|
|
146
|
+
onPress={() => setRemoving(true)}
|
|
147
|
+
testID="password-remove"
|
|
148
|
+
/>
|
|
149
|
+
) : null}
|
|
115
150
|
</ReauthStep>
|
|
116
151
|
);
|
|
117
152
|
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Sign-in & security": every way the signed-in account signs in, each with
|
|
3
|
+
* its state, and every place it is signed in. One page, rendered by the
|
|
4
|
+
* account dialog's "Manage your account" (`SignInSecurity`) and by the
|
|
5
|
+
* Accounts app's security page.
|
|
6
|
+
*
|
|
7
|
+
* Each method opens its own panel: the email (`SignInEmail`), the password
|
|
8
|
+
* (`SignInPassword`), the authenticator app (`SignInAuthenticator`) and
|
|
9
|
+
* linking Commons (`LinkCommons`). An account with a Commons key signs in with
|
|
10
|
+
* Commons, so it sees only that; an organization, project or bot has no
|
|
11
|
+
* methods of its own, only its sessions.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type React from 'react';
|
|
15
|
+
import { View } from 'react-native';
|
|
16
|
+
import { GroupedSection, GroupedSectionItem } from '@oxy.so/bloom/grouped-section';
|
|
17
|
+
import { useTheme } from '@oxy.so/bloom/theme';
|
|
18
|
+
import { useOxy } from '../../context/oxyRuntimeContext';
|
|
19
|
+
import { useI18n } from '../../hooks/useI18n';
|
|
20
|
+
import type { RouteName } from '../../navigation/routes';
|
|
21
|
+
import { SettingsIcon } from '../SettingsIcon';
|
|
22
|
+
import { AccountFlowNotice } from './accountFlowParts';
|
|
23
|
+
import { useAccountSecurityState } from './accountSecurityState';
|
|
24
|
+
import { SessionsSection } from './SessionsSection';
|
|
25
|
+
|
|
26
|
+
type SignInSecurityRoute = Extract<
|
|
27
|
+
RouteName,
|
|
28
|
+
'SignInEmail' | 'SignInPassword' | 'SignInAuthenticator' | 'LinkCommons'
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
export interface OxySignInSecurityPanelProps {
|
|
32
|
+
/**
|
|
33
|
+
* Open one method's panel. Defaults to the provider's `showBottomSheet`;
|
|
34
|
+
* a surface passes its own `navigate` so the panel stacks inside it.
|
|
35
|
+
*/
|
|
36
|
+
onOpen?: (route: SignInSecurityRoute) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const OxySignInSecurityPanel: React.FC<OxySignInSecurityPanelProps> = ({ onOpen }) => {
|
|
40
|
+
const theme = useTheme();
|
|
41
|
+
const { t } = useI18n();
|
|
42
|
+
const { user, showBottomSheet } = useOxy();
|
|
43
|
+
const { keyed, personal, methods } = useAccountSecurityState();
|
|
44
|
+
const open = (route: SignInSecurityRoute) => () =>
|
|
45
|
+
onOpen ? onOpen(route) : showBottomSheet?.(route);
|
|
46
|
+
const on = theme.colors.success;
|
|
47
|
+
const off = theme.colors.textTertiary;
|
|
48
|
+
|
|
49
|
+
const renderMethods = (): React.ReactNode => {
|
|
50
|
+
if (keyed) {
|
|
51
|
+
return (
|
|
52
|
+
<GroupedSectionItem
|
|
53
|
+
icon={<SettingsIcon name="key" color={on} />}
|
|
54
|
+
title={t('signInSecurity.commons.title')}
|
|
55
|
+
description={t('linkCommons.already')}
|
|
56
|
+
showChevron={false}
|
|
57
|
+
disabled
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (!personal) {
|
|
62
|
+
return (
|
|
63
|
+
<GroupedSectionItem
|
|
64
|
+
icon={<SettingsIcon name="information" color={off} />}
|
|
65
|
+
title={t('signInSecurity.notPersonal')}
|
|
66
|
+
showChevron={false}
|
|
67
|
+
disabled
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (!methods.data) {
|
|
72
|
+
return methods.isError ? (
|
|
73
|
+
<GroupedSectionItem
|
|
74
|
+
icon={<SettingsIcon name="alert-circle" color={theme.colors.error} />}
|
|
75
|
+
title={t('signInSecurity.loadFailed')}
|
|
76
|
+
description={t('signInSecurity.retry')}
|
|
77
|
+
onPress={() => void methods.refetch()}
|
|
78
|
+
testID="sign-in-security-retry"
|
|
79
|
+
/>
|
|
80
|
+
) : (
|
|
81
|
+
<GroupedSectionItem
|
|
82
|
+
icon={<SettingsIcon name="sync" color={theme.colors.primary} />}
|
|
83
|
+
title={t('common.status.loading')}
|
|
84
|
+
showChevron={false}
|
|
85
|
+
disabled
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const { hasEmail, hasPassword, totpEnabled, backupCodesRemaining } = methods.data;
|
|
90
|
+
return (
|
|
91
|
+
<>
|
|
92
|
+
<GroupedSectionItem
|
|
93
|
+
icon={<SettingsIcon name="email" color={hasEmail ? on : off} />}
|
|
94
|
+
title={t('signInSecurity.email.title')}
|
|
95
|
+
description={
|
|
96
|
+
hasEmail
|
|
97
|
+
? (user?.email ?? t('signInSecurity.status.on'))
|
|
98
|
+
: t('signInSecurity.email.none')
|
|
99
|
+
}
|
|
100
|
+
onPress={open('SignInEmail')}
|
|
101
|
+
testID="sign-in-security-email"
|
|
102
|
+
/>
|
|
103
|
+
<GroupedSectionItem
|
|
104
|
+
icon={<SettingsIcon name="key" color={hasPassword ? on : off} />}
|
|
105
|
+
title={t('signInSecurity.password.title')}
|
|
106
|
+
description={
|
|
107
|
+
hasPassword ? t('signInSecurity.status.on') : t('signInSecurity.password.row')
|
|
108
|
+
}
|
|
109
|
+
onPress={open('SignInPassword')}
|
|
110
|
+
testID="sign-in-security-password"
|
|
111
|
+
/>
|
|
112
|
+
<GroupedSectionItem
|
|
113
|
+
icon={<SettingsIcon name="shield-check" color={totpEnabled ? on : off} />}
|
|
114
|
+
title={t('signInSecurity.totp.title')}
|
|
115
|
+
description={
|
|
116
|
+
totpEnabled
|
|
117
|
+
? t('signInSecurity.totp.onWithCodes', { count: backupCodesRemaining })
|
|
118
|
+
: t('signInSecurity.totp.row')
|
|
119
|
+
}
|
|
120
|
+
onPress={open('SignInAuthenticator')}
|
|
121
|
+
testID="sign-in-security-totp"
|
|
122
|
+
/>
|
|
123
|
+
<GroupedSectionItem
|
|
124
|
+
icon={<SettingsIcon name="link" color={theme.colors.primary} />}
|
|
125
|
+
title={t('linkCommons.title')}
|
|
126
|
+
description={t('linkCommons.row')}
|
|
127
|
+
onPress={open('LinkCommons')}
|
|
128
|
+
testID="sign-in-security-commons"
|
|
129
|
+
/>
|
|
130
|
+
</>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const noEmail = personal && !keyed && methods.data?.hasEmail === false;
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<View>
|
|
138
|
+
<GroupedSection title={t('signInSecurity.methodsTitle')}>{renderMethods()}</GroupedSection>
|
|
139
|
+
{noEmail ? (
|
|
140
|
+
<AccountFlowNotice type="warning" testID="sign-in-security-no-email">
|
|
141
|
+
{t('signInSecurity.noEmailWarning')}
|
|
142
|
+
</AccountFlowNotice>
|
|
143
|
+
) : null}
|
|
144
|
+
<SessionsSection />
|
|
145
|
+
</View>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* "Confirm it's you": the fresh proof a sensitive change carries (`reauth`) —
|
|
3
3
|
* a code sent to the account's email for THIS change, or the current password
|
|
4
|
-
* where the change accepts one
|
|
5
|
-
*
|
|
4
|
+
* where the change accepts one (the only proof of an account without an
|
|
5
|
+
* email) — plus the authenticator's code (or a backup code) when the account
|
|
6
|
+
* has one. The screen that needs it puts its own fields
|
|
6
7
|
* above (`children`) and names its own action; this owns the proof.
|
|
7
8
|
*/
|
|
8
9
|
|
|
@@ -34,6 +35,11 @@ export interface ReauthStepProps {
|
|
|
34
35
|
action: ReauthAction;
|
|
35
36
|
/** The account has a password, and this change accepts it. */
|
|
36
37
|
allowPassword?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The account has an email, and this change accepts a code sent to it.
|
|
40
|
+
* Without it the proof is the password alone.
|
|
41
|
+
*/
|
|
42
|
+
allowEmail?: boolean;
|
|
37
43
|
/** The account has an authenticator: its code is required too. */
|
|
38
44
|
totpEnabled: boolean;
|
|
39
45
|
/** The screen's action, once the proof is ready. */
|
|
@@ -53,6 +59,7 @@ export const ReauthStep: React.FC<ReauthStepProps> = ({
|
|
|
53
59
|
description,
|
|
54
60
|
action,
|
|
55
61
|
allowPassword = false,
|
|
62
|
+
allowEmail = true,
|
|
56
63
|
totpEnabled,
|
|
57
64
|
submitLabel,
|
|
58
65
|
destructive,
|
|
@@ -63,7 +70,9 @@ export const ReauthStep: React.FC<ReauthStepProps> = ({
|
|
|
63
70
|
}) => {
|
|
64
71
|
const { t } = useI18n();
|
|
65
72
|
const { oxyServices } = useOxy();
|
|
66
|
-
const [mode, setMode] = useState<'email' | 'password'>(
|
|
73
|
+
const [mode, setMode] = useState<'email' | 'password'>(
|
|
74
|
+
allowPassword || !allowEmail ? 'password' : 'email',
|
|
75
|
+
);
|
|
67
76
|
const [verificationId, setVerificationId] = useState<string | null>(null);
|
|
68
77
|
const [emailCode, setEmailCode] = useState('');
|
|
69
78
|
const [password, setPassword] = useState('');
|
|
@@ -221,7 +230,7 @@ export const ReauthStep: React.FC<ReauthStepProps> = ({
|
|
|
221
230
|
testID="reauth-resend"
|
|
222
231
|
/>
|
|
223
232
|
) : null}
|
|
224
|
-
{allowPassword ? (
|
|
233
|
+
{allowPassword && allowEmail ? (
|
|
225
234
|
<SubtleLink
|
|
226
235
|
label={mode === 'password' ? t('reauth.useEmail') : t('reauth.usePassword')}
|
|
227
236
|
onPress={() => {
|