@oxyhq/services 12.2.0 → 13.0.1

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 (120) hide show
  1. package/lib/commonjs/index.js +56 -6
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/components/AccountMenuButton.js +26 -9
  4. package/lib/commonjs/ui/components/AccountMenuButton.js.map +1 -1
  5. package/lib/commonjs/ui/components/AccountSwitcher.js +792 -0
  6. package/lib/commonjs/ui/components/AccountSwitcher.js.map +1 -0
  7. package/lib/commonjs/ui/components/ActiveAccountBanner.js +144 -0
  8. package/lib/commonjs/ui/components/ActiveAccountBanner.js.map +1 -0
  9. package/lib/commonjs/ui/context/OxyContext.js +72 -28
  10. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  11. package/lib/commonjs/ui/index.js +42 -0
  12. package/lib/commonjs/ui/index.js.map +1 -1
  13. package/lib/commonjs/ui/navigation/routes.js +4 -1
  14. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  15. package/lib/commonjs/ui/screens/AccountMembersScreen.js +469 -0
  16. package/lib/commonjs/ui/screens/AccountMembersScreen.js.map +1 -0
  17. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +264 -0
  18. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -0
  19. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +51 -0
  20. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -0
  21. package/lib/commonjs/ui/screens/{CreateManagedAccountScreen.js → CreateAccountScreen.js} +116 -25
  22. package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -0
  23. package/lib/commonjs/ui/screens/ManageAccountScreen.js +11 -11
  24. package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
  25. package/lib/module/index.js +16 -4
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/module/ui/components/AccountMenuButton.js +26 -9
  28. package/lib/module/ui/components/AccountMenuButton.js.map +1 -1
  29. package/lib/module/ui/components/AccountSwitcher.js +786 -0
  30. package/lib/module/ui/components/AccountSwitcher.js.map +1 -0
  31. package/lib/module/ui/components/ActiveAccountBanner.js +141 -0
  32. package/lib/module/ui/components/ActiveAccountBanner.js.map +1 -0
  33. package/lib/module/ui/context/OxyContext.js +72 -28
  34. package/lib/module/ui/context/OxyContext.js.map +1 -1
  35. package/lib/module/ui/index.js +5 -0
  36. package/lib/module/ui/index.js.map +1 -1
  37. package/lib/module/ui/navigation/routes.js +4 -1
  38. package/lib/module/ui/navigation/routes.js.map +1 -1
  39. package/lib/module/ui/screens/AccountMembersScreen.js +464 -0
  40. package/lib/module/ui/screens/AccountMembersScreen.js.map +1 -0
  41. package/lib/module/ui/screens/AccountSettingsScreen.js +259 -0
  42. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -0
  43. package/lib/module/ui/screens/AccountSwitcherScreen.js +48 -0
  44. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -0
  45. package/lib/module/ui/screens/{CreateManagedAccountScreen.js → CreateAccountScreen.js} +117 -26
  46. package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -0
  47. package/lib/module/ui/screens/ManageAccountScreen.js +11 -11
  48. package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
  49. package/lib/typescript/commonjs/index.d.ts +7 -1
  50. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/ui/components/AccountMenuButton.d.ts +13 -4
  52. package/lib/typescript/commonjs/ui/components/AccountMenuButton.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts +47 -0
  54. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts.map +1 -0
  55. package/lib/typescript/commonjs/ui/components/{ActingAsBanner.d.ts → ActiveAccountBanner.d.ts} +1 -1
  56. package/lib/typescript/commonjs/ui/components/ActiveAccountBanner.d.ts.map +1 -0
  57. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +33 -5
  58. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/ui/index.d.ts +5 -0
  61. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
  63. package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/ui/screens/AccountMembersScreen.d.ts +10 -0
  65. package/lib/typescript/commonjs/ui/screens/AccountMembersScreen.d.ts.map +1 -0
  66. package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts +12 -0
  67. package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts.map +1 -0
  68. package/lib/typescript/commonjs/ui/screens/AccountSwitcherScreen.d.ts +11 -0
  69. package/lib/typescript/commonjs/ui/screens/AccountSwitcherScreen.d.ts.map +1 -0
  70. package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts +11 -0
  71. package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -0
  72. package/lib/typescript/module/index.d.ts +7 -1
  73. package/lib/typescript/module/index.d.ts.map +1 -1
  74. package/lib/typescript/module/ui/components/AccountMenuButton.d.ts +13 -4
  75. package/lib/typescript/module/ui/components/AccountMenuButton.d.ts.map +1 -1
  76. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts +47 -0
  77. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts.map +1 -0
  78. package/lib/typescript/module/ui/components/{ActingAsBanner.d.ts → ActiveAccountBanner.d.ts} +1 -1
  79. package/lib/typescript/module/ui/components/ActiveAccountBanner.d.ts.map +1 -0
  80. package/lib/typescript/module/ui/context/OxyContext.d.ts +33 -5
  81. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  82. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  83. package/lib/typescript/module/ui/index.d.ts +5 -0
  84. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  85. package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
  86. package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
  87. package/lib/typescript/module/ui/screens/AccountMembersScreen.d.ts +10 -0
  88. package/lib/typescript/module/ui/screens/AccountMembersScreen.d.ts.map +1 -0
  89. package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts +12 -0
  90. package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts.map +1 -0
  91. package/lib/typescript/module/ui/screens/AccountSwitcherScreen.d.ts +11 -0
  92. package/lib/typescript/module/ui/screens/AccountSwitcherScreen.d.ts.map +1 -0
  93. package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts +11 -0
  94. package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -0
  95. package/package.json +3 -3
  96. package/src/index.ts +18 -4
  97. package/src/ui/components/AccountMenuButton.tsx +25 -10
  98. package/src/ui/components/AccountSwitcher.tsx +763 -0
  99. package/src/ui/components/ActiveAccountBanner.tsx +130 -0
  100. package/src/ui/context/OxyContext.tsx +122 -37
  101. package/src/ui/index.ts +5 -0
  102. package/src/ui/navigation/routes.ts +8 -2
  103. package/src/ui/screens/AccountMembersScreen.tsx +481 -0
  104. package/src/ui/screens/AccountSettingsScreen.tsx +261 -0
  105. package/src/ui/screens/AccountSwitcherScreen.tsx +43 -0
  106. package/src/ui/screens/{CreateManagedAccountScreen.tsx → CreateAccountScreen.tsx} +140 -22
  107. package/src/ui/screens/ManageAccountScreen.tsx +19 -19
  108. package/lib/commonjs/ui/components/ActingAsBanner.js +0 -140
  109. package/lib/commonjs/ui/components/ActingAsBanner.js.map +0 -1
  110. package/lib/commonjs/ui/screens/CreateManagedAccountScreen.js.map +0 -1
  111. package/lib/module/ui/components/ActingAsBanner.js +0 -137
  112. package/lib/module/ui/components/ActingAsBanner.js.map +0 -1
  113. package/lib/module/ui/screens/CreateManagedAccountScreen.js.map +0 -1
  114. package/lib/typescript/commonjs/ui/components/ActingAsBanner.d.ts.map +0 -1
  115. package/lib/typescript/commonjs/ui/screens/CreateManagedAccountScreen.d.ts +0 -5
  116. package/lib/typescript/commonjs/ui/screens/CreateManagedAccountScreen.d.ts.map +0 -1
  117. package/lib/typescript/module/ui/components/ActingAsBanner.d.ts.map +0 -1
  118. package/lib/typescript/module/ui/screens/CreateManagedAccountScreen.d.ts +0 -5
  119. package/lib/typescript/module/ui/screens/CreateManagedAccountScreen.d.ts.map +0 -1
  120. package/src/ui/components/ActingAsBanner.tsx +0 -129
