@oxy.so/services 6.0.0 → 6.1.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 +1 -2
- package/lib/commonjs/ui/components/OxyAuthChooser.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/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/accountFlowParts.js +244 -0
- package/lib/commonjs/ui/components/signIn/accountFlowParts.js.map +1 -0
- 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/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 +2 -3
- package/lib/module/ui/components/OxyAuthChooser.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/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/accountFlowParts.js +235 -0
- package/lib/module/ui/components/signIn/accountFlowParts.js.map +1 -0
- 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/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.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/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/accountFlowParts.d.ts +60 -0
- package/lib/typescript/commonjs/ui/components/signIn/accountFlowParts.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +7 -1
- 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/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.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/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/accountFlowParts.d.ts +60 -0
- package/lib/typescript/module/ui/components/signIn/accountFlowParts.d.ts.map +1 -0
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +7 -1
- 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/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 +2 -3
- package/src/ui/components/authChooser/AccountsMenuView.tsx +0 -7
- package/src/ui/components/authChooser/types.ts +0 -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/accountFlowParts.tsx +204 -0
- package/src/ui/context/OxyContext.tsx +2 -1
- package/src/ui/context/__tests__/passkeyFlow.test.ts +59 -1
- package/src/ui/context/oxyContextTypes.ts +7 -1
- package/src/ui/context/passkeyFlow.ts +28 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/services",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"description": "OxyHQ Expo/React Native SDK — UI components, screens, and native features",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
},
|
|
156
156
|
"dependencies": {
|
|
157
157
|
"@expo/config-plugins": "57.0.9",
|
|
158
|
-
"@oxy.so/contracts": "^2.
|
|
158
|
+
"@oxy.so/contracts": "^2.2.0",
|
|
159
159
|
"@simplewebauthn/browser": "^13.3.0",
|
|
160
160
|
"color": "^4.2.3"
|
|
161
161
|
},
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
"@commitlint/cli": "^21.2.1",
|
|
165
165
|
"@commitlint/config-conventional": "^21.2.0",
|
|
166
166
|
"@expo/vector-icons": "^15.0.3",
|
|
167
|
-
"@oxy.so/core": "1.
|
|
167
|
+
"@oxy.so/core": "1.18.0",
|
|
168
168
|
"@react-native-async-storage/async-storage": "^2.0.0",
|
|
169
169
|
"@react-native-community/netinfo": "^12.0.1",
|
|
170
170
|
"@react-native/babel-preset": "^0.86.2",
|
|
@@ -216,7 +216,7 @@
|
|
|
216
216
|
"peerDependencies": {
|
|
217
217
|
"@expo/vector-icons": "^15.0.3",
|
|
218
218
|
"@oxy.so/bloom": "^4.26.0",
|
|
219
|
-
"@oxy.so/core": "^1.
|
|
219
|
+
"@oxy.so/core": "^1.18.0",
|
|
220
220
|
"@react-native-async-storage/async-storage": "^2.0.0",
|
|
221
221
|
"@react-native-community/netinfo": ">=11.4.1",
|
|
222
222
|
"@tanstack/query-async-storage-persister": "^5.101",
|
package/src/index.ts
CHANGED
|
@@ -368,6 +368,17 @@ export { OxySignUpPanel } from './ui/components/signIn/OxySignUpPanel';
|
|
|
368
368
|
export type { OxySignUpPanelProps } from './ui/components/signIn/OxySignUpPanel';
|
|
369
369
|
export { OxyAccountPicker } from './ui/components/signIn/OxyAccountPicker';
|
|
370
370
|
export type { OxyAccountPickerProps } from './ui/components/signIn/OxyAccountPicker';
|
|
371
|
+
// auth.oxy.so's account pages (ADR 0029 D3): a passkey account is created,
|
|
372
|
+
// recovered through its recovery email, deleted with its passkey, and linked
|
|
373
|
+
// to Commons there.
|
|
374
|
+
export { OxyCreateAccountPanel } from './ui/components/signIn/OxyCreateAccountPanel';
|
|
375
|
+
export type { OxyCreateAccountPanelProps } from './ui/components/signIn/OxyCreateAccountPanel';
|
|
376
|
+
export { OxyRecoverAccountPanel } from './ui/components/signIn/OxyRecoverAccountPanel';
|
|
377
|
+
export type { OxyRecoverAccountPanelProps } from './ui/components/signIn/OxyRecoverAccountPanel';
|
|
378
|
+
export { OxyDeleteAccountPanel } from './ui/components/signIn/OxyDeleteAccountPanel';
|
|
379
|
+
export type { OxyDeleteAccountPanelProps } from './ui/components/signIn/OxyDeleteAccountPanel';
|
|
380
|
+
export { OxyLinkCommonsPanel } from './ui/components/signIn/OxyLinkCommonsPanel';
|
|
381
|
+
export type { OxyLinkCommonsPanelProps } from './ui/components/signIn/OxyLinkCommonsPanel';
|
|
371
382
|
export {
|
|
372
383
|
OxyAuthScreen,
|
|
373
384
|
OxyAuthScreenHeader,
|
|
@@ -38,7 +38,7 @@ import { Linking, Platform } from 'react-native';
|
|
|
38
38
|
import { toast } from '@oxy.so/bloom/toast';
|
|
39
39
|
import { surfaces } from '@oxy.so/bloom/surfaces';
|
|
40
40
|
import { useTheme } from '@oxy.so/bloom/theme';
|
|
41
|
-
import {
|
|
41
|
+
import { getNormalizedUserHandle, type OxyAuthScreen, type User } from '@oxy.so/core';
|
|
42
42
|
import { useQueryClient } from '@tanstack/react-query';
|
|
43
43
|
import { useOxy } from '../context/OxyContext';
|
|
44
44
|
import { useDeviceSwitcher } from '../hooks/useDeviceSwitcher';
|
|
@@ -419,7 +419,6 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({ onComplete }) => {
|
|
|
419
419
|
onSignOut: () => {
|
|
420
420
|
void handleSignOut();
|
|
421
421
|
},
|
|
422
|
-
onOpenIdentity: passkeyMode === 'hub' ? () => openUrl(`${AUTH_WEB_ORIGIN}/identity`) : undefined,
|
|
423
422
|
customItems: (consumerHooks?.menuItems ?? []).map((item) => ({
|
|
424
423
|
...item,
|
|
425
424
|
onPress: () => {
|
|
@@ -428,7 +427,7 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({ onComplete }) => {
|
|
|
428
427
|
},
|
|
429
428
|
})),
|
|
430
429
|
};
|
|
431
|
-
}, [consumerHooks, onComplete, showBottomSheet, handleSignOut, openExternal
|
|
430
|
+
}, [consumerHooks, onComplete, showBottomSheet, handleSignOut, openExternal]);
|
|
432
431
|
|
|
433
432
|
if (!controller) {
|
|
434
433
|
return null;
|
|
@@ -504,13 +504,6 @@ const AccountsMenuView: React.FC<AccountsMenuViewProps> = ({
|
|
|
504
504
|
title={t('accountMenu.settings')}
|
|
505
505
|
onPress={menu.onOpenSettings}
|
|
506
506
|
/>
|
|
507
|
-
{menu.onOpenIdentity ? (
|
|
508
|
-
<SettingsListItem
|
|
509
|
-
icon={<MenuIcon name="shield-account-outline" theme={theme} />}
|
|
510
|
-
title={t('accountMenu.identity')}
|
|
511
|
-
onPress={menu.onOpenIdentity}
|
|
512
|
-
/>
|
|
513
|
-
) : null}
|
|
514
507
|
<SettingsListItem
|
|
515
508
|
icon={<MenuIcon name="help-circle-outline" theme={theme} />}
|
|
516
509
|
title={t('accountMenu.help')}
|
|
@@ -95,11 +95,6 @@ export interface AccountsMenuActions {
|
|
|
95
95
|
onPrivacy: () => void;
|
|
96
96
|
onTerms: () => void;
|
|
97
97
|
onSignOut: () => void;
|
|
98
|
-
/**
|
|
99
|
-
* Open the person's identity at auth.oxy.so/identity (recovery phrase,
|
|
100
|
-
* recovery, account deletion). Web only — `undefined` hides the row.
|
|
101
|
-
*/
|
|
102
|
-
onOpenIdentity?: () => void;
|
|
103
98
|
customItems: readonly {
|
|
104
99
|
key: string;
|
|
105
100
|
label: string;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creating an Oxy account on the web (ADR 0029 D3), auth.oxy.so's `/signup`:
|
|
3
|
+
*
|
|
4
|
+
* username → recovery email → the code sent to it → passkey → signed in
|
|
5
|
+
*
|
|
6
|
+
* A web account is a username, a passkey and a recovery email — no password, no
|
|
7
|
+
* phrase, no key. The email is confirmed before the account exists, and is
|
|
8
|
+
* used only to get back in (`OxyRecoverAccountPanel`). Commons stays the way to
|
|
9
|
+
* hold your own key: the first step says so, and linking Commons later makes
|
|
10
|
+
* the account self-custodied and deletes the email.
|
|
11
|
+
*
|
|
12
|
+
* Only on auth.oxy.so: the passkey belongs to that origin (RP ID `oxy.so`,
|
|
13
|
+
* asserted only there), and an app's dialog opens this page in a window
|
|
14
|
+
* (`OxySignUpPanel`'s `onCreateOnWeb`).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type React from 'react';
|
|
18
|
+
import { useState } from 'react';
|
|
19
|
+
import { Linking, Platform } from 'react-native';
|
|
20
|
+
import { useTheme } from '@oxy.so/bloom/theme';
|
|
21
|
+
import { toast } from '@oxy.so/bloom/toast';
|
|
22
|
+
import { DeviceManager } from '@oxy.so/core';
|
|
23
|
+
import { emailAddressSchema } from '@oxy.so/contracts';
|
|
24
|
+
import { useOxy } from '../../context/OxyContext';
|
|
25
|
+
import { runPasskeyRegisterSignIn } from '../../context/passkeyFlow';
|
|
26
|
+
import { useI18n } from '../../hooks/useI18n';
|
|
27
|
+
import { getCommonsAcquisitionUrl } from '../../utils/commonsStoreLinks';
|
|
28
|
+
import { isPasskeySupported, runRegistrationCeremony } from '../../../webauthn/passkeyClient';
|
|
29
|
+
import { SubtleLink } from '../authChooser/primitives';
|
|
30
|
+
import { OxyAuthScreen, OxyAuthScreenHeader, OxyAuthTerms } from './OxyAuthScreen';
|
|
31
|
+
import {
|
|
32
|
+
AccountFlowAction,
|
|
33
|
+
AccountFlowErrorLine,
|
|
34
|
+
AccountFlowField,
|
|
35
|
+
EmailCodeStep,
|
|
36
|
+
describeAccountFlowError,
|
|
37
|
+
isTicketExpired,
|
|
38
|
+
} from './accountFlowParts';
|
|
39
|
+
|
|
40
|
+
type Step =
|
|
41
|
+
| { name: 'username' }
|
|
42
|
+
| { name: 'email' }
|
|
43
|
+
| { name: 'code'; verificationId: string }
|
|
44
|
+
| { name: 'passkey'; emailTicket: string };
|
|
45
|
+
|
|
46
|
+
export interface OxyCreateAccountPanelProps {
|
|
47
|
+
/** The account exists and this origin is signed in as it. */
|
|
48
|
+
onSignedIn: () => void;
|
|
49
|
+
/** "Already have an account? Sign in". */
|
|
50
|
+
onSignIn: () => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const OxyCreateAccountPanel: React.FC<OxyCreateAccountPanelProps> = ({ onSignedIn, onSignIn }) => {
|
|
54
|
+
const theme = useTheme();
|
|
55
|
+
const { t } = useI18n();
|
|
56
|
+
const { oxyServices, handleWebSession } = useOxy();
|
|
57
|
+
const [step, setStep] = useState<Step>({ name: 'username' });
|
|
58
|
+
const [username, setUsername] = useState('');
|
|
59
|
+
const [email, setEmail] = useState('');
|
|
60
|
+
const [pending, setPending] = useState(false);
|
|
61
|
+
const [error, setError] = useState<string | null>(null);
|
|
62
|
+
|
|
63
|
+
const run = (work: () => Promise<void>) => {
|
|
64
|
+
if (pending) return;
|
|
65
|
+
setError(null);
|
|
66
|
+
setPending(true);
|
|
67
|
+
work()
|
|
68
|
+
.catch((reason: unknown) => {
|
|
69
|
+
if (isTicketExpired(reason)) setStep({ name: 'email' });
|
|
70
|
+
setError(describeAccountFlowError(reason, t));
|
|
71
|
+
})
|
|
72
|
+
.finally(() => setPending(false));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const submitUsername = () => {
|
|
76
|
+
const handle = username.trim();
|
|
77
|
+
if (!handle) {
|
|
78
|
+
setError(t('signup.username.required'));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
run(async () => {
|
|
82
|
+
const { available } = await oxyServices.checkUsernameAvailability(handle);
|
|
83
|
+
if (!available) throw new Error(t('signup.username.taken'));
|
|
84
|
+
setStep({ name: 'email' });
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const sendCode = async (address: string) => {
|
|
89
|
+
const { verificationId } = await oxyServices.startEmailVerification({ purpose: 'signup', email: address });
|
|
90
|
+
setStep({ name: 'code', verificationId });
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const submitEmail = () => {
|
|
94
|
+
const parsed = emailAddressSchema.safeParse(email);
|
|
95
|
+
if (!parsed.success) {
|
|
96
|
+
setError(t('signup.email.invalid'));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
setEmail(parsed.data);
|
|
100
|
+
run(() => sendCode(parsed.data));
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// The ceremony opens the browser's passkey prompt, so it starts from the press.
|
|
104
|
+
const createPasskey = (emailTicket: string) =>
|
|
105
|
+
run(() =>
|
|
106
|
+
runPasskeyRegisterSignIn({
|
|
107
|
+
isSupported: isPasskeySupported,
|
|
108
|
+
getRegisterOptions: () => oxyServices.webauthnRegisterOptions({ username: username.trim() }),
|
|
109
|
+
runCeremony: runRegistrationCeremony,
|
|
110
|
+
registerVerify: (response) =>
|
|
111
|
+
oxyServices.webauthnRegisterVerify(response, {
|
|
112
|
+
username: username.trim(),
|
|
113
|
+
email,
|
|
114
|
+
emailTicket,
|
|
115
|
+
deviceFingerprint: JSON.stringify(DeviceManager.getDeviceFingerprint()),
|
|
116
|
+
}),
|
|
117
|
+
commit: async (session) => {
|
|
118
|
+
await handleWebSession(session);
|
|
119
|
+
onSignedIn();
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const getCommons = () => {
|
|
125
|
+
Promise.resolve()
|
|
126
|
+
.then(() => Linking.openURL(getCommonsAcquisitionUrl(Platform.OS)))
|
|
127
|
+
.catch(() => toast.error(t('accountSwitcher.linkOpenFailed')));
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
switch (step.name) {
|
|
131
|
+
case 'code':
|
|
132
|
+
return (
|
|
133
|
+
<EmailCodeStep
|
|
134
|
+
description={t('emailCode.sentTo', { email })}
|
|
135
|
+
verificationId={step.verificationId}
|
|
136
|
+
onConfirmed={({ ticket }) => {
|
|
137
|
+
setError(null);
|
|
138
|
+
setStep({ name: 'passkey', emailTicket: ticket });
|
|
139
|
+
}}
|
|
140
|
+
onResend={() => sendCode(email)}
|
|
141
|
+
back={{ label: t('emailCode.changeEmail'), onPress: () => setStep({ name: 'email' }) }}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
case 'passkey':
|
|
145
|
+
return (
|
|
146
|
+
<OxyAuthScreen>
|
|
147
|
+
<OxyAuthScreenHeader title={t('signup.passkey.title')} description={t('signup.passkey.subtitle')} />
|
|
148
|
+
{error ? <AccountFlowErrorLine message={error} /> : null}
|
|
149
|
+
<AccountFlowAction
|
|
150
|
+
label={t('signup.passkey.action')}
|
|
151
|
+
onPress={() => createPasskey(step.emailTicket)}
|
|
152
|
+
pending={pending}
|
|
153
|
+
testID="signup-create-passkey"
|
|
154
|
+
/>
|
|
155
|
+
<OxyAuthTerms />
|
|
156
|
+
</OxyAuthScreen>
|
|
157
|
+
);
|
|
158
|
+
case 'email':
|
|
159
|
+
return (
|
|
160
|
+
<OxyAuthScreen>
|
|
161
|
+
<OxyAuthScreenHeader title={t('signup.email.title')} description={t('signup.email.subtitle')} />
|
|
162
|
+
<AccountFlowField
|
|
163
|
+
label={t('signup.email.label')}
|
|
164
|
+
value={email}
|
|
165
|
+
onChange={(value) => {
|
|
166
|
+
setEmail(value);
|
|
167
|
+
if (error) setError(null);
|
|
168
|
+
}}
|
|
169
|
+
onSubmit={submitEmail}
|
|
170
|
+
error={error}
|
|
171
|
+
disabled={pending}
|
|
172
|
+
placeholder={t('signup.email.placeholder')}
|
|
173
|
+
autoComplete="email"
|
|
174
|
+
keyboardType="email-address"
|
|
175
|
+
testID="signup-email"
|
|
176
|
+
/>
|
|
177
|
+
<AccountFlowAction label={t('signin.actions.continue')} onPress={submitEmail} pending={pending} testID="signup-email-continue" />
|
|
178
|
+
<SubtleLink label={t('signin.actions.back')} theme={theme} onPress={() => setStep({ name: 'username' })} testID="signup-back" />
|
|
179
|
+
<OxyAuthTerms />
|
|
180
|
+
</OxyAuthScreen>
|
|
181
|
+
);
|
|
182
|
+
default:
|
|
183
|
+
return (
|
|
184
|
+
<OxyAuthScreen>
|
|
185
|
+
<OxyAuthScreenHeader title={t('signup.title')} description={t('signup.webSubtitle')} />
|
|
186
|
+
<AccountFlowField
|
|
187
|
+
label={t('signup.username.label')}
|
|
188
|
+
value={username}
|
|
189
|
+
onChange={(value) => {
|
|
190
|
+
setUsername(value);
|
|
191
|
+
if (error) setError(null);
|
|
192
|
+
}}
|
|
193
|
+
onSubmit={submitUsername}
|
|
194
|
+
error={error}
|
|
195
|
+
disabled={pending}
|
|
196
|
+
placeholder={t('signup.username.placeholder')}
|
|
197
|
+
autoComplete="username"
|
|
198
|
+
testID="signup-username"
|
|
199
|
+
/>
|
|
200
|
+
<AccountFlowAction label={t('signin.actions.continue')} onPress={submitUsername} pending={pending} testID="signup-username-continue" />
|
|
201
|
+
<SubtleLink label={t('signup.commonsInstead')} theme={theme} onPress={getCommons} testID="signup-commons-instead" />
|
|
202
|
+
<SubtleLink label={t('signup.backToSignInLink')} theme={theme} onPress={onSignIn} testID="back-to-sign-in" />
|
|
203
|
+
<OxyAuthTerms />
|
|
204
|
+
</OxyAuthScreen>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
@@ -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
|
+
});
|