@oxy.so/services 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/LanguageBridge.js +51 -0
- package/lib/commonjs/ui/components/LanguageBridge.js.map +1 -0
- package/lib/commonjs/ui/components/OxyAuthChooser.js +95 -140
- package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -1
- package/lib/commonjs/ui/components/OxyConsentScreen.js +132 -2
- package/lib/commonjs/ui/components/OxyConsentScreen.js.map +1 -1
- package/lib/commonjs/ui/components/OxyProvider.js +19 -1
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/components/authChooser/AccountsMenuView.js +8 -1
- package/lib/commonjs/ui/components/authChooser/AccountsMenuView.js.map +1 -1
- package/lib/commonjs/ui/components/authChooser/SignInEntryView.js +35 -25
- package/lib/commonjs/ui/components/authChooser/SignInEntryView.js.map +1 -1
- package/lib/commonjs/ui/components/authChooser/SignInRequestView.js +2 -3
- package/lib/commonjs/ui/components/authChooser/SignInRequestView.js.map +1 -1
- package/lib/commonjs/ui/components/authChooser/SignUpView.js +12 -127
- package/lib/commonjs/ui/components/authChooser/SignUpView.js.map +1 -1
- package/lib/commonjs/ui/components/authChooser/signInFailureMessage.js +40 -0
- package/lib/commonjs/ui/components/authChooser/signInFailureMessage.js.map +1 -0
- package/lib/commonjs/ui/components/authChooser/types.js +4 -6
- package/lib/commonjs/ui/components/authChooser/types.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +15 -2
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/hooks/accountDialogSnapshot.js +3 -1
- package/lib/commonjs/ui/hooks/accountDialogSnapshot.js.map +1 -1
- package/lib/commonjs/ui/hooks/useProfileEditing.js +14 -0
- package/lib/commonjs/ui/hooks/useProfileEditing.js.map +1 -1
- package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +225 -9
- package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/ManageAccountScreen.js +10 -0
- package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/LanguageBridge.js +47 -0
- package/lib/module/ui/components/LanguageBridge.js.map +1 -0
- package/lib/module/ui/components/OxyAuthChooser.js +97 -142
- package/lib/module/ui/components/OxyAuthChooser.js.map +1 -1
- package/lib/module/ui/components/OxyConsentScreen.js +132 -2
- package/lib/module/ui/components/OxyConsentScreen.js.map +1 -1
- package/lib/module/ui/components/OxyProvider.js +19 -1
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/authChooser/AccountsMenuView.js +8 -1
- package/lib/module/ui/components/authChooser/AccountsMenuView.js.map +1 -1
- package/lib/module/ui/components/authChooser/SignInEntryView.js +35 -25
- package/lib/module/ui/components/authChooser/SignInEntryView.js.map +1 -1
- package/lib/module/ui/components/authChooser/SignInRequestView.js +2 -3
- package/lib/module/ui/components/authChooser/SignInRequestView.js.map +1 -1
- package/lib/module/ui/components/authChooser/SignUpView.js +14 -129
- package/lib/module/ui/components/authChooser/SignUpView.js.map +1 -1
- package/lib/module/ui/components/authChooser/signInFailureMessage.js +36 -0
- package/lib/module/ui/components/authChooser/signInFailureMessage.js.map +1 -0
- package/lib/module/ui/components/authChooser/types.js +4 -6
- package/lib/module/ui/components/authChooser/types.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +15 -2
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/hooks/accountDialogSnapshot.js +3 -1
- package/lib/module/ui/hooks/accountDialogSnapshot.js.map +1 -1
- package/lib/module/ui/hooks/useProfileEditing.js +14 -0
- package/lib/module/ui/hooks/useProfileEditing.js.map +1 -1
- package/lib/module/ui/screens/EditProfileFieldScreen.js +225 -9
- package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
- package/lib/module/ui/screens/ManageAccountScreen.js +12 -2
- package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/LanguageBridge.d.ts +19 -0
- package/lib/typescript/commonjs/ui/components/LanguageBridge.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts +3 -4
- package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyConsentScreen.d.ts +17 -1
- package/lib/typescript/commonjs/ui/components/OxyConsentScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts +14 -0
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/authChooser/AccountsMenuView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/authChooser/SignInEntryView.d.ts +11 -12
- package/lib/typescript/commonjs/ui/components/authChooser/SignInEntryView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/authChooser/SignInRequestView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/authChooser/SignUpView.d.ts +10 -13
- package/lib/typescript/commonjs/ui/components/authChooser/SignUpView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/authChooser/signInFailureMessage.d.ts +16 -0
- package/lib/typescript/commonjs/ui/components/authChooser/signInFailureMessage.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts +11 -13
- package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +3 -2
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +20 -2
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/accountDialogSnapshot.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +3 -0
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useProfileEditing.d.ts +1 -0
- package/lib/typescript/commonjs/ui/hooks/useProfileEditing.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/types/navigation.d.ts +29 -0
- package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/LanguageBridge.d.ts +19 -0
- package/lib/typescript/module/ui/components/LanguageBridge.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts +3 -4
- package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyConsentScreen.d.ts +17 -1
- package/lib/typescript/module/ui/components/OxyConsentScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyProvider.d.ts +14 -0
- package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/authChooser/AccountsMenuView.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/authChooser/SignInEntryView.d.ts +11 -12
- package/lib/typescript/module/ui/components/authChooser/SignInEntryView.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/authChooser/SignInRequestView.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/authChooser/SignUpView.d.ts +10 -13
- package/lib/typescript/module/ui/components/authChooser/SignUpView.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/authChooser/signInFailureMessage.d.ts +16 -0
- package/lib/typescript/module/ui/components/authChooser/signInFailureMessage.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/authChooser/types.d.ts +11 -13
- package/lib/typescript/module/ui/components/authChooser/types.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +3 -2
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +20 -2
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/accountDialogSnapshot.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +3 -0
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useProfileEditing.d.ts +1 -0
- package/lib/typescript/module/ui/hooks/useProfileEditing.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/types/navigation.d.ts +29 -0
- package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/index.ts +2 -1
- package/src/ui/components/LanguageBridge.tsx +44 -0
- package/src/ui/components/OxyAuthChooser.tsx +106 -156
- package/src/ui/components/OxyConsentScreen.tsx +127 -0
- package/src/ui/components/OxyProvider.tsx +17 -0
- package/src/ui/components/__tests__/LanguageBridge.test.tsx +81 -0
- package/src/ui/components/__tests__/OxyConsentScreen.test.tsx +32 -0
- package/src/ui/components/authChooser/AccountsMenuView.tsx +7 -0
- package/src/ui/components/authChooser/SignInEntryView.tsx +41 -44
- package/src/ui/components/authChooser/SignInRequestView.tsx +1 -4
- package/src/ui/components/authChooser/SignUpView.tsx +16 -114
- package/src/ui/components/authChooser/signInFailureMessage.ts +39 -0
- package/src/ui/components/authChooser/types.ts +11 -13
- package/src/ui/context/OxyContext.tsx +1 -1
- package/src/ui/context/hooks/useAuthOperations.ts +10 -5
- package/src/ui/context/oxyContextTypes.ts +16 -2
- package/src/ui/hooks/accountDialogSnapshot.ts +2 -0
- package/src/ui/hooks/useProfileEditing.ts +15 -0
- package/src/ui/screens/EditProfileFieldScreen.tsx +202 -7
- package/src/ui/screens/ManageAccountScreen.tsx +12 -0
- package/src/ui/types/navigation.ts +30 -0
- package/lib/commonjs/ui/components/authChooser/useUsernameAvailability.js +0 -50
- package/lib/commonjs/ui/components/authChooser/useUsernameAvailability.js.map +0 -1
- package/lib/module/ui/components/authChooser/useUsernameAvailability.js +0 -46
- package/lib/module/ui/components/authChooser/useUsernameAvailability.js.map +0 -1
- package/lib/typescript/commonjs/ui/components/authChooser/useUsernameAvailability.d.ts +0 -16
- package/lib/typescript/commonjs/ui/components/authChooser/useUsernameAvailability.d.ts.map +0 -1
- package/lib/typescript/module/ui/components/authChooser/useUsernameAvailability.d.ts +0 -16
- package/lib/typescript/module/ui/components/authChooser/useUsernameAvailability.d.ts.map +0 -1
- package/src/ui/components/authChooser/useUsernameAvailability.ts +0 -60
package/src/index.ts
CHANGED
|
@@ -56,7 +56,7 @@ export type {
|
|
|
56
56
|
OxyRuntimeStatus,
|
|
57
57
|
OxyTokenStatus,
|
|
58
58
|
} from './ui/runtime';
|
|
59
|
-
export type { OxyContextState } from './ui/context/OxyContext';
|
|
59
|
+
export type { LogoutResult, OxyContextState } from './ui/context/OxyContext';
|
|
60
60
|
export { useAuth } from './ui/hooks/useAuth';
|
|
61
61
|
export type { AuthState, AuthActions, SignInOutcome, UseAuthReturn } from './ui/hooks/useAuth';
|
|
62
62
|
export { createDeferredProductAnalytics } from './ui/analytics/productAnalytics';
|
|
@@ -296,6 +296,7 @@ export type {
|
|
|
296
296
|
OxyConsentScreenProps,
|
|
297
297
|
OxyConsentApplication,
|
|
298
298
|
OxyConsentUser,
|
|
299
|
+
OxyConsentResource,
|
|
299
300
|
} from './ui/components/OxyConsentScreen';
|
|
300
301
|
|
|
301
302
|
// Optional signed-out gate primitive. Wrap any subtree (or the whole app via
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { coerceToSupportedLocale } from '@oxy.so/core';
|
|
3
|
+
import { useOxy } from '../context/OxyContext';
|
|
4
|
+
import type { OxyLanguageConfig } from '../types/navigation';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `OxyProvider`'s language config prop, realized. Oxy decides which
|
|
8
|
+
* language an account (or, signed out, the device/guest locale) should see;
|
|
9
|
+
* this is the ONE place that decision reaches the host app's own i18n library
|
|
10
|
+
* — so an app never re-derives or imperatively tracks a language of its own,
|
|
11
|
+
* and never mounts anything extra to get it. It owns only its translation
|
|
12
|
+
* catalogs.
|
|
13
|
+
*
|
|
14
|
+
* `currentLanguage` may name a locale the host app never shipped a catalog
|
|
15
|
+
* for — {@link coerceToSupportedLocale} narrows it to the closest one the app
|
|
16
|
+
* actually has (exact match, then same base language, then `fallbackLocale`).
|
|
17
|
+
*
|
|
18
|
+
* Renders nothing. Mounted by `OxyProvider` INSIDE `OxyRuntimeProvider` only
|
|
19
|
+
* when this config is supplied, so an app with no i18n of its own pays
|
|
20
|
+
* nothing.
|
|
21
|
+
*/
|
|
22
|
+
export function LanguageBridge({ supportedLocales, fallbackLocale, onChange, onError }: OxyLanguageConfig): null {
|
|
23
|
+
const { currentLanguage } = useOxy();
|
|
24
|
+
const resolvedLocale = useMemo(
|
|
25
|
+
() => coerceToSupportedLocale(currentLanguage, supportedLocales, fallbackLocale),
|
|
26
|
+
[currentLanguage, supportedLocales, fallbackLocale],
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Refs, not effect dependencies: an inline `onChange`/`onError` (the common
|
|
30
|
+
// case) must not re-fire the effect on every render — only a genuine
|
|
31
|
+
// change in the RESOLVED locale should call it again.
|
|
32
|
+
const onChangeRef = useRef(onChange);
|
|
33
|
+
onChangeRef.current = onChange;
|
|
34
|
+
const onErrorRef = useRef(onError);
|
|
35
|
+
onErrorRef.current = onError;
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
Promise.resolve(onChangeRef.current(resolvedLocale)).catch((error: unknown) => {
|
|
39
|
+
onErrorRef.current?.(error, resolvedLocale);
|
|
40
|
+
});
|
|
41
|
+
}, [resolvedLocale]);
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
import type React from 'react';
|
|
37
|
-
import { useCallback, useMemo,
|
|
37
|
+
import { useCallback, useMemo, useSyncExternalStore } from 'react';
|
|
38
38
|
import { Linking, Platform } from 'react-native';
|
|
39
39
|
import { toast } from '@oxy.so/bloom/toast';
|
|
40
40
|
import { surfaces } from '@oxy.so/bloom/surfaces';
|
|
41
41
|
import { useTheme } from '@oxy.so/bloom/theme';
|
|
42
|
-
import {
|
|
42
|
+
import { IDENTITY_WEB_ORIGIN, getNormalizedUserHandle, type User } from '@oxy.so/core';
|
|
43
43
|
import { useQueryClient } from '@tanstack/react-query';
|
|
44
44
|
import { useOxy } from '../context/OxyContext';
|
|
45
45
|
import { useDeviceSwitcher } from '../hooks/useDeviceSwitcher';
|
|
@@ -55,6 +55,7 @@ import AccountsMenuView from './authChooser/AccountsMenuView';
|
|
|
55
55
|
import SignInEntryView from './authChooser/SignInEntryView';
|
|
56
56
|
import SignInRequestView from './authChooser/SignInRequestView';
|
|
57
57
|
import SignUpView from './authChooser/SignUpView';
|
|
58
|
+
import { signInFailureMessage } from './authChooser/signInFailureMessage';
|
|
58
59
|
import {
|
|
59
60
|
resolveAccentHex,
|
|
60
61
|
type AccountHeroModel,
|
|
@@ -76,14 +77,23 @@ const COMMONS_CREATE_IDENTITY_URL = 'oxycommons://create-identity';
|
|
|
76
77
|
*/
|
|
77
78
|
const ACCOUNTS_APP_URL = 'https://accounts.oxy.so';
|
|
78
79
|
|
|
80
|
+
/**
|
|
81
|
+
* The last sign-in attempt whose failure was toasted, per controller.
|
|
82
|
+
*
|
|
83
|
+
* Module-level, not per subscription: a failure is ONE event. Remounting the
|
|
84
|
+
* chooser (or re-subscribing to the same controller) must not report it again,
|
|
85
|
+
* while a NEW attempt that fails with the very same message must. The attempt
|
|
86
|
+
* identity is what tells those apart; the message text cannot.
|
|
87
|
+
*/
|
|
88
|
+
const toastedFailureAttempt = new WeakMap<object, number>();
|
|
89
|
+
|
|
79
90
|
export interface OxyAuthChooserProps {
|
|
80
91
|
/** Called after a completed switch, sign-in, or sign-up. */
|
|
81
92
|
onComplete?: () => void;
|
|
82
93
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* @default true
|
|
94
|
+
* @deprecated No effect. The web sign-in entry no longer auto-starts a
|
|
95
|
+
* device-flow request: its primary action opens the identity origin
|
|
96
|
+
* (`id.oxy.so`) in a popup, which a browser only allows from a user gesture.
|
|
87
97
|
*/
|
|
88
98
|
autoStartSignIn?: boolean;
|
|
89
99
|
}
|
|
@@ -93,15 +103,10 @@ export interface OxyAuthChooserProps {
|
|
|
93
103
|
* (wrapped in Bloom's `<Dialog>`) today; mountable bare by any future host that
|
|
94
104
|
* supplies its own `onComplete`.
|
|
95
105
|
*/
|
|
96
|
-
const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
|
|
97
|
-
onComplete,
|
|
98
|
-
autoStartSignIn: autoStartSignInEnabled = true,
|
|
99
|
-
}) => {
|
|
106
|
+
const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({ onComplete }) => {
|
|
100
107
|
const {
|
|
101
108
|
accountDialogController: controller,
|
|
102
109
|
showBottomSheet,
|
|
103
|
-
signInWithPasskey,
|
|
104
|
-
registerWithPasskey,
|
|
105
110
|
oxyServices,
|
|
106
111
|
logout,
|
|
107
112
|
openAvatarPicker,
|
|
@@ -115,114 +120,39 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
|
|
|
115
120
|
// so the two switchers cannot drift.
|
|
116
121
|
const { principals } = useDeviceSwitcher();
|
|
117
122
|
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
// back via `AccountDialogController.startPasskeyHubSignIn` ('hub'). Native
|
|
125
|
-
// has neither: Commons owns identity there ('none'). `isOxyRpOrigin()` reads
|
|
126
|
-
// `location` once and is stable for the component's lifetime.
|
|
127
|
-
const passkeyMode = useMemo<PasskeyMode>(() => {
|
|
128
|
-
if (!isWebBrowser()) return 'none';
|
|
129
|
-
return isOxyRpOrigin() ? 'direct' : 'hub';
|
|
130
|
-
}, []);
|
|
131
|
-
|
|
132
|
-
const [signInPasskeyPending, setSignInPasskeyPending] = useState(false);
|
|
133
|
-
const handleSignInWithPasskey = useCallback(async () => {
|
|
134
|
-
if (signInPasskeyPending) return;
|
|
135
|
-
setSignInPasskeyPending(true);
|
|
136
|
-
try {
|
|
137
|
-
await signInWithPasskey();
|
|
138
|
-
onComplete?.();
|
|
139
|
-
} catch (error) {
|
|
140
|
-
// A cancelled/failed ceremony keeps the view open so the user can retry
|
|
141
|
-
// or fall back to the QR — surface the reason as a toast (owner mandate:
|
|
142
|
-
// errors never render inline inside the dialog), never swallow.
|
|
143
|
-
toast.error(
|
|
144
|
-
error instanceof Error && error.message
|
|
145
|
-
? error.message
|
|
146
|
-
: t('accountSwitcher.toasts.passkeySignInFailed'),
|
|
147
|
-
);
|
|
148
|
-
} finally {
|
|
149
|
-
setSignInPasskeyPending(false);
|
|
150
|
-
}
|
|
151
|
-
}, [signInPasskeyPending, signInWithPasskey, onComplete, t]);
|
|
152
|
-
|
|
153
|
-
const [createPasskeyPending, setCreatePasskeyPending] = useState(false);
|
|
154
|
-
const handleCreateWithPasskey = useCallback(
|
|
155
|
-
async (username: string) => {
|
|
156
|
-
if (createPasskeyPending) return;
|
|
157
|
-
setCreatePasskeyPending(true);
|
|
158
|
-
try {
|
|
159
|
-
await registerWithPasskey({ username });
|
|
160
|
-
onComplete?.();
|
|
161
|
-
} catch (error) {
|
|
162
|
-
toast.error(
|
|
163
|
-
error instanceof Error && error.message
|
|
164
|
-
? error.message
|
|
165
|
-
: t('signup.toasts.passkeyCreateFailed'),
|
|
166
|
-
);
|
|
167
|
-
} finally {
|
|
168
|
-
setCreatePasskeyPending(false);
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
[createPasskeyPending, registerWithPasskey, onComplete, t],
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
// Web: auto-start "Sign in with Oxy" when the sign-in entry becomes the shown
|
|
175
|
-
// view — the request surface is the PRIMARY web route, not something behind a
|
|
176
|
-
// second tap (the user's ONE action was the sign-in button that opened this).
|
|
177
|
-
// `signInWithOxy()` tries the shared keychain first (silently, native-only)
|
|
178
|
-
// then falls to the device flow, whose route the controller picks. Driven from
|
|
179
|
-
// the EVENTS that reach that view — the initial `subscribe` pass below (an
|
|
180
|
-
// open-to-sign-in) and the add / back-to-sign-in handlers — never a watcher
|
|
181
|
-
// effect. Guarded so it never restarts a live flow (phase must be idle) and is
|
|
182
|
-
// a no-op on native (button-driven). Reads the live snapshot at call time (an
|
|
183
|
-
// event callback, not render), so it is stable across renders.
|
|
184
|
-
const autoStartSignIn = useCallback(() => {
|
|
185
|
-
if (!autoStartSignInEnabled || !controller || !isWebBrowser()) return;
|
|
186
|
-
const { view: currentView, signIn } = controller.getSnapshot();
|
|
187
|
-
if ((currentView !== 'signin' && currentView !== 'add') || signIn.phase !== 'idle') return;
|
|
188
|
-
void controller.signInWithOxy();
|
|
189
|
-
}, [autoStartSignInEnabled, controller]);
|
|
123
|
+
// Web sign-in and sign-up run at the identity origin (`id.oxy.so`), never
|
|
124
|
+
// on this page, whatever this page's origin: that is where the passkey
|
|
125
|
+
// ceremony can run for any app AND where the account's identity is created
|
|
126
|
+
// and kept sealed under the passkey (one identity, two carriers). Native has
|
|
127
|
+
// no passkey path: Commons owns identity there ('none').
|
|
128
|
+
const passkeyMode = useMemo<PasskeyMode>(() => (isWebBrowser() ? 'hub' : 'none'), []);
|
|
190
129
|
|
|
191
130
|
// Bind the headless controller. `getSnapshot` returns a stable reference
|
|
192
131
|
// between changes, so it is `useSyncExternalStore`-safe. Guard the no-provider
|
|
193
132
|
// loading state (`controller` is `null`) with an inert store.
|
|
194
133
|
//
|
|
195
|
-
// The subscribe callback ALSO
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
// `signIn` asynchronously) are toasted at the notification site, deduped
|
|
200
|
-
// per subscription — NEVER an inline banner in the request view (owner
|
|
201
|
-
// mandate, same contract as account-switch).
|
|
202
|
-
// 2. On subscribe (mount / re-subscribe — a passive effect, the SAME timing
|
|
203
|
-
// the removed effects had) the web sign-in flow auto-starts if the opening
|
|
204
|
-
// view is the sign-in entry.
|
|
134
|
+
// The subscribe callback ALSO toasts sign-in device-flow failures (poll /
|
|
135
|
+
// socket / popup-cancel land on `signIn` asynchronously) at the notification
|
|
136
|
+
// site — an EVENT callback, not render/effect — deduped per attempt, NEVER an
|
|
137
|
+
// inline banner in the request view (owner mandate).
|
|
205
138
|
const subscribe = useCallback(
|
|
206
139
|
(listener: () => void) => {
|
|
207
140
|
if (!controller) return () => undefined;
|
|
208
|
-
let lastToastedSignInError: string | null = null;
|
|
209
141
|
const maybeToastSignInError = () => {
|
|
210
142
|
const { signIn } = controller.getSnapshot();
|
|
211
|
-
if (signIn.phase
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
143
|
+
if (signIn.phase !== 'error') return;
|
|
144
|
+
if (toastedFailureAttempt.get(controller) === signIn.attempt) return;
|
|
145
|
+
toastedFailureAttempt.set(controller, signIn.attempt);
|
|
146
|
+
const message = signInFailureMessage(signIn.failure, t);
|
|
147
|
+
if (message) toast.error(message);
|
|
217
148
|
};
|
|
218
149
|
maybeToastSignInError();
|
|
219
|
-
autoStartSignIn();
|
|
220
150
|
return controller.subscribe(() => {
|
|
221
151
|
maybeToastSignInError();
|
|
222
152
|
listener();
|
|
223
153
|
});
|
|
224
154
|
},
|
|
225
|
-
[controller,
|
|
155
|
+
[controller, t],
|
|
226
156
|
);
|
|
227
157
|
const getSnapshot = useCallback(
|
|
228
158
|
() => (controller ? controller.getSnapshot() : EMPTY_ACCOUNT_DIALOG_SNAPSHOT),
|
|
@@ -239,31 +169,30 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
|
|
|
239
169
|
const handleActivate = useCallback(
|
|
240
170
|
async (contextId: string) => {
|
|
241
171
|
if (!controller) return;
|
|
242
|
-
|
|
172
|
+
// Another switch or removal is already changing this device: the press is
|
|
173
|
+
// dropped, not queued — and it is not a failure to report.
|
|
174
|
+
if (controller.isDeviceMutationInFlight()) return;
|
|
243
175
|
// Already live: activating it again bumps nothing server-side, so treat
|
|
244
176
|
// the press as "yes, this one" and close.
|
|
245
177
|
if (contextId === controller.getSnapshot().activeContext?.contextId) {
|
|
246
178
|
onComplete?.();
|
|
247
179
|
return;
|
|
248
180
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
toast.error(t('accountSwitcher.toasts.activateFailed'));
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
// The subject changed, so every account-scoped query is now about
|
|
259
|
-
// somebody else. The runtime resets its own caches between the bearer
|
|
260
|
-
// commit and the notify; this drops the Query cache the same way the
|
|
261
|
-
// account switch did.
|
|
262
|
-
queryClient.invalidateQueries();
|
|
263
|
-
onComplete?.();
|
|
264
|
-
} catch {
|
|
181
|
+
// The controller's own verdict on the SWITCH — never `snapshot.error`,
|
|
182
|
+
// which the directory re-read after a successful switch can also set.
|
|
183
|
+
// Reading that as "the switch failed" left the dialog open on a false
|
|
184
|
+
// error with the previous account's queries still cached.
|
|
185
|
+
const switched = await controller.activateContext(contextId).catch(() => false);
|
|
186
|
+
if (!switched) {
|
|
265
187
|
toast.error(t('accountSwitcher.toasts.activateFailed'));
|
|
188
|
+
return;
|
|
266
189
|
}
|
|
190
|
+
// The subject changed, so every account-scoped query is now about
|
|
191
|
+
// somebody else. The runtime resets its own caches between the bearer
|
|
192
|
+
// commit and the notify; this drops the Query cache the same way the
|
|
193
|
+
// account switch did.
|
|
194
|
+
queryClient.invalidateQueries();
|
|
195
|
+
onComplete?.();
|
|
267
196
|
},
|
|
268
197
|
[controller, onComplete, queryClient, t],
|
|
269
198
|
);
|
|
@@ -278,7 +207,7 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
|
|
|
278
207
|
*/
|
|
279
208
|
const handleRemoveContext = useCallback(
|
|
280
209
|
async (contextId: string) => {
|
|
281
|
-
if (!controller) return;
|
|
210
|
+
if (!controller || controller.isDeviceMutationInFlight()) return;
|
|
282
211
|
const group = principals.find((principal) =>
|
|
283
212
|
principal.contexts.some((context) => context.contextId === contextId),
|
|
284
213
|
);
|
|
@@ -308,7 +237,7 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
|
|
|
308
237
|
/** Remove ONE PERSON and every account they reach here, and nobody else's. */
|
|
309
238
|
const handleRemovePrincipal = useCallback(
|
|
310
239
|
async (principalId: string) => {
|
|
311
|
-
if (!controller) return;
|
|
240
|
+
if (!controller || controller.isDeviceMutationInFlight()) return;
|
|
312
241
|
const group = principals.find((principal) => principal.principalId === principalId);
|
|
313
242
|
if (!group) return;
|
|
314
243
|
const confirmed = await surfaces.confirm({
|
|
@@ -329,6 +258,42 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
|
|
|
329
258
|
[controller, principals, t],
|
|
330
259
|
);
|
|
331
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Sign out of the current account, and close only once that is TRUE.
|
|
263
|
+
*
|
|
264
|
+
* Runs under the controller's device-mutation gate, so it can neither race a
|
|
265
|
+
* switch or removal nor be issued twice by a double press. A failed
|
|
266
|
+
* revocation keeps the dialog open and says so: closing it would read as
|
|
267
|
+
* "signed out" while this device still holds the session.
|
|
268
|
+
*/
|
|
269
|
+
const handleSignOut = useCallback(async () => {
|
|
270
|
+
if (!controller) return;
|
|
271
|
+
const outcome = await controller.runDeviceMutation(() => logout());
|
|
272
|
+
if (!outcome.ran) return;
|
|
273
|
+
if (outcome.value.status === 'failed') {
|
|
274
|
+
toast.error(t('common.errors.signOutFailed'));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
onComplete?.();
|
|
278
|
+
}, [controller, logout, onComplete, t]);
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Hand a URL to the OS. `Linking.openURL` REJECTS when nothing can open it
|
|
282
|
+
* (no browser, an unregistered scheme, a blocked navigation) — a press that
|
|
283
|
+
* then does nothing at all is exactly the dead end this reports instead.
|
|
284
|
+
*/
|
|
285
|
+
const openExternal = useCallback(
|
|
286
|
+
(url: string) => {
|
|
287
|
+
// Wrapped so a synchronous throw lands in the same handler as a rejection.
|
|
288
|
+
Promise.resolve()
|
|
289
|
+
.then(() => Linking.openURL(url))
|
|
290
|
+
.catch(() => {
|
|
291
|
+
toast.error(t('accountSwitcher.linkOpenFailed'));
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
[t],
|
|
295
|
+
);
|
|
296
|
+
|
|
332
297
|
const handleManage = useCallback(() => {
|
|
333
298
|
onComplete?.();
|
|
334
299
|
if (consumerHooks?.onNavigateManage) {
|
|
@@ -344,11 +309,9 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
|
|
|
344
309
|
consumerHooks.onAddAccount();
|
|
345
310
|
return;
|
|
346
311
|
}
|
|
347
|
-
// No consumer override: enter the "add account" view
|
|
348
|
-
// sign-in flow, from this handler (the event that reaches the view).
|
|
312
|
+
// No consumer override: enter the "add account" view.
|
|
349
313
|
controller?.add();
|
|
350
|
-
|
|
351
|
-
}, [consumerHooks, controller, autoStartSignIn, onComplete]);
|
|
314
|
+
}, [consumerHooks, controller, onComplete]);
|
|
352
315
|
|
|
353
316
|
const handlers = useMemo<OxyAuthChooserHandlers>(
|
|
354
317
|
() => ({
|
|
@@ -390,23 +353,18 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
|
|
|
390
353
|
const alternatives = useMemo<SignInAlternatives>(
|
|
391
354
|
() => ({
|
|
392
355
|
passkeyAvailable: passkeyMode !== 'none',
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
// component-local pending flag. A ceremony FAILURE is toasted by
|
|
396
|
-
// `handleSignInWithPasskey`, never rendered inline.
|
|
397
|
-
passkeyPending: passkeyMode === 'direct' ? signInPasskeyPending : false,
|
|
398
|
-
onSignInWithPasskey: () => {
|
|
399
|
-
if (passkeyMode === 'direct') {
|
|
400
|
-
void handleSignInWithPasskey();
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
void controller?.startPasskeyHubSignIn();
|
|
404
|
-
},
|
|
356
|
+
// Called straight from the press: the popup opens before any await.
|
|
357
|
+
onSignInWithPasskey: () => void controller?.startPasskeyHubSignIn(),
|
|
405
358
|
onShowQr: () => void controller?.showQr(),
|
|
406
|
-
onGetCommons: () =>
|
|
407
|
-
|
|
359
|
+
onGetCommons: () => openExternal(getCommonsAcquisitionUrl(Platform.OS)),
|
|
360
|
+
// Web: the identity origin creates the account and its identity in the
|
|
361
|
+
// same window a sign-in uses. Native: Commons creates the identity.
|
|
362
|
+
onCreateAccount: () => {
|
|
363
|
+
if (passkeyMode === 'hub') void controller?.startPasskeyHubSignIn();
|
|
364
|
+
else controller?.startSignup();
|
|
365
|
+
},
|
|
408
366
|
}),
|
|
409
|
-
[passkeyMode,
|
|
367
|
+
[passkeyMode, controller, openExternal],
|
|
410
368
|
);
|
|
411
369
|
|
|
412
370
|
// Real storage usage for the account menu's "Oxy storage" block. Disabled
|
|
@@ -430,9 +388,7 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
|
|
|
430
388
|
// endpoints — the same handoff pattern `handleManage`/`getCommonsAcquisitionUrl`
|
|
431
389
|
// already use.
|
|
432
390
|
const accountMenu = useMemo<AccountsMenuActions>(() => {
|
|
433
|
-
const openUrl =
|
|
434
|
-
void Linking.openURL(url);
|
|
435
|
-
};
|
|
391
|
+
const openUrl = openExternal;
|
|
436
392
|
const openSheet = (config: Parameters<NonNullable<typeof showBottomSheet>>[0]) => {
|
|
437
393
|
onComplete?.();
|
|
438
394
|
showBottomSheet?.(config);
|
|
@@ -446,9 +402,9 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
|
|
|
446
402
|
onPrivacy: () => openSheet({ screen: 'LegalDocuments', props: { initialStep: 1 } }),
|
|
447
403
|
onTerms: () => openSheet({ screen: 'LegalDocuments', props: { initialStep: 2 } }),
|
|
448
404
|
onSignOut: () => {
|
|
449
|
-
void
|
|
450
|
-
onComplete?.();
|
|
405
|
+
void handleSignOut();
|
|
451
406
|
},
|
|
407
|
+
onOpenIdentity: passkeyMode === 'hub' ? () => openUrl(`${IDENTITY_WEB_ORIGIN}/`) : undefined,
|
|
452
408
|
customItems: (consumerHooks?.menuItems ?? []).map((item) => ({
|
|
453
409
|
...item,
|
|
454
410
|
onPress: () => {
|
|
@@ -457,7 +413,7 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
|
|
|
457
413
|
},
|
|
458
414
|
})),
|
|
459
415
|
};
|
|
460
|
-
}, [consumerHooks, onComplete, showBottomSheet,
|
|
416
|
+
}, [consumerHooks, onComplete, showBottomSheet, handleSignOut, openExternal, passkeyMode]);
|
|
461
417
|
|
|
462
418
|
if (!controller) {
|
|
463
419
|
return null;
|
|
@@ -483,7 +439,7 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
|
|
|
483
439
|
<SignInRequestView
|
|
484
440
|
snapshot={snapshot}
|
|
485
441
|
t={t}
|
|
486
|
-
onRetry={() => void controller.
|
|
442
|
+
onRetry={() => void controller.retrySignIn()}
|
|
487
443
|
alternatives={alternatives}
|
|
488
444
|
/>
|
|
489
445
|
);
|
|
@@ -495,17 +451,11 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({
|
|
|
495
451
|
snapshot={snapshot}
|
|
496
452
|
theme={theme}
|
|
497
453
|
t={t}
|
|
498
|
-
oxyServices={oxyServices}
|
|
499
454
|
passkeyMode={passkeyMode}
|
|
500
|
-
onCreateWithPasskey={(username) => void handleCreateWithPasskey(username)}
|
|
501
|
-
createPending={createPasskeyPending}
|
|
502
455
|
onOpenHub={() => void controller.startPasskeyHubSignIn()}
|
|
503
|
-
onCreateIdentityInCommons={() =>
|
|
456
|
+
onCreateIdentityInCommons={() => openExternal(COMMONS_CREATE_IDENTITY_URL)}
|
|
504
457
|
onGetCommons={alternatives.onGetCommons}
|
|
505
|
-
onBackToSignIn={() =>
|
|
506
|
-
controller.setView('signin');
|
|
507
|
-
autoStartSignIn();
|
|
508
|
-
}}
|
|
458
|
+
onBackToSignIn={() => controller.setView('signin')}
|
|
509
459
|
/>
|
|
510
460
|
);
|
|
511
461
|
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { useCallback } from 'react';
|
|
22
22
|
import { Linking, Pressable, ScrollView, StyleSheet, View } from 'react-native';
|
|
23
23
|
import { logger } from '@oxy.so/core';
|
|
24
|
+
import type { McpOAuthWriteAction } from '@oxy.so/contracts';
|
|
24
25
|
import { Avatar } from '@oxy.so/bloom/avatar';
|
|
25
26
|
import { Button } from '@oxy.so/bloom/button';
|
|
26
27
|
import { Text } from '@oxy.so/bloom/typography';
|
|
@@ -48,6 +49,8 @@ export interface OxyConsentApplication {
|
|
|
48
49
|
|
|
49
50
|
/** The account that will authorize the request (the currently signed-in user). */
|
|
50
51
|
export interface OxyConsentUser {
|
|
52
|
+
/** Exact effective account bound to the grant. */
|
|
53
|
+
accountId?: string;
|
|
51
54
|
/** Real display name, when the profile has one. */
|
|
52
55
|
displayName?: string;
|
|
53
56
|
/** Normalized handle — the sanctioned fallback when `displayName` is absent. */
|
|
@@ -56,6 +59,18 @@ export interface OxyConsentUser {
|
|
|
56
59
|
avatarUri?: string;
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
export interface OxyConsentResource {
|
|
63
|
+
/** Oxy application that owns the protected MCP resource. */
|
|
64
|
+
application: {
|
|
65
|
+
name: string;
|
|
66
|
+
iconUrl?: string;
|
|
67
|
+
};
|
|
68
|
+
/** Canonical protected-resource URI bound into the access token. */
|
|
69
|
+
uri: string;
|
|
70
|
+
/** Effectful tools enabled by the requested capabilities. */
|
|
71
|
+
writeActions: McpOAuthWriteAction[];
|
|
72
|
+
}
|
|
73
|
+
|
|
59
74
|
export interface OxyConsentScreenProps {
|
|
60
75
|
/** The resolved requesting application. */
|
|
61
76
|
application: OxyConsentApplication;
|
|
@@ -63,6 +78,8 @@ export interface OxyConsentScreenProps {
|
|
|
63
78
|
scopes: string[];
|
|
64
79
|
/** The account that will authorize the request, when known. */
|
|
65
80
|
user?: OxyConsentUser;
|
|
81
|
+
/** Exact protected resource for an external MCP connection. */
|
|
82
|
+
resource?: OxyConsentResource;
|
|
66
83
|
/** Approve handler — the caller mints the code / completes the flow. */
|
|
67
84
|
onAllow: () => void | Promise<void>;
|
|
68
85
|
/** Deny handler — the caller cancels the flow. */
|
|
@@ -192,6 +209,7 @@ export function OxyConsentScreen({
|
|
|
192
209
|
application,
|
|
193
210
|
scopes,
|
|
194
211
|
user,
|
|
212
|
+
resource,
|
|
195
213
|
onAllow,
|
|
196
214
|
onDeny,
|
|
197
215
|
busy = false,
|
|
@@ -259,6 +277,40 @@ export function OxyConsentScreen({
|
|
|
259
277
|
) : null}
|
|
260
278
|
</View>
|
|
261
279
|
|
|
280
|
+
{resource ? (
|
|
281
|
+
<View style={styles.section}>
|
|
282
|
+
<Text style={[styles.sectionLabel, { color: theme.colors.textSecondary }]}>
|
|
283
|
+
{t('consent.resource.title')}
|
|
284
|
+
</Text>
|
|
285
|
+
<View
|
|
286
|
+
testID="consent-resource"
|
|
287
|
+
style={[styles.resourceCard, { borderColor: theme.colors.border, backgroundColor: theme.colors.card }]}
|
|
288
|
+
>
|
|
289
|
+
<Avatar
|
|
290
|
+
source={resource.application.iconUrl}
|
|
291
|
+
name={resource.application.name}
|
|
292
|
+
size={40}
|
|
293
|
+
/>
|
|
294
|
+
<View style={styles.resourceText}>
|
|
295
|
+
<Text
|
|
296
|
+
testID="consent-resource-app"
|
|
297
|
+
numberOfLines={1}
|
|
298
|
+
style={[styles.accountName, { color: theme.colors.text }]}
|
|
299
|
+
>
|
|
300
|
+
{resource.application.name}
|
|
301
|
+
</Text>
|
|
302
|
+
<Text
|
|
303
|
+
testID="consent-resource-uri"
|
|
304
|
+
numberOfLines={2}
|
|
305
|
+
style={[styles.resourceUri, { color: theme.colors.textSecondary }]}
|
|
306
|
+
>
|
|
307
|
+
{resource.uri}
|
|
308
|
+
</Text>
|
|
309
|
+
</View>
|
|
310
|
+
</View>
|
|
311
|
+
</View>
|
|
312
|
+
) : null}
|
|
313
|
+
|
|
262
314
|
{/* Requested permissions */}
|
|
263
315
|
<View style={styles.section}>
|
|
264
316
|
<Text style={[styles.sectionLabel, { color: theme.colors.textSecondary }]}>
|
|
@@ -285,6 +337,29 @@ export function OxyConsentScreen({
|
|
|
285
337
|
</View>
|
|
286
338
|
</View>
|
|
287
339
|
|
|
340
|
+
{resource && resource.writeActions.length > 0 ? (
|
|
341
|
+
<View style={styles.section}>
|
|
342
|
+
<Text style={[styles.sectionLabel, { color: theme.colors.textSecondary }]}>
|
|
343
|
+
{t('consent.writeActions.title')}
|
|
344
|
+
</Text>
|
|
345
|
+
<View style={[styles.card, { borderColor: theme.colors.border, backgroundColor: theme.colors.card }]}>
|
|
346
|
+
{resource.writeActions.map((action) => (
|
|
347
|
+
<View key={`${action.name}:${action.version}`} testID={`consent-write-action-${action.name}`} style={styles.actionRow}>
|
|
348
|
+
<View style={styles.actionCopy}>
|
|
349
|
+
<Text style={[styles.rowText, { color: theme.colors.text }]}>{action.description}</Text>
|
|
350
|
+
<Text style={[styles.actionName, { color: theme.colors.textSecondary }]}>{action.name}</Text>
|
|
351
|
+
</View>
|
|
352
|
+
<View style={[styles.effectBadge, { borderColor: theme.colors.border }]}>
|
|
353
|
+
<Text style={[styles.effectText, { color: theme.colors.textSecondary }]}>
|
|
354
|
+
{t(`consent.effects.${action.effect}`)}
|
|
355
|
+
</Text>
|
|
356
|
+
</View>
|
|
357
|
+
</View>
|
|
358
|
+
))}
|
|
359
|
+
</View>
|
|
360
|
+
</View>
|
|
361
|
+
) : null}
|
|
362
|
+
|
|
288
363
|
{/* Authorizing account */}
|
|
289
364
|
{user ? (
|
|
290
365
|
<View
|
|
@@ -305,6 +380,15 @@ export function OxyConsentScreen({
|
|
|
305
380
|
{user.handle}
|
|
306
381
|
</Text>
|
|
307
382
|
) : null}
|
|
383
|
+
{user.accountId ? (
|
|
384
|
+
<Text
|
|
385
|
+
testID="consent-account-id"
|
|
386
|
+
numberOfLines={1}
|
|
387
|
+
style={[styles.accountId, { color: theme.colors.textSecondary }]}
|
|
388
|
+
>
|
|
389
|
+
{t('consent.account.id', { id: user.accountId })}
|
|
390
|
+
</Text>
|
|
391
|
+
) : null}
|
|
308
392
|
</View>
|
|
309
393
|
</View>
|
|
310
394
|
) : null}
|
|
@@ -466,6 +550,49 @@ const styles = StyleSheet.create({
|
|
|
466
550
|
accountHandle: {
|
|
467
551
|
fontSize: 13,
|
|
468
552
|
},
|
|
553
|
+
accountId: {
|
|
554
|
+
fontSize: 11,
|
|
555
|
+
},
|
|
556
|
+
resourceCard: {
|
|
557
|
+
flexDirection: 'row',
|
|
558
|
+
alignItems: 'center',
|
|
559
|
+
gap: 12,
|
|
560
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
561
|
+
borderRadius: 16,
|
|
562
|
+
padding: 12,
|
|
563
|
+
},
|
|
564
|
+
resourceText: {
|
|
565
|
+
flex: 1,
|
|
566
|
+
minWidth: 0,
|
|
567
|
+
gap: 2,
|
|
568
|
+
},
|
|
569
|
+
resourceUri: {
|
|
570
|
+
fontSize: 12,
|
|
571
|
+
lineHeight: 17,
|
|
572
|
+
},
|
|
573
|
+
actionRow: {
|
|
574
|
+
flexDirection: 'row',
|
|
575
|
+
alignItems: 'center',
|
|
576
|
+
gap: 10,
|
|
577
|
+
},
|
|
578
|
+
actionCopy: {
|
|
579
|
+
flex: 1,
|
|
580
|
+
minWidth: 0,
|
|
581
|
+
gap: 2,
|
|
582
|
+
},
|
|
583
|
+
actionName: {
|
|
584
|
+
fontSize: 11,
|
|
585
|
+
},
|
|
586
|
+
effectBadge: {
|
|
587
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
588
|
+
borderRadius: 999,
|
|
589
|
+
paddingHorizontal: 8,
|
|
590
|
+
paddingVertical: 4,
|
|
591
|
+
},
|
|
592
|
+
effectText: {
|
|
593
|
+
fontSize: 10,
|
|
594
|
+
fontWeight: '600',
|
|
595
|
+
},
|
|
469
596
|
linksRow: {
|
|
470
597
|
flexDirection: 'row',
|
|
471
598
|
flexWrap: 'wrap',
|