@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,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deleting a passkey account (ADR 0029 D3), auth.oxy.so's `/delete-account`.
|
|
3
|
+
*
|
|
4
|
+
* A passkey account has no key to sign its deletion with, so the person types
|
|
5
|
+
* the username and asserts one of the account's passkeys — on auth.oxy.so, the
|
|
6
|
+
* one origin that asserts them — over a challenge the API minted for the
|
|
7
|
+
* account. An account with a Commons key is deleted in Commons, with its key.
|
|
8
|
+
* The page must be signed in as the account; the host renders sign-in first.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type React from 'react';
|
|
12
|
+
import { useState } from 'react';
|
|
13
|
+
import { useOxy } from '../../context/OxyContext';
|
|
14
|
+
import { useI18n } from '../../hooks/useI18n';
|
|
15
|
+
import { isPasskeySupported, runAuthenticationCeremony } from '../../../webauthn/passkeyClient';
|
|
16
|
+
import { PASSKEY_UNSUPPORTED_MESSAGE } from '../../context/passkeyFlow';
|
|
17
|
+
import { OxyAuthScreen, OxyAuthScreenHeader } from './OxyAuthScreen';
|
|
18
|
+
import {
|
|
19
|
+
AccountFlowAction,
|
|
20
|
+
AccountFlowErrorLine,
|
|
21
|
+
AccountFlowField,
|
|
22
|
+
AccountFlowNote,
|
|
23
|
+
describeAccountFlowError,
|
|
24
|
+
} from './accountFlowParts';
|
|
25
|
+
|
|
26
|
+
export interface OxyDeleteAccountPanelProps {
|
|
27
|
+
/** The account is gone and this origin signed out of it. */
|
|
28
|
+
onDeleted?: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const OxyDeleteAccountPanel: React.FC<OxyDeleteAccountPanelProps> = ({ onDeleted }) => {
|
|
32
|
+
const { t } = useI18n();
|
|
33
|
+
const { user, oxyServices, logout } = useOxy();
|
|
34
|
+
const [confirmText, setConfirmText] = useState('');
|
|
35
|
+
const [pending, setPending] = useState(false);
|
|
36
|
+
const [error, setError] = useState<string | null>(null);
|
|
37
|
+
const [deleted, setDeleted] = useState(false);
|
|
38
|
+
|
|
39
|
+
if (deleted) {
|
|
40
|
+
return (
|
|
41
|
+
<OxyAuthScreen>
|
|
42
|
+
<OxyAuthScreenHeader title={t('deleteAccount.passkey.done')} description={t('deleteAccount.passkey.closeWindow')} />
|
|
43
|
+
</OxyAuthScreen>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const username = user?.username ?? '';
|
|
48
|
+
if (user?.publicKey) {
|
|
49
|
+
return (
|
|
50
|
+
<OxyAuthScreen>
|
|
51
|
+
<OxyAuthScreenHeader title={t('deleteAccount.handoff.commonsTitle')} description={t('deleteAccount.passkey.commons')} />
|
|
52
|
+
</OxyAuthScreen>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The ceremony opens the browser's passkey prompt, so it starts from the press.
|
|
57
|
+
const remove = () => {
|
|
58
|
+
if (pending) return;
|
|
59
|
+
if (confirmText.trim() !== username) {
|
|
60
|
+
setError(t('deleteAccount.confirmLabel', { username }));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
setError(null);
|
|
64
|
+
setPending(true);
|
|
65
|
+
(async () => {
|
|
66
|
+
if (!isPasskeySupported()) throw new Error(PASSKEY_UNSUPPORTED_MESSAGE);
|
|
67
|
+
const options = await oxyServices.getAccountDeletionOptions();
|
|
68
|
+
const assertion = await runAuthenticationCeremony(options);
|
|
69
|
+
await oxyServices.deleteAccountWithPasskey(confirmText.trim(), assertion);
|
|
70
|
+
await logout().catch(() => undefined);
|
|
71
|
+
setDeleted(true);
|
|
72
|
+
onDeleted?.();
|
|
73
|
+
})()
|
|
74
|
+
.catch((reason: unknown) => setError(describeAccountFlowError(reason, t)))
|
|
75
|
+
.finally(() => setPending(false));
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<OxyAuthScreen>
|
|
80
|
+
<OxyAuthScreenHeader title={t('deleteAccount.title')} description={t('deleteAccount.passkey.subtitle', { username })} />
|
|
81
|
+
<AccountFlowNote>{t('deleteAccount.warning')}</AccountFlowNote>
|
|
82
|
+
<AccountFlowField
|
|
83
|
+
label={t('deleteAccount.confirmLabel', { username })}
|
|
84
|
+
value={confirmText}
|
|
85
|
+
onChange={(value) => {
|
|
86
|
+
setConfirmText(value);
|
|
87
|
+
if (error) setError(null);
|
|
88
|
+
}}
|
|
89
|
+
onSubmit={remove}
|
|
90
|
+
error={null}
|
|
91
|
+
disabled={pending}
|
|
92
|
+
placeholder={username}
|
|
93
|
+
autoComplete="off"
|
|
94
|
+
testID="delete-account-confirm"
|
|
95
|
+
/>
|
|
96
|
+
{error ? <AccountFlowErrorLine message={error} /> : null}
|
|
97
|
+
<AccountFlowAction
|
|
98
|
+
label={t('deleteAccount.passkey.action')}
|
|
99
|
+
onPress={remove}
|
|
100
|
+
pending={pending}
|
|
101
|
+
disabled={confirmText.trim() !== username}
|
|
102
|
+
testID="delete-account-passkey"
|
|
103
|
+
/>
|
|
104
|
+
</OxyAuthScreen>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linking Commons to a passkey account (ADR 0029 D3), auth.oxy.so's
|
|
3
|
+
* `/link-commons`:
|
|
4
|
+
*
|
|
5
|
+
* QR → Commons scans and signs with its key → both screens show the same
|
|
6
|
+
* code → the person confirms here with the passkey → linked
|
|
7
|
+
*
|
|
8
|
+
* Afterwards the account is self-custodied: its root is the key Commons holds,
|
|
9
|
+
* its recovery email is deleted, and its phrase in Commons is how it gets back
|
|
10
|
+
* in. The passkey keeps signing in on the web. The page must be signed in as
|
|
11
|
+
* the account; the host renders sign-in first.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type React from 'react';
|
|
15
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
16
|
+
import { ActivityIndicator, StyleSheet, View } from 'react-native';
|
|
17
|
+
import QRCode from 'react-native-qrcode-svg';
|
|
18
|
+
import { Button } from '@oxy.so/bloom/button';
|
|
19
|
+
import { useTheme } from '@oxy.so/bloom/theme';
|
|
20
|
+
import { Text } from '@oxy.so/bloom/typography';
|
|
21
|
+
import { deriveIdentityLinkCode } from '@oxy.so/core';
|
|
22
|
+
import type { IdentityLinkCreateResponse } from '@oxy.so/contracts';
|
|
23
|
+
import { useOxy } from '../../context/OxyContext';
|
|
24
|
+
import { PASSKEY_UNSUPPORTED_MESSAGE } from '../../context/passkeyFlow';
|
|
25
|
+
import { useI18n } from '../../hooks/useI18n';
|
|
26
|
+
import { isPasskeySupported, runAuthenticationCeremony } from '../../../webauthn/passkeyClient';
|
|
27
|
+
import { OxyAuthScreen, OxyAuthScreenHeader } from './OxyAuthScreen';
|
|
28
|
+
import { AccountFlowAction, AccountFlowErrorLine, AccountFlowNote, describeAccountFlowError } from './accountFlowParts';
|
|
29
|
+
|
|
30
|
+
/** How often the page asks whether Commons has signed. */
|
|
31
|
+
export const IDENTITY_LINK_POLL_MS = 2000;
|
|
32
|
+
|
|
33
|
+
/** High-contrast, un-themed on purpose: scan reliability. */
|
|
34
|
+
const QR_PLATE_BG = '#FFFFFF';
|
|
35
|
+
const QR_FOREGROUND = '#000000';
|
|
36
|
+
const QR_SIZE = 200;
|
|
37
|
+
|
|
38
|
+
type Step =
|
|
39
|
+
| { name: 'opening' }
|
|
40
|
+
| { name: 'qr'; link: IdentityLinkCreateResponse }
|
|
41
|
+
| { name: 'matching'; link: IdentityLinkCreateResponse; code: string }
|
|
42
|
+
| { name: 'expired' }
|
|
43
|
+
| { name: 'done' };
|
|
44
|
+
|
|
45
|
+
export interface OxyLinkCommonsPanelProps {
|
|
46
|
+
/** Commons is linked. */
|
|
47
|
+
onLinked?: () => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const OxyLinkCommonsPanel: React.FC<OxyLinkCommonsPanelProps> = ({ onLinked }) => {
|
|
51
|
+
const theme = useTheme();
|
|
52
|
+
const { t } = useI18n();
|
|
53
|
+
const { user, oxyServices } = useOxy();
|
|
54
|
+
const [step, setStep] = useState<Step>({ name: 'opening' });
|
|
55
|
+
const [pending, setPending] = useState(false);
|
|
56
|
+
const [error, setError] = useState<string | null>(null);
|
|
57
|
+
const alreadyLinked = Boolean(user?.publicKey);
|
|
58
|
+
|
|
59
|
+
const open = useCallback(() => {
|
|
60
|
+
setError(null);
|
|
61
|
+
setStep({ name: 'opening' });
|
|
62
|
+
oxyServices
|
|
63
|
+
.createIdentityLink()
|
|
64
|
+
.then((link) => setStep({ name: 'qr', link }))
|
|
65
|
+
.catch((reason: unknown) => {
|
|
66
|
+
setError(describeAccountFlowError(reason, t));
|
|
67
|
+
setStep({ name: 'expired' });
|
|
68
|
+
});
|
|
69
|
+
}, [oxyServices, t]);
|
|
70
|
+
|
|
71
|
+
// One request per visit, opened once the page knows the account has no root.
|
|
72
|
+
const opened = useRef(false);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (alreadyLinked || opened.current) return;
|
|
75
|
+
opened.current = true;
|
|
76
|
+
open();
|
|
77
|
+
}, [alreadyLinked, open]);
|
|
78
|
+
|
|
79
|
+
// While the QR shows, ask whether Commons has signed; a request only lives a
|
|
80
|
+
// few minutes, and an expired one says so instead of waiting forever.
|
|
81
|
+
const link = step.name === 'qr' ? step.link : null;
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!link) return;
|
|
84
|
+
let stopped = false;
|
|
85
|
+
const poll = async () => {
|
|
86
|
+
if (stopped) return;
|
|
87
|
+
if (Date.now() >= link.expiresAt) {
|
|
88
|
+
setStep({ name: 'expired' });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const state = await oxyServices.getIdentityLink(link.linkId);
|
|
93
|
+
if (stopped) return;
|
|
94
|
+
if (state.status === 'signed' && state.publicKey) {
|
|
95
|
+
setStep({ name: 'matching', link, code: deriveIdentityLinkCode(link.linkId, state.publicKey) });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (state.status === 'cancelled') {
|
|
99
|
+
setStep({ name: 'expired' });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// A missed poll is retried on the next tick.
|
|
104
|
+
}
|
|
105
|
+
if (!stopped) timer = setTimeout(() => void poll(), IDENTITY_LINK_POLL_MS);
|
|
106
|
+
};
|
|
107
|
+
let timer = setTimeout(() => void poll(), IDENTITY_LINK_POLL_MS);
|
|
108
|
+
return () => {
|
|
109
|
+
stopped = true;
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
};
|
|
112
|
+
}, [link, oxyServices]);
|
|
113
|
+
|
|
114
|
+
// The ceremony opens the browser's passkey prompt, so it starts from the press.
|
|
115
|
+
const confirm = (confirmed: IdentityLinkCreateResponse) => {
|
|
116
|
+
if (pending) return;
|
|
117
|
+
setError(null);
|
|
118
|
+
setPending(true);
|
|
119
|
+
(async () => {
|
|
120
|
+
if (!isPasskeySupported()) throw new Error(PASSKEY_UNSUPPORTED_MESSAGE);
|
|
121
|
+
const options = await oxyServices.getIdentityLinkAssertionOptions(confirmed.linkId, confirmed.challenge);
|
|
122
|
+
const assertion = await runAuthenticationCeremony(options);
|
|
123
|
+
await oxyServices.completeIdentityLink(confirmed.linkId, assertion);
|
|
124
|
+
setStep({ name: 'done' });
|
|
125
|
+
onLinked?.();
|
|
126
|
+
})()
|
|
127
|
+
.catch((reason: unknown) => setError(describeAccountFlowError(reason, t)))
|
|
128
|
+
.finally(() => setPending(false));
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const cancel = (cancelled: IdentityLinkCreateResponse) => {
|
|
132
|
+
void oxyServices.cancelIdentityLink(cancelled.linkId).catch(() => undefined);
|
|
133
|
+
setStep({ name: 'expired' });
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (alreadyLinked) {
|
|
137
|
+
return (
|
|
138
|
+
<OxyAuthScreen>
|
|
139
|
+
<OxyAuthScreenHeader title={t('linkCommons.title')} description={t('linkCommons.already')} />
|
|
140
|
+
</OxyAuthScreen>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
switch (step.name) {
|
|
145
|
+
case 'done':
|
|
146
|
+
return (
|
|
147
|
+
<OxyAuthScreen>
|
|
148
|
+
<OxyAuthScreenHeader title={t('linkCommons.doneTitle')} description={t('linkCommons.done')} />
|
|
149
|
+
</OxyAuthScreen>
|
|
150
|
+
);
|
|
151
|
+
case 'expired':
|
|
152
|
+
return (
|
|
153
|
+
<OxyAuthScreen>
|
|
154
|
+
<OxyAuthScreenHeader title={t('linkCommons.title')} description={t('linkCommons.expired')} />
|
|
155
|
+
{error ? <AccountFlowErrorLine message={error} /> : null}
|
|
156
|
+
<AccountFlowAction label={t('linkCommons.renew')} onPress={open} pending={false} testID="link-commons-renew" />
|
|
157
|
+
</OxyAuthScreen>
|
|
158
|
+
);
|
|
159
|
+
case 'matching':
|
|
160
|
+
return (
|
|
161
|
+
<OxyAuthScreen>
|
|
162
|
+
<OxyAuthScreenHeader title={t('linkCommons.compareTitle')} description={t('linkCommons.compare')} />
|
|
163
|
+
<Text
|
|
164
|
+
accessibilityLabel={step.code.split('').join(' ')}
|
|
165
|
+
style={[styles.code, { color: theme.colors.text }]}
|
|
166
|
+
testID="link-commons-code"
|
|
167
|
+
>
|
|
168
|
+
{`${step.code.slice(0, 3)} ${step.code.slice(3)}`}
|
|
169
|
+
</Text>
|
|
170
|
+
{error ? <AccountFlowErrorLine message={error} /> : null}
|
|
171
|
+
<AccountFlowAction label={t('linkCommons.confirm')} onPress={() => confirm(step.link)} pending={pending} testID="link-commons-confirm" />
|
|
172
|
+
<Button appearance="plain" tone="neutral" size="lg" fullWidth disabled={pending} onPress={() => cancel(step.link)} testID="link-commons-cancel">
|
|
173
|
+
{t('linkCommons.cancel')}
|
|
174
|
+
</Button>
|
|
175
|
+
</OxyAuthScreen>
|
|
176
|
+
);
|
|
177
|
+
case 'qr':
|
|
178
|
+
return (
|
|
179
|
+
<OxyAuthScreen>
|
|
180
|
+
<OxyAuthScreenHeader title={t('linkCommons.title')} description={t('linkCommons.subtitle')} />
|
|
181
|
+
<View style={styles.plateRow}>
|
|
182
|
+
<View style={[styles.plate, { borderColor: theme.colors.border }]} testID="link-commons-qr">
|
|
183
|
+
<QRCode value={step.link.qrPayload} size={QR_SIZE} backgroundColor={QR_PLATE_BG} color={QR_FOREGROUND} />
|
|
184
|
+
</View>
|
|
185
|
+
</View>
|
|
186
|
+
<AccountFlowNote>{t('linkCommons.scan')}</AccountFlowNote>
|
|
187
|
+
<AccountFlowNote>{t('linkCommons.waiting')}</AccountFlowNote>
|
|
188
|
+
<Button appearance="plain" tone="neutral" size="lg" fullWidth onPress={() => cancel(step.link)} testID="link-commons-cancel">
|
|
189
|
+
{t('linkCommons.cancel')}
|
|
190
|
+
</Button>
|
|
191
|
+
</OxyAuthScreen>
|
|
192
|
+
);
|
|
193
|
+
default:
|
|
194
|
+
return (
|
|
195
|
+
<OxyAuthScreen>
|
|
196
|
+
<OxyAuthScreenHeader title={t('linkCommons.title')} description={t('linkCommons.subtitle')} />
|
|
197
|
+
<View style={styles.plateRow}>
|
|
198
|
+
<ActivityIndicator size="large" color={theme.colors.primary} />
|
|
199
|
+
</View>
|
|
200
|
+
</OxyAuthScreen>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const styles = StyleSheet.create({
|
|
206
|
+
plateRow: {
|
|
207
|
+
alignItems: 'center',
|
|
208
|
+
},
|
|
209
|
+
plate: {
|
|
210
|
+
padding: 12,
|
|
211
|
+
borderRadius: 16,
|
|
212
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
213
|
+
backgroundColor: QR_PLATE_BG,
|
|
214
|
+
},
|
|
215
|
+
code: {
|
|
216
|
+
fontSize: 40,
|
|
217
|
+
lineHeight: 48,
|
|
218
|
+
fontWeight: '700',
|
|
219
|
+
letterSpacing: 4,
|
|
220
|
+
textAlign: 'center',
|
|
221
|
+
},
|
|
222
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovering a passkey account (ADR 0029 D3), auth.oxy.so's `/recover`:
|
|
3
|
+
*
|
|
4
|
+
* username or email → the code sent to the recovery email → new passkey → signed in
|
|
5
|
+
*
|
|
6
|
+
* The first step answers the same whether or not the account exists, so it
|
|
7
|
+
* never says who has one. An account that uses Commons has no recovery email:
|
|
8
|
+
* it recovers in Commons with its phrase, and the first step says so. The new
|
|
9
|
+
* passkey joins the ones the account has; lost ones are removed afterwards in
|
|
10
|
+
* the account's security settings.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type React from 'react';
|
|
14
|
+
import { useState } from 'react';
|
|
15
|
+
import { useTheme } from '@oxy.so/bloom/theme';
|
|
16
|
+
import { DeviceManager } from '@oxy.so/core';
|
|
17
|
+
import { useOxy } from '../../context/OxyContext';
|
|
18
|
+
import { runPasskeyRegisterSignIn } from '../../context/passkeyFlow';
|
|
19
|
+
import { useI18n } from '../../hooks/useI18n';
|
|
20
|
+
import { isPasskeySupported, runRegistrationCeremony } from '../../../webauthn/passkeyClient';
|
|
21
|
+
import { SubtleLink } from '../authChooser/primitives';
|
|
22
|
+
import { OxyAuthScreen, OxyAuthScreenHeader, OxyAuthTerms } from './OxyAuthScreen';
|
|
23
|
+
import {
|
|
24
|
+
AccountFlowAction,
|
|
25
|
+
AccountFlowErrorLine,
|
|
26
|
+
AccountFlowField,
|
|
27
|
+
AccountFlowNote,
|
|
28
|
+
EmailCodeStep,
|
|
29
|
+
describeAccountFlowError,
|
|
30
|
+
isTicketExpired,
|
|
31
|
+
} from './accountFlowParts';
|
|
32
|
+
|
|
33
|
+
type Step =
|
|
34
|
+
| { name: 'entry' }
|
|
35
|
+
| { name: 'code'; verificationId: string }
|
|
36
|
+
| { name: 'passkey'; recoveryTicket: string; username: string | null };
|
|
37
|
+
|
|
38
|
+
export interface OxyRecoverAccountPanelProps {
|
|
39
|
+
/** The account has a new passkey and this origin is signed in as it. */
|
|
40
|
+
onRecovered: () => void;
|
|
41
|
+
/** "Back to sign in". */
|
|
42
|
+
onSignIn: () => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const OxyRecoverAccountPanel: React.FC<OxyRecoverAccountPanelProps> = ({ onRecovered, onSignIn }) => {
|
|
46
|
+
const theme = useTheme();
|
|
47
|
+
const { t } = useI18n();
|
|
48
|
+
const { oxyServices, handleWebSession } = useOxy();
|
|
49
|
+
const [step, setStep] = useState<Step>({ name: 'entry' });
|
|
50
|
+
const [identifier, setIdentifier] = useState('');
|
|
51
|
+
const [pending, setPending] = useState(false);
|
|
52
|
+
const [error, setError] = useState<string | null>(null);
|
|
53
|
+
|
|
54
|
+
const run = (work: () => Promise<void>) => {
|
|
55
|
+
if (pending) return;
|
|
56
|
+
setError(null);
|
|
57
|
+
setPending(true);
|
|
58
|
+
work()
|
|
59
|
+
.catch((reason: unknown) => {
|
|
60
|
+
if (isTicketExpired(reason)) setStep({ name: 'entry' });
|
|
61
|
+
setError(describeAccountFlowError(reason, t));
|
|
62
|
+
})
|
|
63
|
+
.finally(() => setPending(false));
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const sendCode = async () => {
|
|
67
|
+
const { verificationId } = await oxyServices.startEmailVerification({ purpose: 'recovery', identifier: identifier.trim() });
|
|
68
|
+
setStep({ name: 'code', verificationId });
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const submitIdentifier = () => {
|
|
72
|
+
if (!identifier.trim()) {
|
|
73
|
+
setError(t('recover.identifier.required'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
run(sendCode);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// The ceremony opens the browser's passkey prompt, so it starts from the press.
|
|
80
|
+
const addPasskey = (recoveryTicket: string) =>
|
|
81
|
+
run(() =>
|
|
82
|
+
runPasskeyRegisterSignIn({
|
|
83
|
+
isSupported: isPasskeySupported,
|
|
84
|
+
getRegisterOptions: () => oxyServices.webauthnRegisterOptions({ recoveryTicket }),
|
|
85
|
+
runCeremony: runRegistrationCeremony,
|
|
86
|
+
registerVerify: (response) =>
|
|
87
|
+
oxyServices.webauthnRegisterVerify(response, {
|
|
88
|
+
recoveryTicket,
|
|
89
|
+
deviceFingerprint: JSON.stringify(DeviceManager.getDeviceFingerprint()),
|
|
90
|
+
}),
|
|
91
|
+
commit: async (session) => {
|
|
92
|
+
await handleWebSession(session);
|
|
93
|
+
onRecovered();
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
switch (step.name) {
|
|
99
|
+
case 'code':
|
|
100
|
+
return (
|
|
101
|
+
<EmailCodeStep
|
|
102
|
+
description={t('emailCode.sentIfAccount')}
|
|
103
|
+
verificationId={step.verificationId}
|
|
104
|
+
onConfirmed={({ ticket, username }) => {
|
|
105
|
+
setError(null);
|
|
106
|
+
setStep({ name: 'passkey', recoveryTicket: ticket, username });
|
|
107
|
+
}}
|
|
108
|
+
onResend={sendCode}
|
|
109
|
+
back={{ label: t('signin.actions.back'), onPress: () => setStep({ name: 'entry' }) }}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
case 'passkey':
|
|
113
|
+
return (
|
|
114
|
+
<OxyAuthScreen>
|
|
115
|
+
<OxyAuthScreenHeader
|
|
116
|
+
title={t('recover.passkey.title')}
|
|
117
|
+
description={t('recover.passkey.subtitle', { username: step.username ?? '' })}
|
|
118
|
+
/>
|
|
119
|
+
{error ? <AccountFlowErrorLine message={error} /> : null}
|
|
120
|
+
<AccountFlowAction
|
|
121
|
+
label={t('recover.passkey.action')}
|
|
122
|
+
onPress={() => addPasskey(step.recoveryTicket)}
|
|
123
|
+
pending={pending}
|
|
124
|
+
testID="recover-create-passkey"
|
|
125
|
+
/>
|
|
126
|
+
<OxyAuthTerms />
|
|
127
|
+
</OxyAuthScreen>
|
|
128
|
+
);
|
|
129
|
+
default:
|
|
130
|
+
return (
|
|
131
|
+
<OxyAuthScreen>
|
|
132
|
+
<OxyAuthScreenHeader title={t('recover.title')} description={t('recover.subtitle')} />
|
|
133
|
+
<AccountFlowField
|
|
134
|
+
label={t('recover.identifier.label')}
|
|
135
|
+
value={identifier}
|
|
136
|
+
onChange={(value) => {
|
|
137
|
+
setIdentifier(value);
|
|
138
|
+
if (error) setError(null);
|
|
139
|
+
}}
|
|
140
|
+
onSubmit={submitIdentifier}
|
|
141
|
+
error={error}
|
|
142
|
+
disabled={pending}
|
|
143
|
+
placeholder={t('recover.identifier.placeholder')}
|
|
144
|
+
autoComplete="username"
|
|
145
|
+
testID="recover-identifier"
|
|
146
|
+
/>
|
|
147
|
+
<AccountFlowNote>{t('recover.commonsNote')}</AccountFlowNote>
|
|
148
|
+
<AccountFlowAction label={t('signin.actions.continue')} onPress={submitIdentifier} pending={pending} testID="recover-continue" />
|
|
149
|
+
<SubtleLink label={t('recover.backToSignIn')} theme={theme} onPress={onSignIn} testID="back-to-sign-in" />
|
|
150
|
+
<OxyAuthTerms />
|
|
151
|
+
</OxyAuthScreen>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
* differs (`host`).
|
|
5
5
|
*
|
|
6
6
|
* picker a returning device: "Choose an account", then "Use another account"
|
|
7
|
-
* entry the header, the Commons way in, and the way in for
|
|
8
|
-
* account. On
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
7
|
+
* entry the header, the Commons way in, the passkey, and the way in for
|
|
8
|
+
* someone with no account. On the web, from `md`, the screen is
|
|
9
|
+
* Bloom `AuthCard`'s split card and the Commons way in is the
|
|
10
|
+
* embedded QR over its photo carousel, in the right column (the
|
|
11
|
+
* account dialog grows to that card); below `md` it is "Continue
|
|
12
|
+
* with Oxy" at the top. The passkey belongs to `oxy.so`: on
|
|
13
|
+
* auth.oxy.so (`page`) it runs here — the username with its Continue,
|
|
14
|
+
* a username-first ceremony that takes a hardware security key with
|
|
15
|
+
* no resident credential too, and the discoverable passkey — and in
|
|
16
|
+
* an app's dialog it opens auth.oxy.so's window for that one step, as
|
|
17
|
+
* does "Create account". On native it is "Continue with Oxy" ("Get
|
|
18
|
+
* Commons" without Commons).
|
|
19
19
|
*
|
|
20
20
|
* `signInMethods.ts` owns which blocks a platform gets.
|
|
21
21
|
*/
|
|
@@ -39,6 +39,7 @@ import { useOxy } from '../../context/OxyContext';
|
|
|
39
39
|
import { useAccountDialogSnapshot } from '../../hooks/accountDialogSnapshot';
|
|
40
40
|
import { useDeviceSwitcher } from '../../hooks/useDeviceSwitcher';
|
|
41
41
|
import { useI18n } from '../../hooks/useI18n';
|
|
42
|
+
import { useSurfaceFrameWidth } from '../../hooks/useSurfaceFrameWidth';
|
|
42
43
|
import { getCommonsAcquisitionUrl } from '../../utils/commonsStoreLinks';
|
|
43
44
|
import { isWebBrowser } from '../../utils/isWebBrowser';
|
|
44
45
|
import { SubtleLink } from '../authChooser/primitives';
|
|
@@ -49,6 +50,9 @@ import { OxyAuthScreen, OxyAuthScreenHeader, OxyAuthSplit, OxyAuthTerms } from '
|
|
|
49
50
|
import { RATE_LIMIT_SECONDS, describePasskeyError, isRateLimited } from './passkeyError';
|
|
50
51
|
import { resolveSignInMethods } from './signInMethods';
|
|
51
52
|
|
|
53
|
+
/** Bloom `AuthCard`'s split card width, which the account dialog grows to for it. */
|
|
54
|
+
const SPLIT_WIDTH = 880;
|
|
55
|
+
|
|
52
56
|
export interface OxySignInPanelProps {
|
|
53
57
|
/** This screen signed the origin in. */
|
|
54
58
|
onSignedIn: () => void;
|
|
@@ -124,7 +128,8 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
|
|
|
124
128
|
void controller?.signInWithOxy();
|
|
125
129
|
showRequest();
|
|
126
130
|
};
|
|
127
|
-
|
|
131
|
+
// The passkey is `oxy.so`'s: in an app it runs in auth.oxy.so's window.
|
|
132
|
+
const passkeyOnOxy = () => {
|
|
128
133
|
void continueOnAuth('signin').then((result) => {
|
|
129
134
|
if (result.status === 'signed-in') onSignedIn();
|
|
130
135
|
else if (result.status === 'failed') toast.error(t('signin.errors.failed'));
|
|
@@ -189,7 +194,7 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
|
|
|
189
194
|
return;
|
|
190
195
|
case 'failed':
|
|
191
196
|
// The pair could not be activated as it stands: sign in as it explicitly.
|
|
192
|
-
if (methods.passkey && context.handle) reauthenticate(context);
|
|
197
|
+
if (methods.passkey === 'here' && context.handle) reauthenticate(context);
|
|
193
198
|
else toast.error(t('accountSwitcher.toasts.activateFailed'));
|
|
194
199
|
return;
|
|
195
200
|
default:
|
|
@@ -198,8 +203,11 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
|
|
|
198
203
|
};
|
|
199
204
|
|
|
200
205
|
const showsPicker = pickerAllowed && !showForm && principals.length > 0;
|
|
201
|
-
//
|
|
206
|
+
// The web entry is the split card from `md`. In the account dialog it grows
|
|
207
|
+
// the dialog to that card; below `md` the dialog is a bottom sheet, which a
|
|
208
|
+
// width does not touch.
|
|
202
209
|
const splits = methods.commons === 'qr';
|
|
210
|
+
useSurfaceFrameWidth(host === 'dialog' && !showsPicker && splits ? SPLIT_WIDTH : null);
|
|
203
211
|
|
|
204
212
|
if (showsPicker) {
|
|
205
213
|
return (
|
|
@@ -241,7 +249,7 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
|
|
|
241
249
|
</Text>
|
|
242
250
|
);
|
|
243
251
|
// One solid action per screen: the username's Continue when there is one.
|
|
244
|
-
const commonsAppearance = methods.passkey ? 'outline' : 'solid';
|
|
252
|
+
const commonsAppearance = methods.passkey === 'here' ? 'outline' : 'solid';
|
|
245
253
|
|
|
246
254
|
const continueWithOxyButton = (
|
|
247
255
|
<Button
|
|
@@ -249,13 +257,28 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
|
|
|
249
257
|
tone={commonsAppearance === 'solid' ? 'action' : 'neutral'}
|
|
250
258
|
size="lg"
|
|
251
259
|
fullWidth
|
|
252
|
-
onPress={
|
|
260
|
+
onPress={continueWithOxy}
|
|
253
261
|
testID="continue-with-oxy"
|
|
254
262
|
>
|
|
255
263
|
{t('accountSwitcher.continueWithOxy')}
|
|
256
264
|
</Button>
|
|
257
265
|
);
|
|
258
266
|
|
|
267
|
+
const passkeyButton = (
|
|
268
|
+
<Button
|
|
269
|
+
appearance="outline"
|
|
270
|
+
tone="neutral"
|
|
271
|
+
size="lg"
|
|
272
|
+
fullWidth
|
|
273
|
+
leadingIcon={RiKey2Line}
|
|
274
|
+
disabled={methods.passkey === 'here' && blocked}
|
|
275
|
+
onPress={methods.passkey === 'here' ? () => void runPasskey(undefined, t('signin.errors.failed')) : passkeyOnOxy}
|
|
276
|
+
testID="passkey-sign-in"
|
|
277
|
+
>
|
|
278
|
+
{t('signin.methods.passkey')}
|
|
279
|
+
</Button>
|
|
280
|
+
);
|
|
281
|
+
|
|
259
282
|
const form = (
|
|
260
283
|
<OxyAuthScreen className={splits ? 'md:max-w-none' : undefined}>
|
|
261
284
|
<OxyAuthScreenHeader title={title} description={description} />
|
|
@@ -263,7 +286,7 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
|
|
|
263
286
|
{/* Below `md` this screen is the phone a QR would be scanned with. */}
|
|
264
287
|
{splits ? <CssView className="md:hidden">{continueWithOxyButton}</CssView> : null}
|
|
265
288
|
|
|
266
|
-
{methods.commons === 'continue'
|
|
289
|
+
{methods.commons === 'continue' ? continueWithOxyButton : null}
|
|
267
290
|
{methods.commons === 'get-commons' ? (
|
|
268
291
|
<View style={styles.stack}>
|
|
269
292
|
<Text style={[styles.note, { color: theme.colors.textSecondary }]}>{t('accountSwitcher.commonsNotInstalled')}</Text>
|
|
@@ -273,7 +296,7 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
|
|
|
273
296
|
</View>
|
|
274
297
|
) : null}
|
|
275
298
|
|
|
276
|
-
{methods.passkey ? (
|
|
299
|
+
{methods.passkey === 'here' ? (
|
|
277
300
|
<View style={styles.stack}>
|
|
278
301
|
<View style={styles.field}>
|
|
279
302
|
<TextFieldLabel nativeID="username-label">{t('signin.username.label')}</TextFieldLabel>
|
|
@@ -314,24 +337,17 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
|
|
|
314
337
|
</View>
|
|
315
338
|
) : null}
|
|
316
339
|
|
|
317
|
-
{/* The alternatives to the screen's primary way in, always under it.
|
|
318
|
-
|
|
319
|
-
{methods.
|
|
340
|
+
{/* The alternatives to the screen's primary way in, always under it. In an
|
|
341
|
+
app's dialog, from `md`, the QR beside it IS the primary way in. */}
|
|
342
|
+
{methods.passkey === 'window' ? (
|
|
343
|
+
<CssView className="md:hidden">
|
|
344
|
+
<Divider>{t('signin.orContinueWith')}</Divider>
|
|
345
|
+
</CssView>
|
|
346
|
+
) : (
|
|
347
|
+
<Divider>{t('signin.orContinueWith')}</Divider>
|
|
348
|
+
)}
|
|
320
349
|
|
|
321
|
-
{methods.passkey ? (
|
|
322
|
-
<Button
|
|
323
|
-
appearance="outline"
|
|
324
|
-
tone="neutral"
|
|
325
|
-
size="lg"
|
|
326
|
-
fullWidth
|
|
327
|
-
leadingIcon={RiKey2Line}
|
|
328
|
-
disabled={blocked}
|
|
329
|
-
onPress={() => void runPasskey(undefined, t('signin.errors.failed'))}
|
|
330
|
-
testID="passkey-sign-in"
|
|
331
|
-
>
|
|
332
|
-
{t('signin.methods.passkey')}
|
|
333
|
-
</Button>
|
|
334
|
-
) : methods.commons === 'window' ? null : (
|
|
350
|
+
{methods.passkey === 'none' ? (
|
|
335
351
|
<Button
|
|
336
352
|
appearance="outline"
|
|
337
353
|
tone="neutral"
|
|
@@ -343,9 +359,11 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
|
|
|
343
359
|
>
|
|
344
360
|
{t('accountSwitcher.scanQr')}
|
|
345
361
|
</Button>
|
|
362
|
+
) : (
|
|
363
|
+
passkeyButton
|
|
346
364
|
)}
|
|
347
365
|
|
|
348
|
-
{methods.passkey ? null : noAccount}
|
|
366
|
+
{methods.passkey === 'here' ? null : noAccount}
|
|
349
367
|
{onRecover ? (
|
|
350
368
|
<SubtleLink label={t('signin.recoverLink')} theme={theme} onPress={onRecover} testID="recover-link" />
|
|
351
369
|
) : null}
|
|
@@ -356,6 +374,7 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
|
|
|
356
374
|
if (!splits) return form;
|
|
357
375
|
return (
|
|
358
376
|
<OxyAuthSplit
|
|
377
|
+
bare={host === 'dialog'}
|
|
359
378
|
aside={
|
|
360
379
|
<>
|
|
361
380
|
<AuthMediaCarousel slides={SIGN_IN_SLIDES} style={StyleSheet.absoluteFill} />
|