@@ -0,0 +1,261 @@
1
+ import type React from 'react';
2
+ import { useCallback, useMemo, useState } from 'react';
3
+ import {
4
+ View,
5
+ ScrollView,
6
+ ActivityIndicator,
7
+ Platform,
8
+ KeyboardAvoidingView,
9
+ } from 'react-native';
10
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
11
+ import { toast, Dialog, useDialogControl } from '@oxyhq/bloom';
12
+ import { useTheme } from '@oxyhq/bloom/theme';
13
+ import { H1, Text } from '@oxyhq/bloom/typography';
14
+ import { Button } from '@oxyhq/bloom/button';
15
+ import { TextField, TextFieldInput } from '@oxyhq/bloom/text-field';
16
+ import { SettingsListGroup, SettingsListItem } from '@oxyhq/bloom/settings-list';
17
+ import type { UpdateAccountInput } from '@oxyhq/core';
18
+ import type { BaseScreenProps } from '../types/navigation';
19
+ import Header from '../components/Header';
20
+ import { SettingsIcon } from '../components/SettingsIcon';
21
+ import { useOxy } from '../context/OxyContext';
22
+ import { useI18n } from '../hooks/useI18n';
23
+
24
+ const DISPLAY_NAME_MAX = 50;
25
+ const BIO_MAX = 160;
26
+
27
+ const errorMessage = (error: unknown, fallback: string): string =>
28
+ error instanceof Error && error.message ? error.message : fallback;
29
+
30
+ /**
31
+ * Per-account settings: edit the account's profile, jump to member management,
32
+ * and archive the account (danger zone). Receives the target `accountId` via
33
+ * navigation props. Self-surface for the caller's own personal account lives in
34
+ * {@link ManageAccountScreen}; this screen manages a non-personal account in the
35
+ * graph.
36
+ */
37
+ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, navigate, accountId }) => {
38
+ const bloomTheme = useTheme();
39
+ const colors = bloomTheme.colors;
40
+ const { t } = useI18n();
41
+ const { oxyServices, canUsePrivateApi, actingAs, setActingAs } = useOxy();
42
+ const queryClient = useQueryClient();
43
+
44
+ const id = typeof accountId === 'string' ? accountId : '';
45
+
46
+ const accountQuery = useQuery({
47
+ queryKey: ['accounts', 'detail', id],
48
+ queryFn: () => oxyServices.getAccount(id),
49
+ enabled: canUsePrivateApi && id.length > 0,
50
+ });
51
+
52
+ const node = accountQuery.data ?? null;
53
+ const permissions = node?.callerMembership?.permissions ?? [];
54
+ const isImplicitOwner = node ? node.relationship !== 'member' : false;
55
+ const can = useCallback(
56
+ (permission: string): boolean => isImplicitOwner || permissions.includes(permission),
57
+ [isImplicitOwner, permissions],
58
+ );
59
+ const canUpdate = can('account:update');
60
+ const canViewMembers = can('members:read');
61
+ const canArchive = can('account:delete');
62
+
63
+ // Editable fields, seeded lazily from the loaded account once.
64
+ const [seeded, setSeeded] = useState(false);
65
+ const [displayName, setDisplayName] = useState('');
66
+ const [bio, setBio] = useState('');
67
+
68
+ // Seed the form from the account during render (no useEffect): the first time
69
+ // the query resolves we capture its values into local edit state.
70
+ if (node && !seeded) {
71
+ const first = node.account?.name?.first ?? '';
72
+ const last = node.account?.name?.last ?? '';
73
+ setDisplayName([first, last].filter(Boolean).join(' '));
74
+ setBio(node.account?.bio ?? '');
75
+ setSeeded(true);
76
+ }
77
+
78
+ const updateMutation = useMutation({
79
+ mutationKey: ['accounts', 'update', id],
80
+ mutationFn: (input: UpdateAccountInput) => oxyServices.updateAccount(id, input),
81
+ onSuccess: () => {
82
+ queryClient.invalidateQueries({ queryKey: ['accounts', 'detail', id] });
83
+ queryClient.invalidateQueries({ queryKey: ['accounts'] });
84
+ toast.success(t('accounts.settings.toasts.saved') || 'Account updated');
85
+ },
86
+ onError: (error) => {
87
+ toast.error(errorMessage(error, t('accounts.settings.toasts.saveFailed') || 'Failed to update account'));
88
+ },
89
+ });
90
+
91
+ const archiveMutation = useMutation({
92
+ mutationKey: ['accounts', 'archive', id],
93
+ mutationFn: () => oxyServices.archiveAccount(id),
94
+ onSuccess: () => {
95
+ // If this was the active account, drop back to the personal account.
96
+ if (actingAs === id) {
97
+ setActingAs(null);
98
+ }
99
+ queryClient.invalidateQueries({ queryKey: ['accounts'] });
100
+ toast.success(t('accounts.settings.toasts.archived') || 'Account archived');
101
+ (onClose ?? goBack)?.();
102
+ },
103
+ onError: (error) => {
104
+ toast.error(errorMessage(error, t('accounts.settings.toasts.archiveFailed') || 'Failed to archive account'));
105
+ },
106
+ });
107
+
108
+ const archiveDialog = useDialogControl();
109
+
110
+ const handleSave = useCallback(() => {
111
+ const trimmed = displayName.trim();
112
+ const nameParts = trimmed.split(/\s+/).filter(Boolean);
113
+ const first = nameParts[0] || '';
114
+ const last = nameParts.length > 1 ? nameParts.slice(1).join(' ') : undefined;
115
+ updateMutation.mutate({
116
+ name: { first, last },
117
+ bio: bio.trim() ? bio.trim() : null,
118
+ });
119
+ }, [displayName, bio, updateMutation]);
120
+
121
+ const title = t('accounts.settings.title') || 'Account settings';
122
+
123
+ const accountHandle = useMemo(() => {
124
+ const username = node?.account?.username;
125
+ return username ? `@${username}` : '';
126
+ }, [node?.account?.username]);
127
+
128
+ if (!id) {
129
+ return (
130
+ <View className="flex-1 bg-bg">
131
+ <Header title={title} onBack={goBack} onClose={onClose} showBackButton showCloseButton elevation="subtle" />
132
+ <View className="flex-1 items-center justify-center px-screen-margin">
133
+ <Text className="text-body font-body text-text-secondary text-center">
134
+ {t('accounts.settings.errors.missingAccount') || 'No account selected.'}
135
+ </Text>
136
+ </View>
137
+ </View>
138
+ );
139
+ }
140
+
141
+ return (
142
+ <View className="flex-1 bg-bg">
143
+ <Header title={title} onBack={goBack} onClose={onClose} showBackButton showCloseButton elevation="subtle" />
144
+
145
+ <KeyboardAvoidingView
146
+ className="flex-1"
147
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}
148
+ keyboardVerticalOffset={Platform.OS === 'ios' ? 88 : 0}
149
+ >
150
+ <ScrollView
151
+ className="flex-1"
152
+ contentContainerClassName="px-screen-margin pt-space-24 pb-space-32 gap-space-24"
153
+ showsVerticalScrollIndicator={false}
154
+ keyboardShouldPersistTaps="handled"
155
+ >
156
+ {accountQuery.isLoading ? (
157
+ <View className="items-center py-space-32">
158
+ <ActivityIndicator color={colors.primary} />
159
+ </View>
160
+ ) : (
161
+ <>
162
+ <View className="gap-space-8">
163
+ <H1 className="text-headerBold font-headerBold text-text" numberOfLines={1}>
164
+ {accountHandle || title}
165
+ </H1>
166
+ <Text className="text-body font-body text-text-secondary">
167
+ {t('accounts.settings.subtitle') || 'Manage this account’s profile, members, and access.'}
168
+ </Text>
169
+ </View>
170
+
171
+ {/* Profile edit */}
172
+ {canUpdate ? (
173
+ <View className="gap-space-16 p-space-16 rounded-radius-20 bg-fill">
174
+ <TextField>
175
+ <TextFieldInput
176
+ floatingLabel
177
+ label={t('accounts.settings.displayName.label') || 'Display name'}
178
+ value={displayName}
179
+ onChangeText={setDisplayName}
180
+ maxLength={DISPLAY_NAME_MAX}
181
+ />
182
+ </TextField>
183
+ <View className="gap-space-4">
184
+ <TextField>
185
+ <TextFieldInput
186
+ floatingLabel
187
+ label={t('accounts.settings.bio.label') || 'Bio (optional)'}
188
+ value={bio}
189
+ onChangeText={setBio}
190
+ maxLength={BIO_MAX}
191
+ multiline
192
+ numberOfLines={3}
193
+ textAlignVertical="top"
194
+ />
195
+ </TextField>
196
+ <Text className="text-caption font-caption text-text-tertiary px-space-4 text-right">
197
+ {bio.length}/{BIO_MAX}
198
+ </Text>
199
+ </View>
200
+ <Button
201
+ variant="primary"
202
+ onPress={handleSave}
203
+ disabled={updateMutation.isPending || !displayName.trim()}
204
+ loading={updateMutation.isPending}
205
+ accessibilityLabel={t('accounts.settings.save') || 'Save changes'}
206
+ className="w-full"
207
+ >
208
+ {t('accounts.settings.save') || 'Save changes'}
209
+ </Button>
210
+ </View>
211
+ ) : null}
212
+
213
+ {/* Members entry */}
214
+ {canViewMembers ? (
215
+ <SettingsListGroup title={t('accounts.settings.sections.access') || 'Access'}>
216
+ <SettingsListItem
217
+ icon={<SettingsIcon name="account-multiple" color={colors.info} />}
218
+ title={t('accounts.members.title') || 'Members'}
219
+ description={t('accounts.settings.members.subtitle') || 'Invite people and manage roles'}
220
+ onPress={() => navigate?.('AccountMembers', { accountId: id })}
221
+ />
222
+ </SettingsListGroup>
223
+ ) : null}
224
+
225
+ {/* Danger zone */}
226
+ {canArchive ? (
227
+ <SettingsListGroup title={t('accounts.settings.sections.dangerZone') || 'Danger zone'}>
228
+ <SettingsListItem
229
+ icon={<SettingsIcon name="archive-arrow-down" color={colors.error} />}
230
+ title={t('accounts.settings.archive.title') || 'Archive account'}
231
+ description={t('accounts.settings.archive.subtitle') || 'Deactivate this account'}
232
+ onPress={() => archiveDialog.open()}
233
+ />
234
+ </SettingsListGroup>
235
+ ) : null}
236
+ </>
237
+ )}
238
+ </ScrollView>
239
+ </KeyboardAvoidingView>
240
+
241
+ <Dialog
242
+ control={archiveDialog}
243
+ title={t('accounts.settings.archive.confirmTitle') || 'Archive account'}
244
+ description={
245
+ t('accounts.settings.archive.confirmDescription')
246
+ || 'Archive this account? It will be deactivated and its members will lose access.'
247
+ }
248
+ actions={[
249
+ {
250
+ label: t('accounts.settings.archive.title') || 'Archive account',
251
+ color: 'destructive',
252
+ onPress: () => archiveMutation.mutate(),
253
+ },
254
+ { label: t('common.cancel') || 'Cancel', color: 'cancel' },
255
+ ]}
256
+ />
257
+ </View>
258
+ );
259
+ };
260
+
261
+ export default AccountSettingsScreen;
@@ -0,0 +1,43 @@
1
+ import type React from 'react';
2
+ import { useCallback } from 'react';
3
+ import { View } from 'react-native';
4
+ import type { BaseScreenProps } from '../types/navigation';
5
+ import Header from '../components/Header';
6
+ import { AccountSwitcherView } from '../components/AccountSwitcher';
7
+ import { useI18n } from '../hooks/useI18n';
8
+
9
+ /**
10
+ * Bottom-sheet route wrapper around {@link AccountSwitcherView}. Used when the
11
+ * switcher is opened as a sheet (e.g. from the `ActiveAccountBanner` cue or the
12
+ * "Your accounts" entry in ManageAccount) rather than as the header-chip
13
+ * popover (which uses the `AccountSwitcher` modal directly).
14
+ */
15
+ const AccountSwitcherScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, navigate }) => {
16
+ const { t } = useI18n();
17
+
18
+ const close = useCallback(() => {
19
+ (onClose ?? goBack)?.();
20
+ }, [onClose, goBack]);
21
+
22
+ return (
23
+ <View className="flex-1 bg-bg">
24
+ <Header
25
+ title={t('accountSwitcher.title') || 'Switch account'}
26
+ onBack={goBack}
27
+ onClose={onClose}
28
+ showBackButton={!!goBack}
29
+ showCloseButton={!!onClose}
30
+ elevation="subtle"
31
+ />
32
+ <AccountSwitcherView
33
+ onClose={close}
34
+ onAddAccount={() => navigate?.('OxyAuth')}
35
+ onNavigateManage={() => navigate?.('ManageAccount')}
36
+ onCreateAccount={() => navigate?.('CreateAccount')}
37
+ onOpenAccountSettings={(accountId) => navigate?.('AccountSettings', { accountId })}
38
+ />
39
+ </View>
40
+ );
41
+ };
42
+
43
+ export default AccountSwitcherScreen;
@@ -6,8 +6,10 @@ import {
6
6
  ActivityIndicator,
7
7
  ScrollView,
8
8
  KeyboardAvoidingView,
9
+ TouchableOpacity,
9
10
  } from 'react-native';
