@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.
Files changed (117) hide show
  1. package/lib/commonjs/index.js +12 -0
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/boot/runProviderColdBoot.js +12 -2
  4. package/lib/commonjs/ui/boot/runProviderColdBoot.js.map +1 -1
  5. package/lib/commonjs/ui/components/OxyAccountDialog.js +27 -546
  6. package/lib/commonjs/ui/components/OxyAccountDialog.js.map +1 -1
  7. package/lib/commonjs/ui/components/OxyAuthChooser.js +881 -0
  8. package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -0
  9. package/lib/commonjs/ui/components/OxyProvider.js +2 -0
  10. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  11. package/lib/commonjs/ui/components/oxyAuthChooserStyles.js +158 -0
  12. package/lib/commonjs/ui/components/oxyAuthChooserStyles.js.map +1 -0
  13. package/lib/commonjs/ui/context/OxyContext.js +50 -72
  14. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  15. package/lib/commonjs/ui/context/passkeyFlow.js +9 -8
  16. package/lib/commonjs/ui/context/passkeyFlow.js.map +1 -1
  17. package/lib/commonjs/ui/context/useOxyAccountGraph.js +6 -2
  18. package/lib/commonjs/ui/context/useOxyAccountGraph.js.map +1 -1
  19. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +2 -1
  20. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -1
  21. package/lib/commonjs/ui/utils/commonsStoreLinks.js +42 -0
  22. package/lib/commonjs/ui/utils/commonsStoreLinks.js.map +1 -0
  23. package/lib/commonjs/ui/utils/crossOriginRestore.js +26 -0
  24. package/lib/commonjs/ui/utils/crossOriginRestore.js.map +1 -1
  25. package/lib/module/index.js +5 -0
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/module/ui/boot/runProviderColdBoot.js +13 -3
  28. package/lib/module/ui/boot/runProviderColdBoot.js.map +1 -1
  29. package/lib/module/ui/components/OxyAccountDialog.js +29 -550
  30. package/lib/module/ui/components/OxyAccountDialog.js.map +1 -1
  31. package/lib/module/ui/components/OxyAuthChooser.js +878 -0
  32. package/lib/module/ui/components/OxyAuthChooser.js.map +1 -0
  33. package/lib/module/ui/components/OxyProvider.js +2 -0
  34. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  35. package/lib/module/ui/components/oxyAuthChooserStyles.js +155 -0
  36. package/lib/module/ui/components/oxyAuthChooserStyles.js.map +1 -0
  37. package/lib/module/ui/context/OxyContext.js +50 -72
  38. package/lib/module/ui/context/OxyContext.js.map +1 -1
  39. package/lib/module/ui/context/passkeyFlow.js +9 -8
  40. package/lib/module/ui/context/passkeyFlow.js.map +1 -1
  41. package/lib/module/ui/context/useOxyAccountGraph.js +6 -2
  42. package/lib/module/ui/context/useOxyAccountGraph.js.map +1 -1
  43. package/lib/module/ui/hooks/useSwitchableAccounts.js +2 -1
  44. package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -1
  45. package/lib/module/ui/utils/commonsStoreLinks.js +37 -0
  46. package/lib/module/ui/utils/commonsStoreLinks.js.map +1 -0
  47. package/lib/module/ui/utils/crossOriginRestore.js +26 -1
  48. package/lib/module/ui/utils/crossOriginRestore.js.map +1 -1
  49. package/lib/typescript/commonjs/index.d.ts +3 -1
  50. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts +6 -0
  52. package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts +10 -20
  54. package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts +60 -0
  56. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -0
  57. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/ui/components/oxyAuthChooserStyles.d.ts +148 -0
  59. package/lib/typescript/commonjs/ui/components/oxyAuthChooserStyles.d.ts.map +1 -0
  60. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +1 -1
  61. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +22 -27
  63. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/ui/context/passkeyFlow.d.ts +15 -4
  65. package/lib/typescript/commonjs/ui/context/passkeyFlow.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts +1 -0
  67. package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/ui/types/navigation.d.ts +8 -0
  70. package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/ui/utils/commonsStoreLinks.d.ts +24 -0
  72. package/lib/typescript/commonjs/ui/utils/commonsStoreLinks.d.ts.map +1 -0
  73. package/lib/typescript/commonjs/ui/utils/crossOriginRestore.d.ts +19 -0
  74. package/lib/typescript/commonjs/ui/utils/crossOriginRestore.d.ts.map +1 -1
  75. package/lib/typescript/module/index.d.ts +3 -1
  76. package/lib/typescript/module/index.d.ts.map +1 -1
  77. package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts +6 -0
  78. package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts.map +1 -1
  79. package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts +10 -20
  80. package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts.map +1 -1
  81. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts +60 -0
  82. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -0
  83. package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
  84. package/lib/typescript/module/ui/components/oxyAuthChooserStyles.d.ts +148 -0
  85. package/lib/typescript/module/ui/components/oxyAuthChooserStyles.d.ts.map +1 -0
  86. package/lib/typescript/module/ui/context/OxyContext.d.ts +1 -1
  87. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  88. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +22 -27
  89. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
  90. package/lib/typescript/module/ui/context/passkeyFlow.d.ts +15 -4
  91. package/lib/typescript/module/ui/context/passkeyFlow.d.ts.map +1 -1
  92. package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts +1 -0
  93. package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts.map +1 -1
  94. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -1
  95. package/lib/typescript/module/ui/types/navigation.d.ts +8 -0
  96. package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
  97. package/lib/typescript/module/ui/utils/commonsStoreLinks.d.ts +24 -0
  98. package/lib/typescript/module/ui/utils/commonsStoreLinks.d.ts.map +1 -0
  99. package/lib/typescript/module/ui/utils/crossOriginRestore.d.ts +19 -0
  100. package/lib/typescript/module/ui/utils/crossOriginRestore.d.ts.map +1 -1
  101. package/package.json +5 -5
  102. package/src/index.ts +8 -1
  103. package/src/ui/boot/runProviderColdBoot.ts +24 -1
  104. package/src/ui/components/OxyAccountDialog.tsx +33 -571
  105. package/src/ui/components/OxyAuthChooser.tsx +883 -0
  106. package/src/ui/components/OxyProvider.tsx +2 -0
  107. package/src/ui/components/oxyAuthChooserStyles.ts +152 -0
  108. package/src/ui/context/OxyContext.tsx +56 -82
  109. package/src/ui/context/__tests__/passkeyFlow.test.ts +22 -15
  110. package/src/ui/context/oxyContextTypes.ts +27 -26
  111. package/src/ui/context/passkeyFlow.ts +17 -9
  112. package/src/ui/context/useOxyAccountGraph.ts +4 -2
  113. package/src/ui/hooks/useSwitchableAccounts.ts +1 -0
  114. package/src/ui/types/navigation.ts +8 -0
  115. package/src/ui/utils/__tests__/crossOriginRestore.test.ts +143 -0
  116. package/src/ui/utils/commonsStoreLinks.ts +38 -0
  117. package/src/ui/utils/crossOriginRestore.ts +34 -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