@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.
- package/README.md +1 -1
- package/lib/commonjs/index.js +32 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/OxyAuthChooser.js +8 -8
- package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -1
- package/lib/commonjs/ui/components/SurfaceScreen.js +21 -12
- package/lib/commonjs/ui/components/SurfaceScreen.js.map +1 -1
- package/lib/commonjs/ui/components/authChooser/AccountsMenuView.js +1 -8
- package/lib/commonjs/ui/components/authChooser/AccountsMenuView.js.map +1 -1
- package/lib/commonjs/ui/components/authChooser/types.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/OxyAuthScreen.js +11 -5
- package/lib/commonjs/ui/components/signIn/OxyAuthScreen.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/OxyCreateAccountPanel.js +236 -0
- package/lib/commonjs/ui/components/signIn/OxyCreateAccountPanel.js.map +1 -0
- package/lib/commonjs/ui/components/signIn/OxyDeleteAccountPanel.js +114 -0
- package/lib/commonjs/ui/components/signIn/OxyDeleteAccountPanel.js.map +1 -0
- package/lib/commonjs/ui/components/signIn/OxyLinkCommonsPanel.js +275 -0
- package/lib/commonjs/ui/components/signIn/OxyLinkCommonsPanel.js.map +1 -0
- package/lib/commonjs/ui/components/signIn/OxyRecoverAccountPanel.js +170 -0
- package/lib/commonjs/ui/components/signIn/OxyRecoverAccountPanel.js.map +1 -0
- package/lib/commonjs/ui/components/signIn/OxySignInPanel.js +45 -32
- package/lib/commonjs/ui/components/signIn/OxySignInPanel.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/accountFlowParts.js +244 -0
- package/lib/commonjs/ui/components/signIn/accountFlowParts.js.map +1 -0
- package/lib/commonjs/ui/components/signIn/signInMethods.js +19 -18
- package/lib/commonjs/ui/components/signIn/signInMethods.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +4 -4
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/passkeyFlow.js +22 -0
- package/lib/commonjs/ui/context/passkeyFlow.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSurfaceFrameWidth.js +28 -0
- package/lib/commonjs/ui/hooks/useSurfaceFrameWidth.js.map +1 -0
- package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +0 -23
- package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/EditProfileScreen.js +0 -8
- package/lib/commonjs/ui/screens/EditProfileScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/ManageAccountScreen.js +3 -3
- package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/commonjs/ui/utils/accountDeletionHandoff.js +2 -2
- package/lib/commonjs/ui/utils/accountDeletionHandoff.js.map +1 -1
- package/lib/module/index.js +7 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/OxyAuthChooser.js +9 -9
- package/lib/module/ui/components/OxyAuthChooser.js.map +1 -1
- package/lib/module/ui/components/SurfaceScreen.js +21 -12
- package/lib/module/ui/components/SurfaceScreen.js.map +1 -1
- package/lib/module/ui/components/authChooser/AccountsMenuView.js +1 -8
- package/lib/module/ui/components/authChooser/AccountsMenuView.js.map +1 -1
- package/lib/module/ui/components/authChooser/types.js.map +1 -1
- package/lib/module/ui/components/signIn/OxyAuthScreen.js +11 -5
- package/lib/module/ui/components/signIn/OxyAuthScreen.js.map +1 -1
- package/lib/module/ui/components/signIn/OxyCreateAccountPanel.js +231 -0
- package/lib/module/ui/components/signIn/OxyCreateAccountPanel.js.map +1 -0
- package/lib/module/ui/components/signIn/OxyDeleteAccountPanel.js +109 -0
- package/lib/module/ui/components/signIn/OxyDeleteAccountPanel.js.map +1 -0
- package/lib/module/ui/components/signIn/OxyLinkCommonsPanel.js +270 -0
- package/lib/module/ui/components/signIn/OxyLinkCommonsPanel.js.map +1 -0
- package/lib/module/ui/components/signIn/OxyRecoverAccountPanel.js +165 -0
- package/lib/module/ui/components/signIn/OxyRecoverAccountPanel.js.map +1 -0
- package/lib/module/ui/components/signIn/OxySignInPanel.js +47 -32
- package/lib/module/ui/components/signIn/OxySignInPanel.js.map +1 -1
- package/lib/module/ui/components/signIn/accountFlowParts.js +235 -0
- package/lib/module/ui/components/signIn/accountFlowParts.js.map +1 -0
- package/lib/module/ui/components/signIn/signInMethods.js +19 -18
- package/lib/module/ui/components/signIn/signInMethods.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +4 -4
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/passkeyFlow.js +21 -0
- package/lib/module/ui/context/passkeyFlow.js.map +1 -1
- package/lib/module/ui/hooks/useSurfaceFrameWidth.js +24 -0
- package/lib/module/ui/hooks/useSurfaceFrameWidth.js.map +1 -0
- package/lib/module/ui/screens/EditProfileFieldScreen.js +1 -24
- package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
- package/lib/module/ui/screens/EditProfileScreen.js +0 -8
- package/lib/module/ui/screens/EditProfileScreen.js.map +1 -1
- package/lib/module/ui/screens/ManageAccountScreen.js +3 -3
- package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/module/ui/utils/accountDeletionHandoff.js +2 -2
- package/lib/module/ui/utils/accountDeletionHandoff.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +8 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts +3 -2
- package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/authChooser/AccountsMenuView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts +0 -5
- package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxyAuthScreen.d.ts +7 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxyCreateAccountPanel.d.ts +24 -0
- package/lib/typescript/commonjs/ui/components/signIn/OxyCreateAccountPanel.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/signIn/OxyDeleteAccountPanel.d.ts +16 -0
- package/lib/typescript/commonjs/ui/components/signIn/OxyDeleteAccountPanel.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/signIn/OxyLinkCommonsPanel.d.ts +21 -0
- package/lib/typescript/commonjs/ui/components/signIn/OxyLinkCommonsPanel.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/signIn/OxyRecoverAccountPanel.d.ts +20 -0
- package/lib/typescript/commonjs/ui/components/signIn/OxyRecoverAccountPanel.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/signIn/OxySignInPanel.d.ts +12 -12
- package/lib/typescript/commonjs/ui/components/signIn/OxySignInPanel.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/accountFlowParts.d.ts +60 -0
- package/lib/typescript/commonjs/ui/components/signIn/accountFlowParts.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/signIn/signInMethods.d.ts +18 -15
- package/lib/typescript/commonjs/ui/components/signIn/signInMethods.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +11 -5
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/passkeyFlow.d.ts +16 -1
- package/lib/typescript/commonjs/ui/context/passkeyFlow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSurfaceFrameWidth.d.ts +12 -0
- package/lib/typescript/commonjs/ui/hooks/useSurfaceFrameWidth.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/EditProfileScreen.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +8 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts +3 -2
- package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/SurfaceScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/authChooser/AccountsMenuView.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/authChooser/types.d.ts +0 -5
- package/lib/typescript/module/ui/components/authChooser/types.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/OxyAuthScreen.d.ts +7 -1
- package/lib/typescript/module/ui/components/signIn/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/OxyCreateAccountPanel.d.ts +24 -0
- package/lib/typescript/module/ui/components/signIn/OxyCreateAccountPanel.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/signIn/OxyDeleteAccountPanel.d.ts +16 -0
- package/lib/typescript/module/ui/components/signIn/OxyDeleteAccountPanel.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/signIn/OxyLinkCommonsPanel.d.ts +21 -0
- package/lib/typescript/module/ui/components/signIn/OxyLinkCommonsPanel.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/signIn/OxyRecoverAccountPanel.d.ts +20 -0
- package/lib/typescript/module/ui/components/signIn/OxyRecoverAccountPanel.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/signIn/OxySignInPanel.d.ts +12 -12
- package/lib/typescript/module/ui/components/signIn/OxySignInPanel.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/accountFlowParts.d.ts +60 -0
- package/lib/typescript/module/ui/components/signIn/accountFlowParts.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/signIn/signInMethods.d.ts +18 -15
- package/lib/typescript/module/ui/components/signIn/signInMethods.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +11 -5
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/passkeyFlow.d.ts +16 -1
- package/lib/typescript/module/ui/context/passkeyFlow.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSurfaceFrameWidth.d.ts +12 -0
- package/lib/typescript/module/ui/hooks/useSurfaceFrameWidth.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts +1 -1
- package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/EditProfileScreen.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +11 -0
- package/src/ui/components/OxyAuthChooser.tsx +9 -9
- package/src/ui/components/SurfaceScreen.tsx +19 -11
- package/src/ui/components/authChooser/AccountsMenuView.tsx +0 -7
- package/src/ui/components/authChooser/types.ts +0 -5
- package/src/ui/components/signIn/OxyAuthScreen.tsx +14 -5
- package/src/ui/components/signIn/OxyCreateAccountPanel.tsx +207 -0
- package/src/ui/components/signIn/OxyDeleteAccountPanel.tsx +106 -0
- package/src/ui/components/signIn/OxyLinkCommonsPanel.tsx +222 -0
- package/src/ui/components/signIn/OxyRecoverAccountPanel.tsx +154 -0
- package/src/ui/components/signIn/OxySignInPanel.tsx +56 -37
- package/src/ui/components/signIn/accountFlowParts.tsx +204 -0
- package/src/ui/components/signIn/signInMethods.ts +21 -17
- package/src/ui/context/OxyContext.tsx +6 -5
- package/src/ui/context/__tests__/passkeyFlow.test.ts +59 -1
- package/src/ui/context/oxyContextTypes.ts +11 -5
- package/src/ui/context/passkeyFlow.ts +28 -0
- package/src/ui/hooks/useSurfaceFrameWidth.ts +21 -0
- package/src/ui/screens/EditProfileFieldScreen.tsx +1 -27
- package/src/ui/screens/EditProfileScreen.tsx +0 -6
- package/src/ui/screens/ManageAccountScreen.tsx +3 -3
- 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
|
|
4
|
+
* One screen everywhere, and sign-in happens IN it (ADR 0029 D1):
|
|
5
5
|
*
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
9
|
-
* on
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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` —
|
|
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' | '
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
129
|
-
* (`recover`) in auth.oxy.so's window over the app
|
|
130
|
-
*
|
|
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 {
|
|
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:
|
|
327
|
-
// auth.oxy.so
|
|
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}/
|
|
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?:\/\//, '')}/
|
|
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
|
|
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'),
|