10
11
  import { Ionicons } from '@expo/vector-icons';
12
+ import type { AccountKind, CreateAccountInput } from '@oxyhq/core';
11
13
  import type { BaseScreenProps } from '../types/navigation';
12
14
  import Header from '../components/Header';
13
15
  import { useI18n } from '../hooks/useI18n';
@@ -20,20 +22,74 @@ import { toast } from '@oxyhq/bloom';
20
22
 
21
23
  type UsernameStatus = 'idle' | 'checking' | 'available' | 'taken' | 'invalid';
22
24
 
25
+ /** Kind of account this screen can create. `personal` is a signup-minted root and is never created here. */
26
+ type CreatableAccountKind = Exclude<AccountKind, 'personal'>;
27
+
23
28
  const USERNAME_REGEX = /^[a-zA-Z0-9_-]{3,30}$/;
24
29
  const DEBOUNCE_MS = 400;
25
30
  const USERNAME_MAX = 30;
26
31
  const DISPLAY_NAME_MAX = 50;
27
32
  const BIO_MAX = 160;
28
33
 
29
- const CreateManagedAccountScreen: React.FC<BaseScreenProps> = ({
34
+ interface KindOption {
35
+ value: CreatableAccountKind;
36
+ icon: React.ComponentProps<typeof Ionicons>['name'];
37
+ }
38
+
39
+ // The creatable account kinds, matching the API `createAccountSchema` enum.
40
+ // Order places the most common choice (a project / persona) first.
41
+ const KIND_OPTIONS: KindOption[] = [
42
+ { value: 'project', icon: 'cube-outline' },
43
+ { value: 'organization', icon: 'business-outline' },
44
+ { value: 'bot', icon: 'hardware-chip-outline' },
45
+ ];
46
+
47
+ const kindLabel = (
48
+ t: (key: string, vars?: Record<string, string | number>) => string,
49
+ kind: CreatableAccountKind,
50
+ ): string => {
51
+ switch (kind) {
52
+ case 'organization':
53
+ return t('accounts.kinds.organization.label') || 'Organization';
54
+ case 'bot':
55
+ return t('accounts.kinds.bot.label') || 'Bot';
56
+ default:
57
+ return t('accounts.kinds.project.label') || 'Project';
58
+ }
59
+ };
60
+
61
+ const kindDescription = (
62
+ t: (key: string, vars?: Record<string, string | number>) => string,
63
+ kind: CreatableAccountKind,
64
+ ): string => {
65
+ switch (kind) {
66
+ case 'organization':
67
+ return t('accounts.kinds.organization.description') || 'A shared team account with members';
68
+ case 'bot':
69
+ return t('accounts.kinds.bot.description') || 'A programmatic account with service credentials';
70
+ default:
71
+ return t('accounts.kinds.project.description') || 'A separate account you control';
72
+ }
73
+ };
74
+
75
+ /**
76
+ * Create a new account in the unified account graph (an organization, project,
77
+ * or bot). The caller becomes its owner. Optionally nested under a parent
78
+ * account via the `parentAccountId` prop. NOT the cryptographic Commons/DID
79
+ * "identity" — that is a separate concept.
80
+ */
81
+ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
30
82
  onClose,
31
83
  goBack,
84
+ parentAccountId,
32
85
  }) => {
33
86
  const bloomTheme = useTheme();
34
- const { oxyServices, createManagedAccount, setActingAs } = useOxy();
87
+ const { oxyServices, createAccount, setActingAs } = useOxy();
35
88
  const { t } = useI18n();
36
89
 
90
+ const parentId = typeof parentAccountId === 'string' ? parentAccountId : undefined;
91
+
92
+ const [kind, setKind] = useState<CreatableAccountKind>('project');
37
93
  const [username, setUsername] = useState('');
38
94
  const [displayName, setDisplayName] = useState('');
39
95
  const [bio, setBio] = useState('');
@@ -51,13 +107,19 @@ const CreateManagedAccountScreen: React.FC<BaseScreenProps> = ({
51
107
 
52
108
  if (!value || value.length < 3) {
53
109
  setUsernameStatus(value.length > 0 ? 'invalid' : 'idle');
54
- setUsernameMessage(value.length > 0 ? 'Username must be at least 3 characters' : '');
110
+ setUsernameMessage(
111
+ value.length > 0
112
+ ? (t('accounts.create.username.tooShort') || 'Username must be at least 3 characters')
113
+ : '',
114
+ );
55
115
  return;
56
116
  }
57
117
 
58
118
  if (!USERNAME_REGEX.test(value)) {
59
119
  setUsernameStatus('invalid');
60
- setUsernameMessage('Only letters, numbers, hyphens, and underscores');
120
+ setUsernameMessage(
121
+ t('accounts.create.username.invalidChars') || 'Only letters, numbers, hyphens, and underscores',
122
+ );
61
123
  return;
62
124
  }
63
125
 
@@ -68,13 +130,18 @@ const CreateManagedAccountScreen: React.FC<BaseScreenProps> = ({
68
130
  try {
69
131
  const result = await oxyServices.checkUsernameAvailability(value);
70
132
  setUsernameStatus(result.available ? 'available' : 'taken');
71
- setUsernameMessage(result.message || (result.available ? 'Username is available' : 'Username is taken'));
133
+ setUsernameMessage(
134
+ result.message
135
+ || (result.available
136
+ ? (t('accounts.create.username.available') || 'Username is available')
137
+ : (t('accounts.create.username.taken') || 'Username is taken')),
138
+ );
72
139
  } catch {
73
140
  setUsernameStatus('idle');
74
- setUsernameMessage('Could not check availability');
141
+ setUsernameMessage(t('accounts.create.username.checkFailed') || 'Could not check availability');
75
142
  }
76
143
  }, DEBOUNCE_MS);
77
- }, [oxyServices]);
144
+ }, [oxyServices, t]);
78
145
 
