@oxyhq/services 13.0.3 → 13.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 +18 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/ProfileButton.js +233 -0
- package/lib/commonjs/ui/components/ProfileButton.js.map +1 -0
- package/lib/commonjs/ui/components/ProfileMenu.js +374 -0
- package/lib/commonjs/ui/components/ProfileMenu.js.map +1 -0
- package/lib/commonjs/ui/context/OxyContext.js +99 -18
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +28 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/index.js +14 -0
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/utils/activeAuthuser.js +76 -8
- package/lib/commonjs/ui/utils/activeAuthuser.js.map +1 -1
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/ProfileButton.js +228 -0
- package/lib/module/ui/components/ProfileButton.js.map +1 -0
- package/lib/module/ui/components/ProfileMenu.js +370 -0
- package/lib/module/ui/components/ProfileMenu.js.map +1 -0
- package/lib/module/ui/context/OxyContext.js +100 -19
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +29 -2
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/index.js +2 -0
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/utils/activeAuthuser.js +74 -9
- package/lib/module/ui/utils/activeAuthuser.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +4 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts +46 -0
- package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +39 -0
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/index.d.ts +2 -0
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts +24 -0
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +4 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ProfileButton.d.ts +46 -0
- package/lib/typescript/module/ui/components/ProfileButton.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts +39 -0
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +1 -0
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/index.d.ts +2 -0
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts +24 -0
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +8 -0
- package/src/ui/components/ProfileButton.tsx +270 -0
- package/src/ui/components/ProfileMenu.tsx +416 -0
- package/src/ui/context/OxyContext.tsx +100 -18
- package/src/ui/context/hooks/useAuthOperations.ts +35 -2
- package/src/ui/index.ts +2 -0
- package/src/ui/utils/__tests__/activeAuthuser.test.ts +75 -0
- package/src/ui/utils/activeAuthuser.ts +74 -8
package/src/index.ts
CHANGED
|
@@ -211,6 +211,14 @@ export type { AccountMenuProps, AccountMenuAnchor } from './ui/components/Accoun
|
|
|
211
211
|
export { default as AccountMenuButton } from './ui/components/AccountMenuButton';
|
|
212
212
|
export type { AccountMenuButtonProps } from './ui/components/AccountMenuButton';
|
|
213
213
|
|
|
214
|
+
// Sidebar account button + its device-account switcher menu (Bluesky-style
|
|
215
|
+
// ProfileCard). `ProfileButton` owns its own open state + anchor measurement
|
|
216
|
+
// and opens `ProfileMenu` upward from a sidebar footer.
|
|
217
|
+
export { default as ProfileButton } from './ui/components/ProfileButton';
|
|
218
|
+
export type { ProfileButtonProps } from './ui/components/ProfileButton';
|
|
219
|
+
export { default as ProfileMenu } from './ui/components/ProfileMenu';
|
|
220
|
+
export type { ProfileMenuProps, ProfileMenuAnchor } from './ui/components/ProfileMenu';
|
|
221
|
+
|
|
214
222
|
// Unified account switcher (device sign-ins + account graph). `AccountMenuButton`
|
|
215
223
|
// opens this; the `AccountSwitcherView` body also backs the `AccountSwitcher`
|
|
216
224
|
// bottom-sheet route.
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { useCallback, useRef, useState } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
View,
|
|
5
|
+
Pressable,
|
|
6
|
+
Platform,
|
|
7
|
+
type StyleProp,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
11
|
+
import { Avatar } from '@oxyhq/bloom/avatar';
|
|
12
|
+
import { Text } from '@oxyhq/bloom/typography';
|
|
13
|
+
import { useTheme } from '@oxyhq/bloom/theme';
|
|
14
|
+
import { getAccountDisplayName, getAccountFallbackHandle } from '@oxyhq/core';
|
|
15
|
+
import { useAuth } from '../hooks/useAuth';
|
|
16
|
+
import { useI18n } from '../hooks/useI18n';
|
|
17
|
+
import ProfileMenu, { type ProfileMenuAnchor } from './ProfileMenu';
|
|
18
|
+
|
|
19
|
+
const isWeb = Platform.OS === 'web';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Fixed popover width used by {@link ProfileMenu} on web. `ProfileButton`
|
|
23
|
+
* mirrors this constant when computing the trigger-relative anchor so the
|
|
24
|
+
* left-edge clamp keeps the panel fully on-screen. Keep the two in sync.
|
|
25
|
+
*/
|
|
26
|
+
const MENU_WIDTH = 300;
|
|
27
|
+
|
|
28
|
+
/** Gutter, in px, kept between the popover and the viewport edges on web. */
|
|
29
|
+
const VIEWPORT_GUTTER = 8;
|
|
30
|
+
|
|
31
|
+
export interface ProfileButtonProps {
|
|
32
|
+
/**
|
|
33
|
+
* Expanded row (avatar + name + handle + chevron) when `true` (default), or a
|
|
34
|
+
* bare avatar-only trigger when `false` (collapsed sidebar).
|
|
35
|
+
*/
|
|
36
|
+
expanded?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Avatar diameter in px. Defaults to 40 when expanded, 32 when collapsed.
|
|
39
|
+
*/
|
|
40
|
+
avatarSize?: number;
|
|
41
|
+
/** Navigate to the "Manage account" surface (settings). */
|
|
42
|
+
onNavigateManage: () => void;
|
|
43
|
+
/** Start the add-account / sign-in flow for an additional account. */
|
|
44
|
+
onAddAccount: () => void;
|
|
45
|
+
/** Optional: navigate to the signed-in user's own profile. */
|
|
46
|
+
onNavigateProfile?: () => void;
|
|
47
|
+
/** Called before the active identity changes so apps can clear scoped state. */
|
|
48
|
+
onBeforeSessionChange?: () => void | Promise<void>;
|
|
49
|
+
/** Extra className applied to the outer trigger. */
|
|
50
|
+
className?: string;
|
|
51
|
+
/** Extra style applied to the outer trigger. */
|
|
52
|
+
style?: StyleProp<ViewStyle>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Self-contained sidebar account trigger, modeled on Bluesky's ProfileCard and
|
|
57
|
+
* the Oxy inbox's `MailboxDrawer` footer. Owns its own open state + anchor
|
|
58
|
+
* measurement, and renders {@link ProfileMenu} (the device-account switcher).
|
|
59
|
+
*
|
|
60
|
+
* Three auth states from {@link useAuth}:
|
|
61
|
+
* - **Undetermined** (`!isAuthResolved || isPrivateApiPending`): a neutral
|
|
62
|
+
* avatar-sized skeleton circle, no press.
|
|
63
|
+
* - **Signed in**: a pressable row — Bloom `Avatar` + display name + `@handle`
|
|
64
|
+
* + an `unfold-more-horizontal` chevron. Press opens the menu upward (web)
|
|
65
|
+
* against the measured trigger, or as a bottom sheet (native).
|
|
66
|
+
* - **Signed out**: a "Sign in" row that calls `useAuth().signIn()`.
|
|
67
|
+
*
|
|
68
|
+
* All display strings resolve through `@oxyhq/core`'s
|
|
69
|
+
* `getAccountDisplayName` / `getAccountFallbackHandle` — no hand-rolled name
|
|
70
|
+
* fallbacks. Avatars pass the bare file id as `source` + `variant="thumb"`, so
|
|
71
|
+
* the app's registered `ImageResolver` builds the thumbnail URL.
|
|
72
|
+
*/
|
|
73
|
+
const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
74
|
+
expanded = true,
|
|
75
|
+
avatarSize,
|
|
76
|
+
onNavigateManage,
|
|
77
|
+
onAddAccount,
|
|
78
|
+
onNavigateProfile,
|
|
79
|
+
onBeforeSessionChange,
|
|
80
|
+
className,
|
|
81
|
+
style,
|
|
82
|
+
}) => {
|
|
83
|
+
const {
|
|
84
|
+
user,
|
|
85
|
+
isAuthenticated,
|
|
86
|
+
isAuthResolved,
|
|
87
|
+
isPrivateApiPending,
|
|
88
|
+
signIn,
|
|
89
|
+
} = useAuth();
|
|
90
|
+
const { colors } = useTheme();
|
|
91
|
+
const { t, locale } = useI18n();
|
|
92
|
+
|
|
93
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
94
|
+
const [anchor, setAnchor] = useState<ProfileMenuAnchor | null>(null);
|
|
95
|
+
|
|
96
|
+
// Trigger ref for the web anchor measurement. RN-Web exposes `measure()` on
|
|
97
|
+
// host views, so we anchor the popover to the button rect and open upward.
|
|
98
|
+
const triggerRef = useRef<View | null>(null);
|
|
99
|
+
|
|
100
|
+
const resolvedAvatarSize = avatarSize ?? (expanded ? 40 : 32);
|
|
101
|
+
|
|
102
|
+
const openMenu = useCallback(() => {
|
|
103
|
+
if (!isWeb) {
|
|
104
|
+
setAnchor(null);
|
|
105
|
+
setMenuOpen(true);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const node = triggerRef.current;
|
|
109
|
+
if (!node || typeof node.measure !== 'function' || typeof window === 'undefined') {
|
|
110
|
+
setAnchor(null);
|
|
111
|
+
setMenuOpen(true);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
node.measure((_x, _y, _w, _h, pageX, pageY) => {
|
|
115
|
+
if (typeof pageX !== 'number' || typeof pageY !== 'number') {
|
|
116
|
+
setAnchor(null);
|
|
117
|
+
} else {
|
|
118
|
+
// Clamp the panel's left edge inside the viewport gutter.
|
|
119
|
+
const maxLeft = Math.max(
|
|
120
|
+
VIEWPORT_GUTTER,
|
|
121
|
+
window.innerWidth - MENU_WIDTH - VIEWPORT_GUTTER,
|
|
122
|
+
);
|
|
123
|
+
const left = Math.min(Math.max(VIEWPORT_GUTTER, pageX), maxLeft);
|
|
124
|
+
// Anchor the panel's BOTTOM edge just above the trigger top so
|
|
125
|
+
// the menu opens upward from this footer button.
|
|
126
|
+
const bottom = Math.max(
|
|
127
|
+
VIEWPORT_GUTTER,
|
|
128
|
+
window.innerHeight - pageY + VIEWPORT_GUTTER,
|
|
129
|
+
);
|
|
130
|
+
setAnchor({ left, bottom });
|
|
131
|
+
}
|
|
132
|
+
setMenuOpen(true);
|
|
133
|
+
});
|
|
134
|
+
}, []);
|
|
135
|
+
|
|
136
|
+
const handleClose = useCallback(() => {
|
|
137
|
+
setMenuOpen(false);
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
// ── Undetermined: skeleton circle, no interaction. ──────────────────────
|
|
141
|
+
if (!isAuthResolved || isPrivateApiPending) {
|
|
142
|
+
return (
|
|
143
|
+
<View
|
|
144
|
+
className={className}
|
|
145
|
+
style={style}
|
|
146
|
+
accessibilityElementsHidden
|
|
147
|
+
importantForAccessibility="no-hide-descendants"
|
|
148
|
+
>
|
|
149
|
+
<View
|
|
150
|
+
className="rounded-full bg-secondary"
|
|
151
|
+
style={{ width: resolvedAvatarSize, height: resolvedAvatarSize }}
|
|
152
|
+
/>
|
|
153
|
+
</View>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Signed out: "Sign in" row. ──────────────────────────────────────────
|
|
158
|
+
if (!isAuthenticated || !user) {
|
|
159
|
+
const signInLabel = t('common.actions.signIn') || 'Sign in';
|
|
160
|
+
return (
|
|
161
|
+
<Pressable
|
|
162
|
+
className={`flex-row items-center gap-3 rounded-full px-2 py-2 ${className ?? ''}`}
|
|
163
|
+
style={style}
|
|
164
|
+
onPress={() => { void signIn(); }}
|
|
165
|
+
accessibilityRole="button"
|
|
166
|
+
accessibilityLabel={signInLabel}
|
|
167
|
+
>
|
|
168
|
+
<View
|
|
169
|
+
className="items-center justify-center rounded-full bg-secondary"
|
|
170
|
+
style={{ width: resolvedAvatarSize, height: resolvedAvatarSize }}
|
|
171
|
+
>
|
|
172
|
+
<MaterialCommunityIcons
|
|
173
|
+
name="login"
|
|
174
|
+
size={Math.round(resolvedAvatarSize * 0.5)}
|
|
175
|
+
color={colors.icon}
|
|
176
|
+
/>
|
|
177
|
+
</View>
|
|
178
|
+
{expanded ? (
|
|
179
|
+
<Text
|
|
180
|
+
className="flex-1 font-semibold text-foreground"
|
|
181
|
+
numberOfLines={1}
|
|
182
|
+
>
|
|
183
|
+
{signInLabel}
|
|
184
|
+
</Text>
|
|
185
|
+
) : null}
|
|
186
|
+
</Pressable>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Signed in: avatar + identity + chevron. ─────────────────────────────
|
|
191
|
+
const displayName = getAccountDisplayName(user, locale);
|
|
192
|
+
const handle = getAccountFallbackHandle(user);
|
|
193
|
+
const handleLine = handle ? `@${handle}` : null;
|
|
194
|
+
|
|
195
|
+
const avatarNode = (
|
|
196
|
+
<Avatar
|
|
197
|
+
source={user.avatar ?? undefined}
|
|
198
|
+
variant="thumb"
|
|
199
|
+
name={displayName}
|
|
200
|
+
size={resolvedAvatarSize}
|
|
201
|
+
/>
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
if (!expanded) {
|
|
205
|
+
return (
|
|
206
|
+
<View className={className} style={style}>
|
|
207
|
+
<Pressable
|
|
208
|
+
ref={triggerRef}
|
|
209
|
+
className="rounded-full"
|
|
210
|
+
onPress={openMenu}
|
|
211
|
+
accessibilityRole="button"
|
|
212
|
+
accessibilityLabel={t('accountSwitcher.switchWhileSignedInAs', { name: displayName })
|
|
213
|
+
|| `Switch account, signed in as ${displayName}`}
|
|
214
|
+
>
|
|
215
|
+
{avatarNode}
|
|
216
|
+
</Pressable>
|
|
217
|
+
<ProfileMenu
|
|
218
|
+
open={menuOpen}
|
|
219
|
+
onClose={handleClose}
|
|
220
|
+
anchor={anchor}
|
|
221
|
+
onNavigateManage={onNavigateManage}
|
|
222
|
+
onAddAccount={onAddAccount}
|
|
223
|
+
onNavigateProfile={onNavigateProfile}
|
|
224
|
+
onBeforeSessionChange={onBeforeSessionChange}
|
|
225
|
+
/>
|
|
226
|
+
</View>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<View className={className} style={style}>
|
|
232
|
+
<Pressable
|
|
233
|
+
ref={triggerRef}
|
|
234
|
+
className="flex-row items-center gap-3 rounded-full px-2 py-2"
|
|
235
|
+
onPress={openMenu}
|
|
236
|
+
accessibilityRole="button"
|
|
237
|
+
accessibilityLabel={t('accountSwitcher.switchWhileSignedInAs', { name: displayName })
|
|
238
|
+
|| `Switch account, signed in as ${displayName}`}
|
|
239
|
+
>
|
|
240
|
+
{avatarNode}
|
|
241
|
+
<View className="min-w-0 flex-1">
|
|
242
|
+
<Text className="font-bold text-foreground" numberOfLines={1}>
|
|
243
|
+
{displayName}
|
|
244
|
+
</Text>
|
|
245
|
+
{handleLine ? (
|
|
246
|
+
<Text className="text-xs text-muted-foreground" numberOfLines={1}>
|
|
247
|
+
{handleLine}
|
|
248
|
+
</Text>
|
|
249
|
+
) : null}
|
|
250
|
+
</View>
|
|
251
|
+
<MaterialCommunityIcons
|
|
252
|
+
name="unfold-more-horizontal"
|
|
253
|
+
size={18}
|
|
254
|
+
color={colors.textSecondary}
|
|
255
|
+
/>
|
|
256
|
+
</Pressable>
|
|
257
|
+
<ProfileMenu
|
|
258
|
+
open={menuOpen}
|
|
259
|
+
onClose={handleClose}
|
|
260
|
+
anchor={anchor}
|
|
261
|
+
onNavigateManage={onNavigateManage}
|
|
262
|
+
onAddAccount={onAddAccount}
|
|
263
|
+
onNavigateProfile={onNavigateProfile}
|
|
264
|
+
onBeforeSessionChange={onBeforeSessionChange}
|
|
265
|
+
/>
|
|
266
|
+
</View>
|
|
267
|
+
);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export default ProfileButton;
|