@oxyhq/services 22.0.0 → 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/components/OxyAccountDialog.js +27 -529
- 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/oxyAuthChooserStyles.js +158 -0
- package/lib/commonjs/ui/components/oxyAuthChooserStyles.js.map +1 -0
- package/lib/commonjs/ui/context/OxyContext.js +28 -3
- package/lib/commonjs/ui/context/OxyContext.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/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/OxyAccountDialog.js +29 -533
- 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/oxyAuthChooserStyles.js +155 -0
- package/lib/module/ui/components/oxyAuthChooserStyles.js.map +1 -0
- package/lib/module/ui/context/OxyContext.js +28 -3
- package/lib/module/ui/context/OxyContext.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/typescript/commonjs/index.d.ts +2 -0
- package/lib/typescript/commonjs/index.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/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.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/utils/commonsStoreLinks.d.ts +24 -0
- package/lib/typescript/commonjs/ui/utils/commonsStoreLinks.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +2 -0
- package/lib/typescript/module/index.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/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.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/utils/commonsStoreLinks.d.ts +24 -0
- package/lib/typescript/module/ui/utils/commonsStoreLinks.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/index.ts +7 -0
- package/src/ui/components/OxyAccountDialog.tsx +33 -557
- package/src/ui/components/OxyAuthChooser.tsx +883 -0
- package/src/ui/components/oxyAuthChooserStyles.ts +152 -0
- package/src/ui/context/OxyContext.tsx +31 -3
- package/src/ui/context/useOxyAccountGraph.ts +4 -2
- package/src/ui/hooks/useSwitchableAccounts.ts +1 -0
- package/src/ui/utils/commonsStoreLinks.ts +38 -0
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
var _reactNative = require("react-native");
|
|
9
|
+
var _vectorIcons = require("@expo/vector-icons");
|
|
10
|
+
var _reactNativeQrcodeSvg = _interopRequireDefault(require("react-native-qrcode-svg"));
|
|
11
|
+
var _avatar = require("@oxyhq/bloom/avatar");
|
|
12
|
+
var _button = require("@oxyhq/bloom/button");
|
|
13
|
+
var _typography = require("@oxyhq/bloom/typography");
|
|
14
|
+
var _theme = require("@oxyhq/bloom/theme");
|
|
15
|
+
var _core = require("@oxyhq/core");
|
|
16
|
+
var _reactQuery = require("@tanstack/react-query");
|
|
17
|
+
var _OxyContext = require("../context/OxyContext.js");
|
|
18
|
+
var _useI18n = require("../hooks/useI18n.js");
|
|
19
|
+
var _isWebBrowser = require("../utils/isWebBrowser.js");
|
|
20
|
+
var _commonsStoreLinks = require("../utils/commonsStoreLinks.js");
|
|
21
|
+
var _oxyAuthChooserStyles = require("./oxyAuthChooserStyles.js");
|
|
22
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
23
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
24
|
+
/**
|
|
25
|
+
* OxyAuthChooser — the account switcher + sign-in/sign-up surface, WITHOUT
|
|
26
|
+
* any dialog chrome.
|
|
27
|
+
*
|
|
28
|
+
* A thin React Native binding over the headless `AccountDialogController` in
|
|
29
|
+
* `@oxyhq/core` (bound via `useSyncExternalStore`) — the same data/state
|
|
30
|
+
* machine {@link OxyAccountDialog} renders, extracted so it can be mounted in
|
|
31
|
+
* TWO places: wrapped in Bloom's `<Dialog>` by `OxyAccountDialog` (the normal
|
|
32
|
+
* in-app surface), and mounted bare by a future auth.oxy.so hub page for the
|
|
33
|
+
* cross-origin passkey popup (b2) — same chooser, two hosts, two completion
|
|
34
|
+
* strategies via the `onComplete` prop. Neither host duplicates view logic.
|
|
35
|
+
*
|
|
36
|
+
* Views (from `snapshot.view`):
|
|
37
|
+
* - `accounts` — the unified `SwitchableAccount[]` list. Tapping a row
|
|
38
|
+
* switches through `controller.switchTo(accountId)`, the active row is
|
|
39
|
+
* flagged, and a "+ Add account" row opens the sign-in entry.
|
|
40
|
+
* - `add` / `signin` — the sign-in entry. On WEB this auto-starts "Sign in
|
|
41
|
+
* with Oxy" (native shared-keychain silently, else the QR handoff) the
|
|
42
|
+
* instant the view is entered — the QR is the PRIMARY web surface, not a
|
|
43
|
+
* button behind a click. On NATIVE it stays button-driven (unchanged):
|
|
44
|
+
* primary "Sign in with Oxy", secondary "Scan a QR from another device".
|
|
45
|
+
* Both platforms get a footer entry into `signup`.
|
|
46
|
+
* - `signup` — account creation. Commons is ALWAYS the priority path (owner
|
|
47
|
+
* mandate) and leads everywhere it appears: native shows only "Create your
|
|
48
|
+
* identity in Commons" (or "Get Commons" first, if not installed); web on
|
|
49
|
+
* a first-party Oxy origin leads with the SAME "Get Commons" CTA, with
|
|
50
|
+
* inline passkey creation (username + `registerWithPasskey`) offered
|
|
51
|
+
* UNDERNEATH as the de-emphasized "don't want to install anything"
|
|
52
|
+
* alternative — never co-equal, never a competing button. Anywhere else (a
|
|
53
|
+
* non-Oxy web origin, before the b2 hub-relay ships) shows an honest "not
|
|
54
|
+
* available here yet" message instead of a broken button.
|
|
55
|
+
* - `qr` — the cross-device Commons handoff, and the PRIMARY web sign-in
|
|
56
|
+
* surface. On a first-party Oxy origin this ALSO carries a "No Commons?"
|
|
57
|
+
* passkey link — identity is ONE thing (Commons key or browser passkey,
|
|
58
|
+
* same Oxy ID), so there is no separate "sign in with a passkey" button
|
|
59
|
+
* anywhere in this file, but passkey is explicitly the LIGHTWEIGHT
|
|
60
|
+
* ALTERNATIVE for someone who doesn't want to install Commons, never
|
|
61
|
+
* co-equal: same small-text-link subordination, same framing, wherever it
|
|
62
|
+
* appears. Native additionally branches on `commonsAvailability`: when
|
|
63
|
+
* Commons isn't installed, this leads with a "Get Commons" CTA instead of
|
|
64
|
+
* a same-device dead-end QR.
|
|
65
|
+
*
|
|
66
|
+
* Per-account color re-theming uses Bloom's `APP_COLOR_PRESETS` + `BloomColorScope`
|
|
67
|
+
* (same visual language auth.oxy.so uses). Base theming is `useTheme()` + a
|
|
68
|
+
* `StyleSheet`, so this renders correctly in EVERY consumer — including apps
|
|
69
|
+
* that do not use NativeWind (e.g. the accounts app).
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/** Diameter of a row avatar. */const ROW_AVATAR_SIZE = 40;
|
|
73
|
+
/** High-contrast QR colors — intentionally fixed (NOT themed) for scan reliability. */
|
|
74
|
+
const QR_PLATE_BG = '#FFFFFF';
|
|
75
|
+
const QR_FOREGROUND = '#000000';
|
|
76
|
+
const QR_SIZE = 196;
|
|
77
|
+
/** Commons' own identity-creation deep link (mirrors the `approve`/`attest`/`card` intents). */
|
|
78
|
+
const COMMONS_CREATE_IDENTITY_URL = 'oxycommons://create-identity';
|
|
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, fallback) {
|
|
86
|
+
const preset = toPreset(color);
|
|
87
|
+
return preset ? _theme.APP_COLOR_PRESETS[preset].hex : fallback;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Narrow a stored color string to a known `AppColorName`, or `undefined`. */
|
|
91
|
+
function toPreset(color) {
|
|
92
|
+
if (!color) return undefined;
|
|
93
|
+
return _theme.APP_COLOR_NAMES.includes(color) ? color : undefined;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The account switcher + sign-in/sign-up chooser. Mounted by `OxyAccountDialog`
|
|
97
|
+
* (wrapped in Bloom's `<Dialog>`) today; mountable bare by any future host that
|
|
98
|
+
* supplies its own `onComplete`.
|
|
99
|
+
*/
|
|
100
|
+
const OxyAuthChooser = ({
|
|
101
|
+
onComplete
|
|
102
|
+
}) => {
|
|
103
|
+
const {
|
|
104
|
+
accountDialogController: controller,
|
|
105
|
+
showBottomSheet,
|
|
106
|
+
refreshAccounts,
|
|
107
|
+
signInWithPasskey,
|
|
108
|
+
registerWithPasskey,
|
|
109
|
+
oxyServices
|
|
110
|
+
} = (0, _OxyContext.useOxy)();
|
|
111
|
+
const theme = (0, _theme.useTheme)();
|
|
112
|
+
const {
|
|
113
|
+
t
|
|
114
|
+
} = (0, _useI18n.useI18n)();
|
|
115
|
+
const queryClient = (0, _reactQuery.useQueryClient)();
|
|
116
|
+
|
|
117
|
+
// A credential minted for `oxy.so` can only be asserted there (or a loopback
|
|
118
|
+
// dev host) — this is a hard WebAuthn RP-ID boundary, not feature detection,
|
|
119
|
+
// so it is the ONLY gate on the passkey affordances. `isOxyRpOrigin()` is
|
|
120
|
+
// `false` on native by construction (no `location`), so no separate native
|
|
121
|
+
// check is needed.
|
|
122
|
+
const passkeyAvailable = (0, _react.useMemo)(() => (0, _core.isOxyRpOrigin)(), []);
|
|
123
|
+
const [signInPasskeyPending, setSignInPasskeyPending] = (0, _react.useState)(false);
|
|
124
|
+
const [signInPasskeyError, setSignInPasskeyError] = (0, _react.useState)(null);
|
|
125
|
+
const handleSignInWithPasskey = (0, _react.useCallback)(async () => {
|
|
126
|
+
if (signInPasskeyPending) return;
|
|
127
|
+
setSignInPasskeyPending(true);
|
|
128
|
+
setSignInPasskeyError(null);
|
|
129
|
+
try {
|
|
130
|
+
await signInWithPasskey();
|
|
131
|
+
onComplete?.();
|
|
132
|
+
} catch (error) {
|
|
133
|
+
// A cancelled/failed ceremony keeps the view open so the user can retry
|
|
134
|
+
// or fall back to the QR — surface a concise message, never swallow.
|
|
135
|
+
setSignInPasskeyError(error instanceof Error && error.message ? error.message : 'Passkey sign-in failed.');
|
|
136
|
+
} finally {
|
|
137
|
+
setSignInPasskeyPending(false);
|
|
138
|
+
}
|
|
139
|
+
}, [signInPasskeyPending, signInWithPasskey, onComplete]);
|
|
140
|
+
const [createPasskeyPending, setCreatePasskeyPending] = (0, _react.useState)(false);
|
|
141
|
+
const [createPasskeyError, setCreatePasskeyError] = (0, _react.useState)(null);
|
|
142
|
+
const handleCreateWithPasskey = (0, _react.useCallback)(async username => {
|
|
143
|
+
if (createPasskeyPending) return;
|
|
144
|
+
setCreatePasskeyPending(true);
|
|
145
|
+
setCreatePasskeyError(null);
|
|
146
|
+
try {
|
|
147
|
+
await registerWithPasskey({
|
|
148
|
+
username
|
|
149
|
+
});
|
|
150
|
+
onComplete?.();
|
|
151
|
+
} catch (error) {
|
|
152
|
+
setCreatePasskeyError(error instanceof Error && error.message ? error.message : 'Account creation failed.');
|
|
153
|
+
} finally {
|
|
154
|
+
setCreatePasskeyPending(false);
|
|
155
|
+
}
|
|
156
|
+
}, [createPasskeyPending, registerWithPasskey, onComplete]);
|
|
157
|
+
|
|
158
|
+
// Bind the headless controller. `getSnapshot` returns a stable reference
|
|
159
|
+
// between changes, so it is `useSyncExternalStore`-safe. Guard the no-provider
|
|
160
|
+
// loading state (`controller` is `null`) with an inert store.
|
|
161
|
+
const subscribe = (0, _react.useCallback)(listener => controller ? controller.subscribe(listener) : () => undefined, [controller]);
|
|
162
|
+
const getSnapshot = (0, _react.useCallback)(() => controller ? controller.getSnapshot() : EMPTY_SNAPSHOT, [controller]);
|
|
163
|
+
const snapshot = (0, _react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
164
|
+
const {
|
|
165
|
+
view
|
|
166
|
+
} = snapshot;
|
|
167
|
+
|
|
168
|
+
// Web: "Sign in with Oxy" auto-starts the instant the sign-in entry is
|
|
169
|
+
// reached — no button tap needed. `signInWithOxy()` tries the shared
|
|
170
|
+
// keychain first (silently, native-only) then falls to the QR handoff,
|
|
171
|
+
// which is what actually renders: the QR is the primary web surface, not a
|
|
172
|
+
// peer button among several. Gated on `phase === 'idle'` so navigating back
|
|
173
|
+
// to this view while a flow is already active (or re-entering it) never
|
|
174
|
+
// restarts/duplicates it. Native stays fully button-driven (unchanged) —
|
|
175
|
+
// this effect is a no-op there.
|
|
176
|
+
(0, _react.useEffect)(() => {
|
|
177
|
+
if (!controller || !(0, _isWebBrowser.isWebBrowser)()) return;
|
|
178
|
+
if (view !== 'signin' && view !== 'add' || snapshot.signIn.phase !== 'idle') return;
|
|
179
|
+
void controller.signInWithOxy();
|
|
180
|
+
}, [controller, view, snapshot.signIn.phase]);
|
|
181
|
+
const [switching, setSwitching] = (0, _react.useState)(false);
|
|
182
|
+
const handleSwitch = (0, _react.useCallback)(async accountId => {
|
|
183
|
+
if (!controller || switching) return;
|
|
184
|
+
if (accountId === snapshot.activeAccountId) {
|
|
185
|
+
onComplete?.();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
setSwitching(true);
|
|
189
|
+
try {
|
|
190
|
+
await controller.switchTo(accountId);
|
|
191
|
+
// A switch succeeds unless the controller recorded an error. On success
|
|
192
|
+
// reload the app's account graph and drop cached account-scoped data so
|
|
193
|
+
// the new active identity re-fetches — the same side effects the
|
|
194
|
+
// context's own `switchToAccount` performs.
|
|
195
|
+
if (!controller.getSnapshot().error) {
|
|
196
|
+
void refreshAccounts();
|
|
197
|
+
queryClient.invalidateQueries();
|
|
198
|
+
onComplete?.();
|
|
199
|
+
}
|
|
200
|
+
} finally {
|
|
201
|
+
setSwitching(false);
|
|
202
|
+
}
|
|
203
|
+
}, [controller, switching, snapshot.activeAccountId, onComplete, refreshAccounts, queryClient]);
|
|
204
|
+
const handleManage = (0, _react.useCallback)(() => {
|
|
205
|
+
onComplete?.();
|
|
206
|
+
showBottomSheet?.('ManageAccount');
|
|
207
|
+
}, [onComplete, showBottomSheet]);
|
|
208
|
+
const handlers = (0, _react.useMemo)(() => ({
|
|
209
|
+
onSwitch: accountId => {
|
|
210
|
+
void handleSwitch(accountId);
|
|
211
|
+
},
|
|
212
|
+
onAdd: () => controller?.add(),
|
|
213
|
+
onManage: handleManage
|
|
214
|
+
}), [handleSwitch, controller, handleManage]);
|
|
215
|
+
if (!controller) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
if (view === 'accounts') {
|
|
219
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(AccountsView, {
|
|
220
|
+
snapshot: snapshot,
|
|
221
|
+
theme: theme,
|
|
222
|
+
t: t,
|
|
223
|
+
handlers: handlers
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (view === 'qr') {
|
|
227
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(QrView, {
|
|
228
|
+
snapshot: snapshot,
|
|
229
|
+
theme: theme,
|
|
230
|
+
t: t,
|
|
231
|
+
onRetry: () => void controller.showQr(),
|
|
232
|
+
passkeyAvailable: passkeyAvailable,
|
|
233
|
+
onSignInWithPasskey: () => void handleSignInWithPasskey(),
|
|
234
|
+
passkeyPending: signInPasskeyPending,
|
|
235
|
+
passkeyError: signInPasskeyError,
|
|
236
|
+
onCreateAccount: () => controller.startSignup()
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (view === 'signup') {
|
|
240
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(SignUpView, {
|
|
241
|
+
snapshot: snapshot,
|
|
242
|
+
theme: theme,
|
|
243
|
+
t: t,
|
|
244
|
+
oxyServices: oxyServices,
|
|
245
|
+
passkeyAvailable: passkeyAvailable,
|
|
246
|
+
onCreateWithPasskey: handleCreateWithPasskey,
|
|
247
|
+
createPending: createPasskeyPending,
|
|
248
|
+
createError: createPasskeyError,
|
|
249
|
+
onBackToSignIn: () => controller.setView('signin')
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(SignInView, {
|
|
253
|
+
snapshot: snapshot,
|
|
254
|
+
theme: theme,
|
|
255
|
+
t: t,
|
|
256
|
+
handlers: handlers,
|
|
257
|
+
onSignInWithOxy: () => void controller.signInWithOxy(),
|
|
258
|
+
onScanQr: () => void controller.showQr(),
|
|
259
|
+
onCreateAccount: () => controller.startSignup()
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// Accounts view
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Inline error banner for `snapshot.error` (the account-list / switch-failure
|
|
269
|
+
* error — distinct from `snapshot.signIn.error`, which `QrView` already
|
|
270
|
+
* renders). `AccountsView` and `SignInView` previously swallowed it: a
|
|
271
|
+
* blocked/failed switch left the user with no feedback outside the QR view.
|
|
272
|
+
*/
|
|
273
|
+
const ErrorBanner = ({
|
|
274
|
+
error,
|
|
275
|
+
theme
|
|
276
|
+
}) => {
|
|
277
|
+
if (!error) return null;
|
|
278
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
279
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.errorBanner, {
|
|
280
|
+
backgroundColor: theme.colors.negativeSubtle
|
|
281
|
+
}],
|
|
282
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.MaterialCommunityIcons, {
|
|
283
|
+
name: "alert-circle-outline",
|
|
284
|
+
size: 18,
|
|
285
|
+
color: theme.colors.error
|
|
286
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
287
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.errorBannerText, {
|
|
288
|
+
color: theme.colors.error
|
|
289
|
+
}],
|
|
290
|
+
children: error
|
|
291
|
+
})]
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
const AccountsView = ({
|
|
295
|
+
snapshot,
|
|
296
|
+
theme,
|
|
297
|
+
t,
|
|
298
|
+
handlers
|
|
299
|
+
}) => {
|
|
300
|
+
if (snapshot.loading && snapshot.accounts.length === 0) {
|
|
301
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
302
|
+
style: _oxyAuthChooserStyles.authChooserStyles.centeredBlock,
|
|
303
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.MaterialCommunityIcons, {
|
|
304
|
+
name: "loading",
|
|
305
|
+
size: 24,
|
|
306
|
+
color: theme.colors.textSecondary
|
|
307
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
308
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.mutedText, {
|
|
309
|
+
color: theme.colors.textSecondary
|
|
310
|
+
}],
|
|
311
|
+
children: t('accountSwitcher.loading') || 'Loading accounts…'
|
|
312
|
+
})]
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
316
|
+
style: _oxyAuthChooserStyles.authChooserStyles.rows,
|
|
317
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(ErrorBanner, {
|
|
318
|
+
error: snapshot.error,
|
|
319
|
+
theme: theme
|
|
320
|
+
}), snapshot.accounts.map(account => /*#__PURE__*/(0, _jsxRuntime.jsx)(AccountRow, {
|
|
321
|
+
account: account,
|
|
322
|
+
theme: theme,
|
|
323
|
+
switching: snapshot.switchingAccountId === account.accountId,
|
|
324
|
+
disabled: snapshot.switchingAccountId !== null,
|
|
325
|
+
onPress: () => handlers.onSwitch(account.accountId)
|
|
326
|
+
}, account.accountId)), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
|
|
327
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.addRow, {
|
|
328
|
+
borderColor: theme.colors.border
|
|
329
|
+
}],
|
|
330
|
+
onPress: handlers.onAdd,
|
|
331
|
+
accessibilityRole: "button",
|
|
332
|
+
accessibilityLabel: t('signin.addAccountTitle') || 'Add another account',
|
|
333
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
334
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.addBadge, {
|
|
335
|
+
borderColor: theme.colors.border
|
|
336
|
+
}],
|
|
337
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.MaterialCommunityIcons, {
|
|
338
|
+
name: "plus",
|
|
339
|
+
size: 20,
|
|
340
|
+
color: theme.colors.textSecondary
|
|
341
|
+
})
|
|
342
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
343
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.rowName, {
|
|
344
|
+
color: theme.colors.textSecondary
|
|
345
|
+
}],
|
|
346
|
+
children: t('signin.addAccountTitle') || 'Add another account'
|
|
347
|
+
})]
|
|
348
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
349
|
+
style: _oxyAuthChooserStyles.authChooserStyles.footerLinks,
|
|
350
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
|
|
351
|
+
onPress: handlers.onManage,
|
|
352
|
+
accessibilityRole: "button",
|
|
353
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
354
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.linkText, {
|
|
355
|
+
color: theme.colors.primary
|
|
356
|
+
}],
|
|
357
|
+
children: t('accountMenu.manage') || 'Manage accounts'
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
})]
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
const AccountRow = ({
|
|
364
|
+
account,
|
|
365
|
+
theme,
|
|
366
|
+
switching,
|
|
367
|
+
disabled,
|
|
368
|
+
onPress
|
|
369
|
+
}) => {
|
|
370
|
+
const accent = resolveAccentHex(account.color, theme.colors.primary);
|
|
371
|
+
const rowStyle = [_oxyAuthChooserStyles.authChooserStyles.accountRow, {
|
|
372
|
+
borderColor: account.isCurrent ? accent : theme.colors.border,
|
|
373
|
+
backgroundColor: theme.colors.card
|
|
374
|
+
}, disabled && !switching ? _oxyAuthChooserStyles.authChooserStyles.rowDisabled : null];
|
|
375
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_theme.BloomColorScope, {
|
|
376
|
+
colorPreset: toPreset(account.color),
|
|
377
|
+
asChild: true,
|
|
378
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
|
|
379
|
+
style: rowStyle,
|
|
380
|
+
onPress: onPress,
|
|
381
|
+
disabled: disabled,
|
|
382
|
+
accessibilityRole: "button",
|
|
383
|
+
accessibilityState: {
|
|
384
|
+
selected: account.isCurrent,
|
|
385
|
+
disabled
|
|
386
|
+
},
|
|
387
|
+
accessibilityLabel: account.displayName,
|
|
388
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
389
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.avatarRing, {
|
|
390
|
+
borderColor: account.isCurrent ? accent : 'transparent'
|
|
391
|
+
}],
|
|
392
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_avatar.Avatar, {
|
|
393
|
+
source: account.avatarUrl ?? undefined,
|
|
394
|
+
variant: "thumb",
|
|
395
|
+
name: account.displayName,
|
|
396
|
+
size: ROW_AVATAR_SIZE
|
|
397
|
+
})
|
|
398
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
399
|
+
style: _oxyAuthChooserStyles.authChooserStyles.rowMeta,
|
|
400
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
401
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.rowName, {
|
|
402
|
+
color: theme.colors.text
|
|
403
|
+
}],
|
|
404
|
+
numberOfLines: 1,
|
|
405
|
+
children: account.displayName
|
|
406
|
+
}), account.email ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
407
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.rowHandle, {
|
|
408
|
+
color: theme.colors.textSecondary
|
|
409
|
+
}],
|
|
410
|
+
numberOfLines: 1,
|
|
411
|
+
children: account.email
|
|
412
|
+
}) : null]
|
|
413
|
+
}), switching ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.MaterialCommunityIcons, {
|
|
414
|
+
name: "loading",
|
|
415
|
+
size: 20,
|
|
416
|
+
color: accent
|
|
417
|
+
}) : account.isCurrent ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.MaterialCommunityIcons, {
|
|
418
|
+
name: "check-circle",
|
|
419
|
+
size: 20,
|
|
420
|
+
color: accent
|
|
421
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.MaterialCommunityIcons, {
|
|
422
|
+
name: "chevron-right",
|
|
423
|
+
size: 20,
|
|
424
|
+
color: theme.colors.textSecondary
|
|
425
|
+
})]
|
|
426
|
+
})
|
|
427
|
+
});
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
// Sign-in view (add / signin) — NATIVE-facing today: on web the auto-start
|
|
432
|
+
// effect above moves the view to `qr` almost immediately, so this renders
|
|
433
|
+
// only for a brief instant there.
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
const SignInView = ({
|
|
437
|
+
snapshot,
|
|
438
|
+
theme,
|
|
439
|
+
t,
|
|
440
|
+
handlers,
|
|
441
|
+
onSignInWithOxy,
|
|
442
|
+
onScanQr,
|
|
443
|
+
onCreateAccount
|
|
444
|
+
}) => /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
445
|
+
style: _oxyAuthChooserStyles.authChooserStyles.signInBlock,
|
|
446
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(ErrorBanner, {
|
|
447
|
+
error: snapshot.error,
|
|
448
|
+
theme: theme
|
|
449
|
+
}), snapshot.accounts.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
450
|
+
style: _oxyAuthChooserStyles.authChooserStyles.rows,
|
|
451
|
+
children: [snapshot.accounts.map(account => /*#__PURE__*/(0, _jsxRuntime.jsx)(AccountRow, {
|
|
452
|
+
account: account,
|
|
453
|
+
theme: theme,
|
|
454
|
+
switching: snapshot.switchingAccountId === account.accountId,
|
|
455
|
+
disabled: snapshot.switchingAccountId !== null,
|
|
456
|
+
onPress: () => handlers.onSwitch(account.accountId)
|
|
457
|
+
}, account.accountId)), /*#__PURE__*/(0, _jsxRuntime.jsx)(Dividerish, {
|
|
458
|
+
theme: theme,
|
|
459
|
+
label: t('signin.or') || 'or'
|
|
460
|
+
})]
|
|
461
|
+
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_button.Button, {
|
|
462
|
+
variant: "primary",
|
|
463
|
+
onPress: onSignInWithOxy,
|
|
464
|
+
style: _oxyAuthChooserStyles.authChooserStyles.primaryButton,
|
|
465
|
+
children: t('accountSwitcher.signInWithOxy') || 'Sign in with Oxy'
|
|
466
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_button.Button, {
|
|
467
|
+
variant: "secondary",
|
|
468
|
+
onPress: onScanQr,
|
|
469
|
+
style: _oxyAuthChooserStyles.authChooserStyles.secondaryButton,
|
|
470
|
+
children: t('accountSwitcher.scanQr') || 'Scan a QR from another device'
|
|
471
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(SignUpFooterLink, {
|
|
472
|
+
theme: theme,
|
|
473
|
+
t: t,
|
|
474
|
+
onPress: onCreateAccount
|
|
475
|
+
})]
|
|
476
|
+
});
|
|
477
|
+
const Dividerish = ({
|
|
478
|
+
theme,
|
|
479
|
+
label
|
|
480
|
+
}) => /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
481
|
+
style: _oxyAuthChooserStyles.authChooserStyles.dividerRow,
|
|
482
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
483
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.dividerLine, {
|
|
484
|
+
backgroundColor: theme.colors.border
|
|
485
|
+
}]
|
|
486
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
487
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.dividerText, {
|
|
488
|
+
color: theme.colors.textSecondary
|
|
489
|
+
}],
|
|
490
|
+
children: label
|
|
491
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
492
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.dividerLine, {
|
|
493
|
+
backgroundColor: theme.colors.border
|
|
494
|
+
}]
|
|
495
|
+
})]
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
/** "New to Oxy? Create one" — shared by `SignInView` and `QrView`. */
|
|
499
|
+
const SignUpFooterLink = ({
|
|
500
|
+
theme,
|
|
501
|
+
t,
|
|
502
|
+
onPress
|
|
503
|
+
}) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
|
|
504
|
+
onPress: onPress,
|
|
505
|
+
accessibilityRole: "button",
|
|
506
|
+
style: _oxyAuthChooserStyles.authChooserStyles.footerLink,
|
|
507
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
508
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.linkText, {
|
|
509
|
+
color: theme.colors.textSecondary
|
|
510
|
+
}],
|
|
511
|
+
children: t('signin.createAccountLink') || 'New to Oxy? Create one'
|
|
512
|
+
})
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
/** "Already have an account? Sign in" — used by `SignUpView`. */
|
|
516
|
+
const SignInFooterLink = ({
|
|
517
|
+
theme,
|
|
518
|
+
t,
|
|
519
|
+
onPress
|
|
520
|
+
}) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
|
|
521
|
+
onPress: onPress,
|
|
522
|
+
accessibilityRole: "button",
|
|
523
|
+
style: _oxyAuthChooserStyles.authChooserStyles.footerLink,
|
|
524
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
525
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.linkText, {
|
|
526
|
+
color: theme.colors.textSecondary
|
|
527
|
+
}],
|
|
528
|
+
children: t('signup.backToSignInLink') || 'Already have an account? Sign in'
|
|
529
|
+
})
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
// QR view — the PRIMARY web sign-in surface, and the ONLY sign-in surface on
|
|
534
|
+
// native. Commons is ALWAYS the priority path (owner mandate): the QR here
|
|
535
|
+
// and the "Get Commons" CTA below are the leading content everywhere they
|
|
536
|
+
// appear. The passkey link is explicitly the lightweight, de-emphasized
|
|
537
|
+
// alternative for someone who doesn't want to install Commons — same visual
|
|
538
|
+
// subordination (a small text link, never a button) and the same "No
|
|
539
|
+
// Commons?" framing in its copy wherever it's offered.
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
|
|
542
|
+
const QrView = ({
|
|
543
|
+
snapshot,
|
|
544
|
+
theme,
|
|
545
|
+
t,
|
|
546
|
+
onRetry,
|
|
547
|
+
passkeyAvailable,
|
|
548
|
+
onSignInWithPasskey,
|
|
549
|
+
passkeyPending,
|
|
550
|
+
passkeyError,
|
|
551
|
+
onCreateAccount
|
|
552
|
+
}) => {
|
|
553
|
+
const {
|
|
554
|
+
signIn,
|
|
555
|
+
commonsAvailability
|
|
556
|
+
} = snapshot;
|
|
557
|
+
const [showQrAnyway, setShowQrAnyway] = (0, _react.useState)(false);
|
|
558
|
+
const passkeyLink = passkeyAvailable && signIn.phase !== 'authorized' ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
559
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
|
|
560
|
+
onPress: onSignInWithPasskey,
|
|
561
|
+
disabled: passkeyPending,
|
|
562
|
+
accessibilityRole: "button",
|
|
563
|
+
style: _oxyAuthChooserStyles.authChooserStyles.footerLink,
|
|
564
|
+
testID: "passkey-signin-link",
|
|
565
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
566
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.linkText, {
|
|
567
|
+
color: theme.colors.textSecondary
|
|
568
|
+
}],
|
|
569
|
+
children: passkeyPending ? t('accountSwitcher.passkeySigningIn') || 'Signing in…' : t('accountSwitcher.useIdentityOnDevice') || 'No Commons? Use a passkey on this device instead'
|
|
570
|
+
})
|
|
571
|
+
}), passkeyError ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
572
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.errorText, {
|
|
573
|
+
color: theme.colors.error
|
|
574
|
+
}],
|
|
575
|
+
children: passkeyError
|
|
576
|
+
}) : null]
|
|
577
|
+
}) : null;
|
|
578
|
+
|
|
579
|
+
// Native, Commons confirmed NOT installed: a same-device QR is a dead
|
|
580
|
+
// end (the user can't scan their own screen) — lead with "Get Commons"
|
|
581
|
+
// instead, and demote the QR to an explicit "I have it elsewhere" reveal.
|
|
582
|
+
if (commonsAvailability === 'unavailable' && !showQrAnyway) {
|
|
583
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
584
|
+
style: _oxyAuthChooserStyles.authChooserStyles.centeredBlock,
|
|
585
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
586
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.mutedText, {
|
|
587
|
+
color: theme.colors.textSecondary
|
|
588
|
+
}],
|
|
589
|
+
children: t('accountSwitcher.commonsNotInstalled') || "Don't have Commons? Get the app to sign in with your Oxy ID."
|
|
590
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_button.Button, {
|
|
591
|
+
variant: "primary",
|
|
592
|
+
onPress: () => void _reactNative.Linking.openURL((0, _commonsStoreLinks.getCommonsAcquisitionUrl)(_reactNative.Platform.OS)),
|
|
593
|
+
style: _oxyAuthChooserStyles.authChooserStyles.primaryButton,
|
|
594
|
+
children: t('accountSwitcher.getCommons') || 'Get Commons'
|
|
595
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
|
|
596
|
+
onPress: () => setShowQrAnyway(true),
|
|
597
|
+
accessibilityRole: "button",
|
|
598
|
+
style: _oxyAuthChooserStyles.authChooserStyles.footerLink,
|
|
599
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
600
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.linkText, {
|
|
601
|
+
color: theme.colors.textSecondary
|
|
602
|
+
}],
|
|
603
|
+
children: t('accountSwitcher.showQrAnyway') || 'I have Commons on another device'
|
|
604
|
+
})
|
|
605
|
+
}), passkeyLink, /*#__PURE__*/(0, _jsxRuntime.jsx)(SignUpFooterLink, {
|
|
606
|
+
theme: theme,
|
|
607
|
+
t: t,
|
|
608
|
+
onPress: onCreateAccount
|
|
609
|
+
})]
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
if (signIn.phase === 'error') {
|
|
613
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
614
|
+
style: _oxyAuthChooserStyles.authChooserStyles.centeredBlock,
|
|
615
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
616
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.errorText, {
|
|
617
|
+
color: theme.colors.error
|
|
618
|
+
}],
|
|
619
|
+
children: signIn.error || 'Sign-in failed. Please try again.'
|
|
620
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_button.Button, {
|
|
621
|
+
variant: "secondary",
|
|
622
|
+
onPress: onRetry,
|
|
623
|
+
style: _oxyAuthChooserStyles.authChooserStyles.secondaryButton,
|
|
624
|
+
children: t('common.actions.tryAgain') || 'Try again'
|
|
625
|
+
}), passkeyLink]
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
if (signIn.phase === 'starting' || signIn.phase === 'authorized' || !signIn.qrPayload) {
|
|
629
|
+
const label = signIn.phase === 'authorized' ? t('signin.status.signingIn') || 'Signing in…' : t('accountSwitcher.loading') || 'Preparing sign-in…';
|
|
630
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
631
|
+
style: _oxyAuthChooserStyles.authChooserStyles.centeredBlock,
|
|
632
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.MaterialCommunityIcons, {
|
|
633
|
+
name: "loading",
|
|
634
|
+
size: 26,
|
|
635
|
+
color: theme.colors.primary
|
|
636
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
637
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.mutedText, {
|
|
638
|
+
color: theme.colors.textSecondary
|
|
639
|
+
}],
|
|
640
|
+
children: label
|
|
641
|
+
})]
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
645
|
+
style: _oxyAuthChooserStyles.authChooserStyles.centeredBlock,
|
|
646
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
647
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.qrHeadline, {
|
|
648
|
+
color: theme.colors.text
|
|
649
|
+
}],
|
|
650
|
+
children: t('accountSwitcher.qrHeadline') || 'Sign in with your Oxy identity'
|
|
651
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
652
|
+
style: _oxyAuthChooserStyles.authChooserStyles.qrPlate,
|
|
653
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeQrcodeSvg.default, {
|
|
654
|
+
value: signIn.qrPayload,
|
|
655
|
+
size: QR_SIZE,
|
|
656
|
+
backgroundColor: QR_PLATE_BG,
|
|
657
|
+
color: QR_FOREGROUND
|
|
658
|
+
})
|
|
659
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
660
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.mutedText, {
|
|
661
|
+
color: theme.colors.textSecondary
|
|
662
|
+
}],
|
|
663
|
+
children: t('accountSwitcher.scanWithOxy') || 'Scan with Commons on your phone to continue.'
|
|
664
|
+
}), passkeyLink, /*#__PURE__*/(0, _jsxRuntime.jsx)(SignUpFooterLink, {
|
|
665
|
+
theme: theme,
|
|
666
|
+
t: t,
|
|
667
|
+
onPress: onCreateAccount
|
|
668
|
+
})]
|
|
669
|
+
});
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
// ---------------------------------------------------------------------------
|
|
673
|
+
// Sign-up view
|
|
674
|
+
// ---------------------------------------------------------------------------
|
|
675
|
+
|
|
676
|
+
/** Debounced username-availability check over the existing SDK method. */
|
|
677
|
+
function useUsernameAvailability(oxyServices) {
|
|
678
|
+
const [status, setStatus] = (0, _react.useState)('idle');
|
|
679
|
+
const timerRef = (0, _react.useRef)(null);
|
|
680
|
+
const requestSeqRef = (0, _react.useRef)(0);
|
|
681
|
+
const check = (0, _react.useCallback)(value => {
|
|
682
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
683
|
+
const seq = ++requestSeqRef.current;
|
|
684
|
+
if (!value || value.length < 3) {
|
|
685
|
+
setStatus('idle');
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
setStatus('checking');
|
|
689
|
+
timerRef.current = setTimeout(() => {
|
|
690
|
+
void oxyServices.checkUsernameAvailability(value).then(result => {
|
|
691
|
+
if (seq !== requestSeqRef.current) return; // superseded by a newer check
|
|
692
|
+
setStatus(result.available ? 'available' : 'taken');
|
|
693
|
+
}).catch(() => {
|
|
694
|
+
if (seq !== requestSeqRef.current) return;
|
|
695
|
+
setStatus('idle'); // network/error — don't block on a stale verdict
|
|
696
|
+
});
|
|
697
|
+
}, 400);
|
|
698
|
+
}, [oxyServices]);
|
|
699
|
+
return {
|
|
700
|
+
status,
|
|
701
|
+
check
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
const UsernameStatusText = ({
|
|
705
|
+
status,
|
|
706
|
+
theme,
|
|
707
|
+
t
|
|
708
|
+
}) => {
|
|
709
|
+
if (status === 'idle') return null;
|
|
710
|
+
if (status === 'checking') {
|
|
711
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
712
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.mutedText, {
|
|
713
|
+
color: theme.colors.textSecondary
|
|
714
|
+
}],
|
|
715
|
+
children: t('signup.usernameChecking') || 'Checking…'
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
if (status === 'available') {
|
|
719
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
720
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.mutedText, {
|
|
721
|
+
color: theme.colors.success
|
|
722
|
+
}],
|
|
723
|
+
children: t('signup.usernameAvailable') || 'Available'
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
727
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.mutedText, {
|
|
728
|
+
color: theme.colors.error
|
|
729
|
+
}],
|
|
730
|
+
children: t('signup.usernameTaken') || 'This username is taken'
|
|
731
|
+
});
|
|
732
|
+
};
|
|
733
|
+
const SignUpView = ({
|
|
734
|
+
snapshot,
|
|
735
|
+
theme,
|
|
736
|
+
t,
|
|
737
|
+
oxyServices,
|
|
738
|
+
passkeyAvailable,
|
|
739
|
+
onCreateWithPasskey,
|
|
740
|
+
createPending,
|
|
741
|
+
createError,
|
|
742
|
+
onBackToSignIn
|
|
743
|
+
}) => {
|
|
744
|
+
const [username, setUsername] = (0, _react.useState)('');
|
|
745
|
+
const {
|
|
746
|
+
status: usernameStatus,
|
|
747
|
+
check: checkUsername
|
|
748
|
+
} = useUsernameAvailability(oxyServices);
|
|
749
|
+
const canSubmit = username.trim().length >= 3 && usernameStatus === 'available' && !createPending;
|
|
750
|
+
|
|
751
|
+
// Native — Commons owns identity creation. Mirrors the QR view's own
|
|
752
|
+
// commonsAvailability branch: deep-link straight in when installed, else
|
|
753
|
+
// lead with the same "Get Commons" fallback.
|
|
754
|
+
if (!(0, _isWebBrowser.isWebBrowser)()) {
|
|
755
|
+
const commonsInstalled = snapshot.commonsAvailability === 'available';
|
|
756
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
757
|
+
style: _oxyAuthChooserStyles.authChooserStyles.centeredBlock,
|
|
758
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_button.Button, {
|
|
759
|
+
variant: "primary",
|
|
760
|
+
onPress: () => void _reactNative.Linking.openURL(commonsInstalled ? COMMONS_CREATE_IDENTITY_URL : (0, _commonsStoreLinks.getCommonsAcquisitionUrl)(_reactNative.Platform.OS)),
|
|
761
|
+
style: _oxyAuthChooserStyles.authChooserStyles.primaryButton,
|
|
762
|
+
children: commonsInstalled ? t('signup.createInCommons') || 'Create your identity in Commons' : t('accountSwitcher.getCommons') || 'Get Commons'
|
|
763
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(SignInFooterLink, {
|
|
764
|
+
theme: theme,
|
|
765
|
+
t: t,
|
|
766
|
+
onPress: onBackToSignIn
|
|
767
|
+
})]
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Web, first-party Oxy origin. Commons leads (owner mandate: it's ALWAYS
|
|
772
|
+
// the priority path, everywhere the two appear) with a "Get Commons" CTA;
|
|
773
|
+
// passkey creation is the de-emphasized alternative underneath for someone
|
|
774
|
+
// who doesn't want to install anything — secondary button, introduced by
|
|
775
|
+
// its own "No Commons?" framing, same subordination as the QR view's link.
|
|
776
|
+
if (passkeyAvailable) {
|
|
777
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
778
|
+
style: _oxyAuthChooserStyles.authChooserStyles.signInBlock,
|
|
779
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
780
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.qrHeadline, {
|
|
781
|
+
color: theme.colors.text
|
|
782
|
+
}],
|
|
783
|
+
children: t('signup.commonsHeadline') || 'Create your identity in Commons'
|
|
784
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
785
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.mutedText, {
|
|
786
|
+
color: theme.colors.textSecondary
|
|
787
|
+
}],
|
|
788
|
+
children: t('signup.commonsExplainer') || 'Commons is your full self-custody Oxy identity — one app, works everywhere.'
|
|
789
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_button.Button, {
|
|
790
|
+
variant: "primary",
|
|
791
|
+
onPress: () => void _reactNative.Linking.openURL((0, _commonsStoreLinks.getCommonsAcquisitionUrl)(_reactNative.Platform.OS)),
|
|
792
|
+
style: _oxyAuthChooserStyles.authChooserStyles.primaryButton,
|
|
793
|
+
children: t('accountSwitcher.getCommons') || 'Get Commons'
|
|
794
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(Dividerish, {
|
|
795
|
+
theme: theme,
|
|
796
|
+
label: t('signin.or') || 'or'
|
|
797
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
798
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.mutedText, {
|
|
799
|
+
color: theme.colors.textSecondary
|
|
800
|
+
}],
|
|
801
|
+
children: t('signup.passkeyAlternative') || "No Commons? Create a passkey on this device instead"
|
|
802
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
|
|
803
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.usernameInput, {
|
|
804
|
+
borderColor: theme.colors.border,
|
|
805
|
+
color: theme.colors.text
|
|
806
|
+
}],
|
|
807
|
+
placeholder: t('signup.usernamePlaceholder') || 'Choose a username',
|
|
808
|
+
placeholderTextColor: theme.colors.textSecondary,
|
|
809
|
+
value: username,
|
|
810
|
+
autoCapitalize: "none",
|
|
811
|
+
autoCorrect: false,
|
|
812
|
+
autoComplete: "username",
|
|
813
|
+
onChangeText: value => {
|
|
814
|
+
setUsername(value);
|
|
815
|
+
checkUsername(value.trim());
|
|
816
|
+
},
|
|
817
|
+
testID: "signup-username-input"
|
|
818
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(UsernameStatusText, {
|
|
819
|
+
status: usernameStatus,
|
|
820
|
+
theme: theme,
|
|
821
|
+
t: t
|
|
822
|
+
}), createError ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
823
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.errorText, {
|
|
824
|
+
color: theme.colors.error
|
|
825
|
+
}],
|
|
826
|
+
children: createError
|
|
827
|
+
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_button.Button, {
|
|
828
|
+
variant: "secondary",
|
|
829
|
+
onPress: () => void onCreateWithPasskey(username.trim()),
|
|
830
|
+
disabled: !canSubmit,
|
|
831
|
+
style: _oxyAuthChooserStyles.authChooserStyles.secondaryButton,
|
|
832
|
+
testID: "signup-create-button",
|
|
833
|
+
children: createPending ? t('signup.creating') || 'Creating…' : t('signup.createAccount') || 'Create account'
|
|
834
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(SignInFooterLink, {
|
|
835
|
+
theme: theme,
|
|
836
|
+
t: t,
|
|
837
|
+
onPress: onBackToSignIn
|
|
838
|
+
})]
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Web, non-Oxy origin: passkey creation here requires the cross-origin hub
|
|
843
|
+
// relay (b2), which has not shipped yet — an honest message beats a button
|
|
844
|
+
// that would fail.
|
|
845
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
846
|
+
style: _oxyAuthChooserStyles.authChooserStyles.centeredBlock,
|
|
847
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_typography.Text, {
|
|
848
|
+
style: [_oxyAuthChooserStyles.authChooserStyles.mutedText, {
|
|
849
|
+
color: theme.colors.textSecondary
|
|
850
|
+
}],
|
|
851
|
+
children: t('signup.notAvailableHere') || "Account creation isn't available in this app yet — create your identity with Commons, or visit oxy.so to sign up with a passkey."
|
|
852
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(SignInFooterLink, {
|
|
853
|
+
theme: theme,
|
|
854
|
+
t: t,
|
|
855
|
+
onPress: onBackToSignIn
|
|
856
|
+
})]
|
|
857
|
+
});
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
// ---------------------------------------------------------------------------
|
|
861
|
+
// Empty snapshot (no-provider loading state)
|
|
862
|
+
// ---------------------------------------------------------------------------
|
|
863
|
+
|
|
864
|
+
const EMPTY_SNAPSHOT = {
|
|
865
|
+
view: 'accounts',
|
|
866
|
+
accounts: [],
|
|
867
|
+
activeAccountId: null,
|
|
868
|
+
loading: false,
|
|
869
|
+
error: null,
|
|
870
|
+
switchingAccountId: null,
|
|
871
|
+
signIn: {
|
|
872
|
+
phase: 'idle',
|
|
873
|
+
authorizeCode: null,
|
|
874
|
+
qrPayload: null,
|
|
875
|
+
expiresAt: null,
|
|
876
|
+
error: null
|
|
877
|
+
},
|
|
878
|
+
commonsAvailability: 'unknown'
|
|
879
|
+
};
|
|
880
|
+
var _default = exports.default = OxyAuthChooser;
|
|
881
|
+
//# sourceMappingURL=OxyAuthChooser.js.map
|