79
146
  const handleUsernameChange = useCallback((value: string) => {
80
147
  const cleaned = value.toLowerCase().replace(/[^a-z0-9_-]/g, '');
@@ -103,27 +170,32 @@ const CreateManagedAccountScreen: React.FC<BaseScreenProps> = ({
103
170
  const firstName = nameParts[0] || '';
104
171
  const lastName = nameParts.length > 1 ? nameParts.slice(1).join(' ') : undefined;
105
172
 
106
- const account = await createManagedAccount({
173
+ const input: CreateAccountInput = {
174
+ kind,
107
175
  username,
108
176
  name: { first: firstName, last: lastName },
109
177
  bio: bio.trim() || undefined,
110
- });
178
+ ...(parentId ? { parentAccountId: parentId } : null),
179
+ };
180
+ const account = await createAccount(input);
111
181
 
112
- toast.success('Identity created successfully');
182
+ toast.success(t('accounts.create.toasts.success') || 'Account created');
113
183
 
114
- // Switch to the new managed account
184
+ // Switch to the new account
115
185
  if (account.accountId) {
116
186
  setActingAs(account.accountId);
117
187
  }
118
188
 
119
189
  onClose?.();
120
190
  } catch (error) {
121
- const message = error instanceof Error ? error.message : 'Failed to create identity';
191
+ const message = error instanceof Error
192
+ ? error.message
193
+ : (t('accounts.create.toasts.failed') || 'Failed to create account');
122
194
  toast.error(message);
123
195
  } finally {
124
196
  setIsCreating(false);
125
197
  }
126
- }, [canCreate, username, displayName, bio, createManagedAccount, setActingAs, onClose]);
198
+ }, [canCreate, kind, username, displayName, bio, parentId, createAccount, setActingAs, onClose, t]);
127
199
 
