@oxyhq/services 21.0.1 → 22.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/lib/commonjs/index.js +12 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/boot/runProviderColdBoot.js +12 -2
- package/lib/commonjs/ui/boot/runProviderColdBoot.js.map +1 -1
- package/lib/commonjs/ui/components/OxyAccountDialog.js +27 -546
- package/lib/commonjs/ui/components/OxyAccountDialog.js.map +1 -1
- package/lib/commonjs/ui/components/OxyAuthChooser.js +881 -0
- package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -0
- package/lib/commonjs/ui/components/OxyProvider.js +2 -0
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/components/oxyAuthChooserStyles.js +158 -0
- package/lib/commonjs/ui/components/oxyAuthChooserStyles.js.map +1 -0
- package/lib/commonjs/ui/context/OxyContext.js +50 -72
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/passkeyFlow.js +9 -8
- package/lib/commonjs/ui/context/passkeyFlow.js.map +1 -1
- package/lib/commonjs/ui/context/useOxyAccountGraph.js +6 -2
- package/lib/commonjs/ui/context/useOxyAccountGraph.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +2 -1
- package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -1
- package/lib/commonjs/ui/utils/commonsStoreLinks.js +42 -0
- package/lib/commonjs/ui/utils/commonsStoreLinks.js.map +1 -0
- package/lib/commonjs/ui/utils/crossOriginRestore.js +26 -0
- package/lib/commonjs/ui/utils/crossOriginRestore.js.map +1 -1
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/boot/runProviderColdBoot.js +13 -3
- package/lib/module/ui/boot/runProviderColdBoot.js.map +1 -1
- package/lib/module/ui/components/OxyAccountDialog.js +29 -550
- package/lib/module/ui/components/OxyAccountDialog.js.map +1 -1
- package/lib/module/ui/components/OxyAuthChooser.js +878 -0
- package/lib/module/ui/components/OxyAuthChooser.js.map +1 -0
- package/lib/module/ui/components/OxyProvider.js +2 -0
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/oxyAuthChooserStyles.js +155 -0
- package/lib/module/ui/components/oxyAuthChooserStyles.js.map +1 -0
- package/lib/module/ui/context/OxyContext.js +50 -72
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/passkeyFlow.js +9 -8
- package/lib/module/ui/context/passkeyFlow.js.map +1 -1
- package/lib/module/ui/context/useOxyAccountGraph.js +6 -2
- package/lib/module/ui/context/useOxyAccountGraph.js.map +1 -1
- package/lib/module/ui/hooks/useSwitchableAccounts.js +2 -1
- package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -1
- package/lib/module/ui/utils/commonsStoreLinks.js +37 -0
- package/lib/module/ui/utils/commonsStoreLinks.js.map +1 -0
- package/lib/module/ui/utils/crossOriginRestore.js +26 -1
- package/lib/module/ui/utils/crossOriginRestore.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +3 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts +6 -0
- package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts +10 -20
- package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts +60 -0
- package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/oxyAuthChooserStyles.d.ts +148 -0
- package/lib/typescript/commonjs/ui/components/oxyAuthChooserStyles.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +22 -27
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/passkeyFlow.d.ts +15 -4
- package/lib/typescript/commonjs/ui/context/passkeyFlow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts +1 -0
- package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/types/navigation.d.ts +8 -0
- package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/commonsStoreLinks.d.ts +24 -0
- package/lib/typescript/commonjs/ui/utils/commonsStoreLinks.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/crossOriginRestore.d.ts +19 -0
- package/lib/typescript/commonjs/ui/utils/crossOriginRestore.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +3 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts +6 -0
- package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts +10 -20
- package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts +60 -0
- package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/oxyAuthChooserStyles.d.ts +148 -0
- package/lib/typescript/module/ui/components/oxyAuthChooserStyles.d.ts.map +1 -0
- package/lib/typescript/module/ui/context/OxyContext.d.ts +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +22 -27
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/passkeyFlow.d.ts +15 -4
- package/lib/typescript/module/ui/context/passkeyFlow.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts +1 -0
- package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -1
- package/lib/typescript/module/ui/types/navigation.d.ts +8 -0
- package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/commonsStoreLinks.d.ts +24 -0
- package/lib/typescript/module/ui/utils/commonsStoreLinks.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/crossOriginRestore.d.ts +19 -0
- package/lib/typescript/module/ui/utils/crossOriginRestore.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +8 -1
- package/src/ui/boot/runProviderColdBoot.ts +24 -1
- package/src/ui/components/OxyAccountDialog.tsx +33 -571
- package/src/ui/components/OxyAuthChooser.tsx +883 -0
- package/src/ui/components/OxyProvider.tsx +2 -0
- package/src/ui/components/oxyAuthChooserStyles.ts +152 -0
- package/src/ui/context/OxyContext.tsx +56 -82
- package/src/ui/context/__tests__/passkeyFlow.test.ts +22 -15
- package/src/ui/context/oxyContextTypes.ts +27 -26
- package/src/ui/context/passkeyFlow.ts +17 -9
- package/src/ui/context/useOxyAccountGraph.ts +4 -2
- package/src/ui/hooks/useSwitchableAccounts.ts +1 -0
- package/src/ui/types/navigation.ts +8 -0
- package/src/ui/utils/__tests__/crossOriginRestore.test.ts +143 -0
- package/src/ui/utils/commonsStoreLinks.ts +38 -0
- package/src/ui/utils/crossOriginRestore.ts +34 -0
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OxyAccountDialog — the ONE unified account dialog for `@oxyhq/services`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* (the uniform switch), the active row is flagged, and a "+ Add account" row
|
|
12
|
-
* opens the sign-in entry. This is what `ProfileButton` opens.
|
|
13
|
-
* - `add` / `signin` — the sign-in entry: primary "Sign in with Oxy" (device
|
|
14
|
-
* flow), "Scan a QR", and a secondary "Use a password" hand-off that opens
|
|
15
|
-
* auth.oxy.so (password + 2FA are NOT in the SDK).
|
|
16
|
-
* - `qr` — the cross-device QR handoff + its waiting state.
|
|
17
|
-
*
|
|
18
|
-
* Per-account color re-theming uses Bloom's `APP_COLOR_PRESETS` + `BloomColorScope`
|
|
19
|
-
* (same visual language auth.oxy.so uses). Base theming is `useTheme()` + a
|
|
20
|
-
* `StyleSheet`, so the dialog renders correctly in EVERY consumer — including apps
|
|
21
|
-
* that do not use NativeWind (e.g. the accounts app).
|
|
4
|
+
* Bloom `<Dialog>` chrome (header, back button, placement, scroll body) around
|
|
5
|
+
* the headless chooser logic, which lives in `OxyAuthChooser` — the account
|
|
6
|
+
* switcher, sign-in, sign-up, and QR views are ALL there, extracted so the
|
|
7
|
+
* same chooser can be mounted bare by a future host with no Dialog chrome
|
|
8
|
+
* (e.g. an auth.oxy.so hub page driving the cross-origin passkey popup, b2).
|
|
9
|
+
* This file owns ONLY the chrome: the title/subtitle per view, the back
|
|
10
|
+
* button, and the Bloom `<Dialog>` surface itself.
|
|
22
11
|
*
|
|
23
12
|
* The surface is Bloom's `<Dialog>` (`@oxyhq/bloom/dialog`) with a responsive
|
|
24
13
|
* `placement` — a bottom sheet on narrow viewports, a centered card on wide ones.
|
|
@@ -34,121 +23,39 @@
|
|
|
34
23
|
* Backdrop / swipe-to-dismiss is disabled (`dismissOnBackdrop={false}`) on purpose:
|
|
35
24
|
* Bloom's controlled `bottom` placement does not fire `onClose` on a gesture or
|
|
36
25
|
* backdrop dismissal, so allowing it would desync `isAccountDialogOpen` from the
|
|
37
|
-
* sheet and block reopening. The header close button (and
|
|
38
|
-
* drive `closeAccountDialog`, which flips `open` and runs the exit
|
|
26
|
+
* sheet and block reopening. The header close button (and `OxyAuthChooser`'s
|
|
27
|
+
* `onComplete`) drive `closeAccountDialog`, which flips `open` and runs the exit
|
|
28
|
+
* animation.
|
|
39
29
|
*/
|
|
40
30
|
|
|
41
31
|
import type React from 'react';
|
|
42
|
-
import { useCallback,
|
|
43
|
-
import {
|
|
44
|
-
Pressable,
|
|
45
|
-
ScrollView,
|
|
46
|
-
StyleSheet,
|
|
47
|
-
View,
|
|
48
|
-
type StyleProp,
|
|
49
|
-
type ViewStyle,
|
|
50
|
-
} from 'react-native';
|
|
32
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
33
|
+
import { Pressable, ScrollView, StyleSheet, View } from 'react-native';
|
|
51
34
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
52
|
-
import QRCode from 'react-native-qrcode-svg';
|
|
53
|
-
import { Avatar } from '@oxyhq/bloom/avatar';
|
|
54
|
-
import { Button } from '@oxyhq/bloom/button';
|
|
55
35
|
import { Dialog } from '@oxyhq/bloom/dialog';
|
|
56
36
|
import { Text } from '@oxyhq/bloom/typography';
|
|
57
|
-
import {
|
|
58
|
-
|
|
59
|
-
BloomColorScope,
|
|
60
|
-
APP_COLOR_NAMES,
|
|
61
|
-
APP_COLOR_PRESETS,
|
|
62
|
-
type AppColorName,
|
|
63
|
-
} from '@oxyhq/bloom/theme';
|
|
64
|
-
import type { SwitchableAccount, AccountDialogSnapshot } from '@oxyhq/core';
|
|
65
|
-
import { isOxyRpOrigin } from '@oxyhq/core';
|
|
66
|
-
import { useQueryClient } from '@tanstack/react-query';
|
|
37
|
+
import { useTheme } from '@oxyhq/bloom/theme';
|
|
38
|
+
import type { AccountDialogSnapshot } from '@oxyhq/core';
|
|
67
39
|
import { useOxy } from '../context/OxyContext';
|
|
68
40
|
import { useI18n } from '../hooks/useI18n';
|
|
69
|
-
import
|
|
41
|
+
import OxyAuthChooser from './OxyAuthChooser';
|
|
70
42
|
import { LogoIcon } from './logo/LogoIcon';
|
|
71
43
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
/** High-contrast QR colors — intentionally fixed (NOT themed) for scan reliability. */
|
|
75
|
-
const QR_PLATE_BG = '#FFFFFF';
|
|
76
|
-
const QR_FOREGROUND = '#000000';
|
|
77
|
-
const QR_SIZE = 196;
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Resolve an account's stored color (a named Bloom preset, e.g. `'purple'`) to
|
|
81
|
-
* a concrete brand hex for the row accent. Falls back to the theme primary when
|
|
82
|
-
* the account has no color or the value is not a recognized preset, so the accent
|
|
83
|
-
* renders in EVERY consumer regardless of NativeWind availability.
|
|
84
|
-
*/
|
|
85
|
-
function resolveAccentHex(color: string | null, fallback: string): string {
|
|
86
|
-
const preset = toPreset(color);
|
|
87
|
-
return preset ? APP_COLOR_PRESETS[preset].hex : fallback;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Narrow a stored color string to a known `AppColorName`, or `undefined`. */
|
|
91
|
-
function toPreset(color: string | null): AppColorName | undefined {
|
|
92
|
-
if (!color) return undefined;
|
|
93
|
-
return (APP_COLOR_NAMES as readonly string[]).includes(color)
|
|
94
|
-
? (color as AppColorName)
|
|
95
|
-
: undefined;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
interface OxyAccountDialogHandlers {
|
|
99
|
-
onSwitch: (accountId: string) => void;
|
|
100
|
-
onAdd: () => void;
|
|
101
|
-
onManage: () => void;
|
|
102
|
-
onClose: () => void;
|
|
103
|
-
}
|
|
44
|
+
type Theme = ReturnType<typeof useTheme>;
|
|
45
|
+
type Translate = ReturnType<typeof useI18n>['t'];
|
|
104
46
|
|
|
105
47
|
/**
|
|
106
48
|
* The unified account dialog. Mounted once by `OxyProvider`; opened imperatively
|
|
107
49
|
* via `useOxy().openAccountDialog(view?)` or imperative `openAccountDialog('signin')`.
|
|
108
50
|
*/
|
|
109
51
|
const OxyAccountDialog: React.FC = () => {
|
|
110
|
-
const {
|
|
111
|
-
accountDialogController: controller,
|
|
112
|
-
isAccountDialogOpen,
|
|
113
|
-
closeAccountDialog,
|
|
114
|
-
showBottomSheet,
|
|
115
|
-
logoutAll,
|
|
116
|
-
refreshAccounts,
|
|
117
|
-
signInWithPasskey,
|
|
118
|
-
} = useOxy();
|
|
52
|
+
const { accountDialogController: controller, isAccountDialogOpen, closeAccountDialog } = useOxy();
|
|
119
53
|
const theme = useTheme();
|
|
120
54
|
const { t } = useI18n();
|
|
121
|
-
const queryClient = useQueryClient();
|
|
122
|
-
|
|
123
|
-
// Passkey sign-in is offered ONLY on a first-party Oxy web origin (a credential
|
|
124
|
-
// minted for `oxy.so` can only be asserted there or on a loopback dev host).
|
|
125
|
-
// Off the web / on a non-Oxy origin the button is hidden entirely (a hub popup
|
|
126
|
-
// for arbitrary web origins is a later phase). This is environment-static.
|
|
127
|
-
const passkeyAvailable = useMemo(() => isWebBrowser() && isOxyRpOrigin(), []);
|
|
128
|
-
const [passkeyPending, setPasskeyPending] = useState(false);
|
|
129
|
-
const [passkeyError, setPasskeyError] = useState<string | null>(null);
|
|
130
|
-
|
|
131
|
-
const handleSignInWithPasskey = useCallback(async () => {
|
|
132
|
-
if (passkeyPending) return;
|
|
133
|
-
setPasskeyPending(true);
|
|
134
|
-
setPasskeyError(null);
|
|
135
|
-
try {
|
|
136
|
-
await signInWithPasskey();
|
|
137
|
-
closeAccountDialog();
|
|
138
|
-
} catch (error) {
|
|
139
|
-
// A cancelled/failed ceremony keeps the dialog open so the user can retry
|
|
140
|
-
// or fall back to another method — surface a concise message, never swallow.
|
|
141
|
-
setPasskeyError(
|
|
142
|
-
error instanceof Error && error.message ? error.message : 'Passkey sign-in failed.',
|
|
143
|
-
);
|
|
144
|
-
} finally {
|
|
145
|
-
setPasskeyPending(false);
|
|
146
|
-
}
|
|
147
|
-
}, [passkeyPending, signInWithPasskey, closeAccountDialog]);
|
|
148
55
|
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
56
|
+
// A lightweight, header-only binding to the same controller `OxyAuthChooser`
|
|
57
|
+
// binds independently — cheap, and the established pattern here (
|
|
58
|
+
// `useSwitchableAccounts` also binds to this controller on its own).
|
|
152
59
|
const subscribe = useCallback(
|
|
153
60
|
(listener: () => void) => (controller ? controller.subscribe(listener) : () => undefined),
|
|
154
61
|
[controller],
|
|
@@ -159,62 +66,13 @@ const OxyAccountDialog: React.FC = () => {
|
|
|
159
66
|
);
|
|
160
67
|
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
161
68
|
|
|
162
|
-
const [switching, setSwitching] = useState(false);
|
|
163
|
-
|
|
164
|
-
const handleSwitch = useCallback(
|
|
165
|
-
async (accountId: string) => {
|
|
166
|
-
if (!controller || switching) return;
|
|
167
|
-
if (accountId === snapshot.activeAccountId) {
|
|
168
|
-
closeAccountDialog();
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
setSwitching(true);
|
|
172
|
-
try {
|
|
173
|
-
await controller.switchTo(accountId);
|
|
174
|
-
// A switch succeeds unless the controller recorded an error. On success
|
|
175
|
-
// reload the app's account graph and drop cached account-scoped data so
|
|
176
|
-
// the new active identity re-fetches — the same side effects the
|
|
177
|
-
// context's own `switchToAccount` performs.
|
|
178
|
-
if (!controller.getSnapshot().error) {
|
|
179
|
-
void refreshAccounts();
|
|
180
|
-
queryClient.invalidateQueries();
|
|
181
|
-
closeAccountDialog();
|
|
182
|
-
}
|
|
183
|
-
} finally {
|
|
184
|
-
setSwitching(false);
|
|
185
|
-
}
|
|
186
|
-
},
|
|
187
|
-
[controller, switching, snapshot.activeAccountId, closeAccountDialog, refreshAccounts, queryClient],
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
const handleManage = useCallback(() => {
|
|
191
|
-
closeAccountDialog();
|
|
192
|
-
showBottomSheet?.('ManageAccount');
|
|
193
|
-
}, [closeAccountDialog, showBottomSheet]);
|
|
194
|
-
|
|
195
|
-
const handleSignOutAll = useCallback(() => {
|
|
196
|
-
closeAccountDialog();
|
|
197
|
-
void logoutAll();
|
|
198
|
-
}, [closeAccountDialog, logoutAll]);
|
|
199
|
-
|
|
200
|
-
const handlers = useMemo<OxyAccountDialogHandlers>(
|
|
201
|
-
() => ({
|
|
202
|
-
onSwitch: (accountId) => {
|
|
203
|
-
void handleSwitch(accountId);
|
|
204
|
-
},
|
|
205
|
-
onAdd: () => controller?.add(),
|
|
206
|
-
onManage: handleManage,
|
|
207
|
-
onClose: closeAccountDialog,
|
|
208
|
-
}),
|
|
209
|
-
[handleSwitch, controller, handleManage, closeAccountDialog],
|
|
210
|
-
);
|
|
211
|
-
|
|
212
69
|
if (!controller) {
|
|
213
70
|
return null;
|
|
214
71
|
}
|
|
215
72
|
|
|
216
73
|
const { view } = snapshot;
|
|
217
|
-
const showBack =
|
|
74
|
+
const showBack =
|
|
75
|
+
view === 'qr' || view === 'signup' || (view === 'add' && snapshot.accounts.length > 0);
|
|
218
76
|
|
|
219
77
|
return (
|
|
220
78
|
<Dialog
|
|
@@ -233,29 +91,8 @@ const OxyAccountDialog: React.FC = () => {
|
|
|
233
91
|
onBack={() => controller.setView('accounts')}
|
|
234
92
|
onClose={closeAccountDialog}
|
|
235
93
|
/>
|
|
236
|
-
<ScrollView
|
|
237
|
-
|
|
238
|
-
contentContainerStyle={styles.bodyContent}
|
|
239
|
-
showsVerticalScrollIndicator={false}
|
|
240
|
-
>
|
|
241
|
-
{view === 'accounts' ? (
|
|
242
|
-
<AccountsView snapshot={snapshot} theme={theme} t={t} handlers={handlers} />
|
|
243
|
-
) : view === 'qr' ? (
|
|
244
|
-
<QrView snapshot={snapshot} theme={theme} t={t} onRetry={() => void controller.showQr()} />
|
|
245
|
-
) : (
|
|
246
|
-
<SignInView
|
|
247
|
-
snapshot={snapshot}
|
|
248
|
-
theme={theme}
|
|
249
|
-
t={t}
|
|
250
|
-
handlers={handlers}
|
|
251
|
-
onSignInWithOxy={() => void controller.signInWithOxy()}
|
|
252
|
-
onScanQr={() => void controller.showQr()}
|
|
253
|
-
onUsePassword={() => void controller.openPasswordAtOxyAuth()}
|
|
254
|
-
onSignInWithPasskey={passkeyAvailable ? () => void handleSignInWithPasskey() : undefined}
|
|
255
|
-
passkeyPending={passkeyPending}
|
|
256
|
-
passkeyError={passkeyError}
|
|
257
|
-
/>
|
|
258
|
-
)}
|
|
94
|
+
<ScrollView style={styles.body} contentContainerStyle={styles.bodyContent} showsVerticalScrollIndicator={false}>
|
|
95
|
+
<OxyAuthChooser onComplete={closeAccountDialog} />
|
|
259
96
|
</ScrollView>
|
|
260
97
|
</Dialog>
|
|
261
98
|
);
|
|
@@ -265,9 +102,6 @@ const OxyAccountDialog: React.FC = () => {
|
|
|
265
102
|
// Header
|
|
266
103
|
// ---------------------------------------------------------------------------
|
|
267
104
|
|
|
268
|
-
type Theme = ReturnType<typeof useTheme>;
|
|
269
|
-
type Translate = ReturnType<typeof useI18n>['t'];
|
|
270
|
-
|
|
271
105
|
interface HeaderProps {
|
|
272
106
|
snapshot: AccountDialogSnapshot;
|
|
273
107
|
theme: Theme;
|
|
@@ -316,267 +150,6 @@ const DialogHeader: React.FC<HeaderProps> = ({ snapshot, theme, t, showBack, onB
|
|
|
316
150
|
);
|
|
317
151
|
};
|
|
318
152
|
|
|
319
|
-
// ---------------------------------------------------------------------------
|
|
320
|
-
// Accounts view
|
|
321
|
-
// ---------------------------------------------------------------------------
|
|
322
|
-
|
|
323
|
-
interface AccountsViewProps {
|
|
324
|
-
snapshot: AccountDialogSnapshot;
|
|
325
|
-
theme: Theme;
|
|
326
|
-
t: Translate;
|
|
327
|
-
handlers: OxyAccountDialogHandlers;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const AccountsView: React.FC<AccountsViewProps> = ({ snapshot, theme, t, handlers }) => {
|
|
331
|
-
if (snapshot.loading && snapshot.accounts.length === 0) {
|
|
332
|
-
return (
|
|
333
|
-
<View style={styles.centeredBlock}>
|
|
334
|
-
<MaterialCommunityIcons name="loading" size={24} color={theme.colors.textSecondary} />
|
|
335
|
-
<Text style={[styles.mutedText, { color: theme.colors.textSecondary }]}>
|
|
336
|
-
{t('accountSwitcher.loading') || 'Loading accounts…'}
|
|
337
|
-
</Text>
|
|
338
|
-
</View>
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return (
|
|
343
|
-
<View style={styles.rows}>
|
|
344
|
-
{snapshot.accounts.map((account) => (
|
|
345
|
-
<AccountRow
|
|
346
|
-
key={account.accountId}
|
|
347
|
-
account={account}
|
|
348
|
-
theme={theme}
|
|
349
|
-
switching={snapshot.switchingAccountId === account.accountId}
|
|
350
|
-
disabled={snapshot.switchingAccountId !== null}
|
|
351
|
-
onPress={() => handlers.onSwitch(account.accountId)}
|
|
352
|
-
/>
|
|
353
|
-
))}
|
|
354
|
-
|
|
355
|
-
<Pressable
|
|
356
|
-
style={[styles.addRow, { borderColor: theme.colors.border }]}
|
|
357
|
-
onPress={handlers.onAdd}
|
|
358
|
-
accessibilityRole="button"
|
|
359
|
-
accessibilityLabel={t('signin.addAccountTitle') || 'Add another account'}
|
|
360
|
-
>
|
|
361
|
-
<View style={[styles.addBadge, { borderColor: theme.colors.border }]}>
|
|
362
|
-
<MaterialCommunityIcons name="plus" size={20} color={theme.colors.textSecondary} />
|
|
363
|
-
</View>
|
|
364
|
-
<Text style={[styles.rowName, { color: theme.colors.textSecondary }]}>
|
|
365
|
-
{t('signin.addAccountTitle') || 'Add another account'}
|
|
366
|
-
</Text>
|
|
367
|
-
</Pressable>
|
|
368
|
-
|
|
369
|
-
<View style={styles.footerLinks}>
|
|
370
|
-
<Pressable onPress={handlers.onManage} accessibilityRole="button">
|
|
371
|
-
<Text style={[styles.linkText, { color: theme.colors.primary }]}>
|
|
372
|
-
{t('accountMenu.manage') || 'Manage accounts'}
|
|
373
|
-
</Text>
|
|
374
|
-
</Pressable>
|
|
375
|
-
</View>
|
|
376
|
-
</View>
|
|
377
|
-
);
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
interface AccountRowProps {
|
|
381
|
-
account: SwitchableAccount;
|
|
382
|
-
theme: Theme;
|
|
383
|
-
switching: boolean;
|
|
384
|
-
disabled: boolean;
|
|
385
|
-
onPress: () => void;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const AccountRow: React.FC<AccountRowProps> = ({ account, theme, switching, disabled, onPress }) => {
|
|
389
|
-
const accent = resolveAccentHex(account.color, theme.colors.primary);
|
|
390
|
-
const rowStyle: StyleProp<ViewStyle> = [
|
|
391
|
-
styles.accountRow,
|
|
392
|
-
{
|
|
393
|
-
borderColor: account.isCurrent ? accent : theme.colors.border,
|
|
394
|
-
backgroundColor: theme.colors.card,
|
|
395
|
-
},
|
|
396
|
-
disabled && !switching ? styles.rowDisabled : null,
|
|
397
|
-
];
|
|
398
|
-
|
|
399
|
-
return (
|
|
400
|
-
<BloomColorScope colorPreset={toPreset(account.color)} asChild>
|
|
401
|
-
<Pressable
|
|
402
|
-
style={rowStyle}
|
|
403
|
-
onPress={onPress}
|
|
404
|
-
disabled={disabled}
|
|
405
|
-
accessibilityRole="button"
|
|
406
|
-
accessibilityState={{ selected: account.isCurrent, disabled }}
|
|
407
|
-
accessibilityLabel={account.displayName}
|
|
408
|
-
>
|
|
409
|
-
<View style={[styles.avatarRing, { borderColor: account.isCurrent ? accent : 'transparent' }]}>
|
|
410
|
-
<Avatar
|
|
411
|
-
source={account.avatarUrl ?? undefined}
|
|
412
|
-
variant="thumb"
|
|
413
|
-
name={account.displayName}
|
|
414
|
-
size={ROW_AVATAR_SIZE}
|
|
415
|
-
/>
|
|
416
|
-
</View>
|
|
417
|
-
<View style={styles.rowMeta}>
|
|
418
|
-
<Text style={[styles.rowName, { color: theme.colors.text }]} numberOfLines={1}>
|
|
419
|
-
{account.displayName}
|
|
420
|
-
</Text>
|
|
421
|
-
{account.email ? (
|
|
422
|
-
<Text style={[styles.rowHandle, { color: theme.colors.textSecondary }]} numberOfLines={1}>
|
|
423
|
-
{account.email}
|
|
424
|
-
</Text>
|
|
425
|
-
) : null}
|
|
426
|
-
</View>
|
|
427
|
-
{switching ? (
|
|
428
|
-
<MaterialCommunityIcons name="loading" size={20} color={accent} />
|
|
429
|
-
) : account.isCurrent ? (
|
|
430
|
-
<MaterialCommunityIcons name="check-circle" size={20} color={accent} />
|
|
431
|
-
) : (
|
|
432
|
-
<MaterialCommunityIcons name="chevron-right" size={20} color={theme.colors.textSecondary} />
|
|
433
|
-
)}
|
|
434
|
-
</Pressable>
|
|
435
|
-
</BloomColorScope>
|
|
436
|
-
);
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
// ---------------------------------------------------------------------------
|
|
440
|
-
// Sign-in view (add / signin)
|
|
441
|
-
// ---------------------------------------------------------------------------
|
|
442
|
-
|
|
443
|
-
interface SignInViewProps {
|
|
444
|
-
snapshot: AccountDialogSnapshot;
|
|
445
|
-
theme: Theme;
|
|
446
|
-
t: Translate;
|
|
447
|
-
handlers: OxyAccountDialogHandlers;
|
|
448
|
-
onSignInWithOxy: () => void;
|
|
449
|
-
onScanQr: () => void;
|
|
450
|
-
onUsePassword: () => void;
|
|
451
|
-
/** When present, offer a "Sign in with a passkey" button (first-party Oxy web only). */
|
|
452
|
-
onSignInWithPasskey?: () => void;
|
|
453
|
-
passkeyPending: boolean;
|
|
454
|
-
passkeyError: string | null;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const SignInView: React.FC<SignInViewProps> = ({
|
|
458
|
-
snapshot,
|
|
459
|
-
theme,
|
|
460
|
-
t,
|
|
461
|
-
handlers,
|
|
462
|
-
onSignInWithOxy,
|
|
463
|
-
onScanQr,
|
|
464
|
-
onUsePassword,
|
|
465
|
-
onSignInWithPasskey,
|
|
466
|
-
passkeyPending,
|
|
467
|
-
passkeyError,
|
|
468
|
-
}) => (
|
|
469
|
-
<View style={styles.signInBlock}>
|
|
470
|
-
{snapshot.accounts.length > 0 ? (
|
|
471
|
-
<View style={styles.rows}>
|
|
472
|
-
{snapshot.accounts.map((account) => (
|
|
473
|
-
<AccountRow
|
|
474
|
-
key={account.accountId}
|
|
475
|
-
account={account}
|
|
476
|
-
theme={theme}
|
|
477
|
-
switching={snapshot.switchingAccountId === account.accountId}
|
|
478
|
-
disabled={snapshot.switchingAccountId !== null}
|
|
479
|
-
onPress={() => handlers.onSwitch(account.accountId)}
|
|
480
|
-
/>
|
|
481
|
-
))}
|
|
482
|
-
<Dividerish theme={theme} label={t('signin.or') || 'or'} />
|
|
483
|
-
</View>
|
|
484
|
-
) : null}
|
|
485
|
-
|
|
486
|
-
<Button variant="primary" onPress={onSignInWithOxy} style={styles.primaryButton}>
|
|
487
|
-
Sign in with Oxy
|
|
488
|
-
</Button>
|
|
489
|
-
|
|
490
|
-
{onSignInWithPasskey ? (
|
|
491
|
-
<Button
|
|
492
|
-
variant="secondary"
|
|
493
|
-
onPress={onSignInWithPasskey}
|
|
494
|
-
disabled={passkeyPending}
|
|
495
|
-
style={styles.secondaryButton}
|
|
496
|
-
testID="passkey-signin-button"
|
|
497
|
-
>
|
|
498
|
-
{passkeyPending
|
|
499
|
-
? t('accountSwitcher.passkeySigningIn') || 'Signing in…'
|
|
500
|
-
: t('accountSwitcher.signInWithPasskey') || 'Sign in with a passkey'}
|
|
501
|
-
</Button>
|
|
502
|
-
) : null}
|
|
503
|
-
|
|
504
|
-
{passkeyError ? (
|
|
505
|
-
<Text style={[styles.errorText, { color: theme.colors.error }]}>{passkeyError}</Text>
|
|
506
|
-
) : null}
|
|
507
|
-
|
|
508
|
-
<Button variant="secondary" onPress={onScanQr} style={styles.secondaryButton}>
|
|
509
|
-
{t('accountSwitcher.scanQr') || 'Scan a QR from another device'}
|
|
510
|
-
</Button>
|
|
511
|
-
|
|
512
|
-
<Pressable onPress={onUsePassword} accessibilityRole="button" style={styles.passwordLink}>
|
|
513
|
-
<Text style={[styles.linkText, { color: theme.colors.textSecondary }]}>
|
|
514
|
-
Use a password instead
|
|
515
|
-
</Text>
|
|
516
|
-
</Pressable>
|
|
517
|
-
</View>
|
|
518
|
-
);
|
|
519
|
-
|
|
520
|
-
const Dividerish: React.FC<{ theme: Theme; label: string }> = ({ theme, label }) => (
|
|
521
|
-
<View style={styles.dividerRow}>
|
|
522
|
-
<View style={[styles.dividerLine, { backgroundColor: theme.colors.border }]} />
|
|
523
|
-
<Text style={[styles.dividerText, { color: theme.colors.textSecondary }]}>{label}</Text>
|
|
524
|
-
<View style={[styles.dividerLine, { backgroundColor: theme.colors.border }]} />
|
|
525
|
-
</View>
|
|
526
|
-
);
|
|
527
|
-
|
|
528
|
-
// ---------------------------------------------------------------------------
|
|
529
|
-
// QR view
|
|
530
|
-
// ---------------------------------------------------------------------------
|
|
531
|
-
|
|
532
|
-
interface QrViewProps {
|
|
533
|
-
snapshot: AccountDialogSnapshot;
|
|
534
|
-
theme: Theme;
|
|
535
|
-
t: Translate;
|
|
536
|
-
onRetry: () => void;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
const QrView: React.FC<QrViewProps> = ({ snapshot, theme, t, onRetry }) => {
|
|
540
|
-
const { signIn } = snapshot;
|
|
541
|
-
|
|
542
|
-
if (signIn.phase === 'error') {
|
|
543
|
-
return (
|
|
544
|
-
<View style={styles.centeredBlock}>
|
|
545
|
-
<Text style={[styles.errorText, { color: theme.colors.error }]}>
|
|
546
|
-
{signIn.error || 'Sign-in failed. Please try again.'}
|
|
547
|
-
</Text>
|
|
548
|
-
<Button variant="secondary" onPress={onRetry} style={styles.secondaryButton}>
|
|
549
|
-
{t('common.actions.tryAgain') || 'Try again'}
|
|
550
|
-
</Button>
|
|
551
|
-
</View>
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
if (signIn.phase === 'starting' || signIn.phase === 'authorized' || !signIn.qrPayload) {
|
|
556
|
-
const label =
|
|
557
|
-
signIn.phase === 'authorized'
|
|
558
|
-
? t('signin.status.signingIn') || 'Signing in…'
|
|
559
|
-
: t('accountSwitcher.loading') || 'Preparing sign-in…';
|
|
560
|
-
return (
|
|
561
|
-
<View style={styles.centeredBlock}>
|
|
562
|
-
<MaterialCommunityIcons name="loading" size={26} color={theme.colors.primary} />
|
|
563
|
-
<Text style={[styles.mutedText, { color: theme.colors.textSecondary }]}>{label}</Text>
|
|
564
|
-
</View>
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
return (
|
|
569
|
-
<View style={styles.centeredBlock}>
|
|
570
|
-
<View style={styles.qrPlate}>
|
|
571
|
-
<QRCode value={signIn.qrPayload} size={QR_SIZE} backgroundColor={QR_PLATE_BG} color={QR_FOREGROUND} />
|
|
572
|
-
</View>
|
|
573
|
-
<Text style={[styles.mutedText, { color: theme.colors.textSecondary }]}>
|
|
574
|
-
{t('accountSwitcher.scanWithOxy') || 'Scan with any Oxy app and approve.'}
|
|
575
|
-
</Text>
|
|
576
|
-
</View>
|
|
577
|
-
);
|
|
578
|
-
};
|
|
579
|
-
|
|
580
153
|
// ---------------------------------------------------------------------------
|
|
581
154
|
// Copy + helpers
|
|
582
155
|
// ---------------------------------------------------------------------------
|
|
@@ -594,8 +167,13 @@ function headerCopy(
|
|
|
594
167
|
};
|
|
595
168
|
case 'qr':
|
|
596
169
|
return {
|
|
597
|
-
title: t('accountSwitcher.scanTitle') || '
|
|
598
|
-
subtitle: t('accountSwitcher.scanSubtitle') || '
|
|
170
|
+
title: t('accountSwitcher.scanTitle') || 'Sign in with Oxy',
|
|
171
|
+
subtitle: t('accountSwitcher.scanSubtitle') || 'Scan with Commons on your phone.',
|
|
172
|
+
};
|
|
173
|
+
case 'signup':
|
|
174
|
+
return {
|
|
175
|
+
title: t('signup.title') || 'Create your account',
|
|
176
|
+
subtitle: t('signup.subtitle') || 'One identity for the whole ecosystem.',
|
|
599
177
|
};
|
|
600
178
|
default:
|
|
601
179
|
return accountCount > 0
|
|
@@ -610,12 +188,6 @@ function headerCopy(
|
|
|
610
188
|
}
|
|
611
189
|
}
|
|
612
190
|
|
|
613
|
-
/** Current document URL on web; `undefined` where `location` is absent. */
|
|
614
|
-
function currentHref(): string | undefined {
|
|
615
|
-
const location = (globalThis as { location?: { href?: string } }).location;
|
|
616
|
-
return typeof location?.href === 'string' ? location.href : undefined;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
191
|
const EMPTY_SNAPSHOT: AccountDialogSnapshot = {
|
|
620
192
|
view: 'accounts',
|
|
621
193
|
accounts: [],
|
|
@@ -624,6 +196,7 @@ const EMPTY_SNAPSHOT: AccountDialogSnapshot = {
|
|
|
624
196
|
error: null,
|
|
625
197
|
switchingAccountId: null,
|
|
626
198
|
signIn: { phase: 'idle', authorizeCode: null, qrPayload: null, expiresAt: null, error: null },
|
|
199
|
+
commonsAvailability: 'unknown',
|
|
627
200
|
};
|
|
628
201
|
|
|
629
202
|
const styles = StyleSheet.create({
|
|
@@ -665,117 +238,6 @@ const styles = StyleSheet.create({
|
|
|
665
238
|
paddingTop: 4,
|
|
666
239
|
paddingBottom: 4,
|
|
667
240
|
},
|
|
668
|
-
rows: {
|
|
669
|
-
width: '100%',
|
|
670
|
-
gap: 8,
|
|
671
|
-
},
|
|
672
|
-
accountRow: {
|
|
673
|
-
flexDirection: 'row',
|
|
674
|
-
alignItems: 'center',
|
|
675
|
-
gap: 12,
|
|
676
|
-
borderWidth: StyleSheet.hairlineWidth,
|
|
677
|
-
borderRadius: 16,
|
|
678
|
-
paddingVertical: 10,
|
|
679
|
-
paddingHorizontal: 12,
|
|
680
|
-
},
|
|
681
|
-
rowDisabled: {
|
|
682
|
-
opacity: 0.6,
|
|
683
|
-
},
|
|
684
|
-
avatarRing: {
|
|
685
|
-
borderRadius: 9999,
|
|
686
|
-
borderWidth: 2,
|
|
687
|
-
padding: 1,
|
|
688
|
-
},
|
|
689
|
-
rowMeta: {
|
|
690
|
-
flex: 1,
|
|
691
|
-
minWidth: 0,
|
|
692
|
-
},
|
|
693
|
-
rowName: {
|
|
694
|
-
fontSize: 15,
|
|
695
|
-
fontWeight: '600',
|
|
696
|
-
},
|
|
697
|
-
rowHandle: {
|
|
698
|
-
fontSize: 12.5,
|
|
699
|
-
marginTop: 1,
|
|
700
|
-
},
|
|
701
|
-
addRow: {
|
|
702
|
-
flexDirection: 'row',
|
|
703
|
-
alignItems: 'center',
|
|
704
|
-
gap: 12,
|
|
705
|
-
borderWidth: StyleSheet.hairlineWidth,
|
|
706
|
-
borderStyle: 'dashed',
|
|
707
|
-
borderRadius: 16,
|
|
708
|
-
paddingVertical: 10,
|
|
709
|
-
paddingHorizontal: 12,
|
|
710
|
-
marginTop: 8,
|
|
711
|
-
},
|
|
712
|
-
addBadge: {
|
|
713
|
-
width: ROW_AVATAR_SIZE,
|
|
714
|
-
height: ROW_AVATAR_SIZE,
|
|
715
|
-
borderRadius: 9999,
|
|
716
|
-
borderWidth: StyleSheet.hairlineWidth,
|
|
717
|
-
alignItems: 'center',
|
|
718
|
-
justifyContent: 'center',
|
|
719
|
-
},
|
|
720
|
-
footerLinks: {
|
|
721
|
-
alignItems: 'center',
|
|
722
|
-
marginTop: 16,
|
|
723
|
-
},
|
|
724
|
-
linkText: {
|
|
725
|
-
fontSize: 14,
|
|
726
|
-
fontWeight: '600',
|
|
727
|
-
},
|
|
728
|
-
signInBlock: {
|
|
729
|
-
width: '100%',
|
|
730
|
-
},
|
|
731
|
-
primaryButton: {
|
|
732
|
-
width: '100%',
|
|
733
|
-
borderRadius: 14,
|
|
734
|
-
marginTop: 8,
|
|
735
|
-
},
|
|
736
|
-
secondaryButton: {
|
|
737
|
-
width: '100%',
|
|
738
|
-
borderRadius: 14,
|
|
739
|
-
marginTop: 10,
|
|
740
|
-
},
|
|
741
|
-
passwordLink: {
|
|
742
|
-
alignSelf: 'center',
|
|
743
|
-
paddingVertical: 10,
|
|
744
|
-
marginTop: 12,
|
|
745
|
-
},
|
|
746
|
-
dividerRow: {
|
|
747
|
-
flexDirection: 'row',
|
|
748
|
-
alignItems: 'center',
|
|
749
|
-
width: '100%',
|
|
750
|
-
marginVertical: 14,
|
|
751
|
-
},
|
|
752
|
-
dividerLine: {
|
|
753
|
-
flex: 1,
|
|
754
|
-
height: StyleSheet.hairlineWidth,
|
|
755
|
-
},
|
|
756
|
-
dividerText: {
|
|
757
|
-
marginHorizontal: 12,
|
|
758
|
-
fontSize: 13,
|
|
759
|
-
},
|
|
760
|
-
centeredBlock: {
|
|
761
|
-
alignItems: 'center',
|
|
762
|
-
justifyContent: 'center',
|
|
763
|
-
gap: 12,
|
|
764
|
-
paddingVertical: 24,
|
|
765
|
-
},
|
|
766
|
-
mutedText: {
|
|
767
|
-
fontSize: 14,
|
|
768
|
-
textAlign: 'center',
|
|
769
|
-
},
|
|
770
|
-
errorText: {
|
|
771
|
-
fontSize: 14,
|
|
772
|
-
textAlign: 'center',
|
|
773
|
-
},
|
|
774
|
-
qrPlate: {
|
|
775
|
-
padding: 16,
|
|
776
|
-
borderRadius: 16,
|
|
777
|
-
backgroundColor: QR_PLATE_BG,
|
|
778
|
-
},
|
|
779
241
|
});
|
|
780
242
|
|
|
781
243
|
export default OxyAccountDialog;
|