128
200
  // Status icon + color shown alongside the username field message
129
201
  const usernameIsInvalid = usernameStatus === 'taken' || usernameStatus === 'invalid';
@@ -133,10 +205,12 @@ const CreateManagedAccountScreen: React.FC<BaseScreenProps> = ({
133
205
  ? bloomTheme.colors.negative
134
206
  : bloomTheme.colors.textSecondary;
135
207
 
208
+ const title = t('accounts.create.title') || 'Create account';
209
+
136
210
  return (
137
211
  <View className="flex-1 bg-bg">
138
212
  <Header
139
- title="Create Identity"
213
+ title={title}
140
214
  onBack={goBack}
141
215
  onClose={onClose}
142
216
  showBackButton={true}
@@ -158,13 +232,57 @@ const CreateManagedAccountScreen: React.FC<BaseScreenProps> = ({
158
232
  {/* Big Title */}
159
233
  <View className="gap-space-8">
160
234
  <H1 className="text-headerBold font-headerBold text-text">
161
- Create Identity
235
+ {title}
162
236
  </H1>
163
237
  <Text className="text-body font-body text-text-secondary">
164
- Create a managed identity that you control. It will have its own profile, posts, and interactions.
238
+ {t('accounts.create.subtitle')
239
+ || 'Create an account you control. It will have its own profile, members, and apps.'}
165
240
  </Text>
166
241
  </View>
167
242
 
243
+ {/* Kind picker */}
244
+ <View className="gap-space-8">
245
+ {KIND_OPTIONS.map((option) => {
246
+ const selected = option.value === kind;
247
+ return (
248
+ <TouchableOpacity
249
+ key={option.value}
250
+ accessibilityRole="radio"
251
+ accessibilityState={{ selected }}
252
+ accessibilityLabel={kindLabel(t, option.value)}
253
+ onPress={() => setKind(option.value)}
254
+ activeOpacity={0.7}
255
+ className="flex-row items-center gap-space-12 p-space-16 rounded-radius-20"
256
+ style={{
257
+ backgroundColor: selected ? bloomTheme.colors.primarySubtle : bloomTheme.colors.card,
258
+ borderWidth: 1,
259
+ borderColor: selected ? bloomTheme.colors.primary : bloomTheme.colors.border,
260
+ }}
261
+ >
262
+ <Ionicons
263
+ name={option.icon}
264
+ size={22}
265
+ color={selected ? bloomTheme.colors.primary : bloomTheme.colors.icon}
266
+ />
267
+ <View className="flex-1">
268
+ <Text
269
+ className="text-body font-bodyBold"
270
+ style={{ color: selected ? bloomTheme.colors.primary : bloomTheme.colors.text }}
271
+ >
272
+ {kindLabel(t, option.value)}
273
+ </Text>
274
+ <Text className="text-caption font-caption text-text-secondary">
275
+ {kindDescription(t, option.value)}
276
+ </Text>
277
+ </View>
278
+ {selected ? (
279
+ <Ionicons name="checkmark-circle" size={20} color={bloomTheme.colors.primary} />
280
+ ) : null}
281
+ </TouchableOpacity>
282
+ );
283
+ })}
284
+ </View>
285
+
168
286
  {/* Form Content */}
169
287
  <View className="gap-space-16 p-space-16 rounded-radius-20 bg-fill">
170
288
  {/* Username */}
@@ -172,7 +290,7 @@ const CreateManagedAccountScreen: React.FC<BaseScreenProps> = ({
172
290
  <TextField isInvalid={usernameIsInvalid}>
173
291
  <TextFieldInput
174
292
  floatingLabel
175
- label="Username"
293
+ label={t('accounts.create.username.label') || 'Username'}
176
294
  value={username}
177
295
  onChangeText={handleUsernameChange}
178
296
  isInvalid={usernameIsInvalid}
@@ -204,7 +322,7 @@ const CreateManagedAccountScreen: React.FC<BaseScreenProps> = ({
204
322
  <TextField>
205
323
  <TextFieldInput
206
324
  floatingLabel
207
- label="Display Name"
325
+ label={t('accounts.create.displayName.label') || 'Display name'}
208
326
  value={displayName}
209
327
  onChangeText={setDisplayName}
210
328
  maxLength={DISPLAY_NAME_MAX}
@@ -216,7 +334,7 @@ const CreateManagedAccountScreen: React.FC<BaseScreenProps> = ({
216
334
  <TextField>
217
335
  <TextFieldInput
218
336
  floatingLabel
219
- label="Bio (optional)"
337
+ label={t('accounts.create.bio.label') || 'Bio (optional)'}
220
338
  value={bio}
221
339
  onChangeText={setBio}
222
340
  maxLength={BIO_MAX}
@@ -237,10 +355,10 @@ const CreateManagedAccountScreen: React.FC<BaseScreenProps> = ({
237
355
  onPress={handleCreate}
238
356
  disabled={!canCreate}
239
357
  loading={isCreating}
240
- accessibilityLabel="Create Identity"
358
+ accessibilityLabel={title}
241
359
  className="w-full"
242
360
  >
243
- Create Identity
361
+ {title}
244
362
  </Button>
245
363
  </ScrollView>
246
364
  </KeyboardAvoidingView>
@@ -248,4 +366,4 @@ const CreateManagedAccountScreen: React.FC<BaseScreenProps> = ({
248
366
  );
249
367
  };
250
368
 
251
- export default CreateManagedAccountScreen;
369
+ export default CreateAccountScreen;
@@ -94,7 +94,7 @@ const ManageAccountScreen: React.FC<BaseScreenProps> = ({
94
94
  activeSessionId,
95
95
  logout,
96
96
  openAvatarPicker,
97
- managedAccounts,
97
+ accounts,
98
98
  } = useOxy();
99
99
 
100
100
  const { data: userFromQuery, isLoading: userLoading } = useCurrentUser({
@@ -667,12 +667,12 @@ const ManageAccountScreen: React.FC<BaseScreenProps> = ({
667
667
  />
668
668
  </SettingsListGroup>
669
669
 
670
- {/* Managed accounts */}
671
- {managedAccounts.length > 0 || isAuthenticated ? (
670
+ {/* Accounts (unified account graph) */}
671
+ {accounts.length > 0 || isAuthenticated ? (
672
672
  <SettingsListGroup
673
673
  title={
674
- t('accountCenter.sections.managedAccounts')
675
- || 'Managed accounts'
674
+ t('accountCenter.sections.accounts')
675
+ || 'Accounts'
676
676
  }
677
677
  >
678
678
  <SettingsListItem
@@ -683,23 +683,23 @@ const ManageAccountScreen: React.FC<BaseScreenProps> = ({
683
683
  />
684
684
  }
685
685
  title={
686
- t('accountCenter.items.manageIdentities.title')
687
- || 'Manage identities'
686
+ t('accounts.manage.switch.title')
687
+ || 'Switch account'
688
688
  }
689
689
  description={
690
- managedAccounts.length > 0
690
+ accounts.length > 0
691
691
  ? (
692
- t('accountCenter.items.manageIdentities.count', {
693
- count: managedAccounts.length,
692
+ t('accounts.manage.switch.count', {
693
+ count: accounts.length,
694
694
  })
695
- || `${managedAccounts.length} managed ${managedAccounts.length === 1 ? 'identity' : 'identities'}`
695
+ || `${accounts.length} ${accounts.length === 1 ? 'account' : 'accounts'}`
696
696
  )
697
697
  : (
698
- t('accountCenter.items.manageIdentities.empty')
699
- || 'Sub-accounts you control'
698
+ t('accounts.manage.switch.empty')
699
+ || 'Accounts you own or share'
700
700
  )
701
701
  }
702
- onPress={() => navigate?.('CreateManagedAccount')}
702
+ onPress={() => navigate?.('AccountSwitcher')}
703
703
  />
704
704
  <SettingsListItem
705
705
  icon={
@@ -709,14 +709,14 @@ const ManageAccountScreen: React.FC<BaseScreenProps> = ({
709
709
  />
710
710
  }
711
711
  title={
712
- t('accountCenter.items.createIdentity.title')
713
- || 'Create new identity'
712
+ t('accounts.create.title')
713
+ || 'Create account'
714
714
  }
715
715
  description={
716
- t('accountCenter.items.createIdentity.subtitle')
717
- || 'Add a managed sub-account'
716
+ t('accounts.manage.create.subtitle')
717
+ || 'Add an organization, project, or bot'
718
718
  }
719
- onPress={() => navigate?.('CreateManagedAccount')}
719
+ onPress={() => navigate?.('CreateAccount')}
720
720
  />
721
721
  </SettingsListGroup>
722
722
  ) : null}