@oxy.so/services 11.2.0 → 11.3.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 (85) hide show
  1. package/lib/commonjs/ui/boot/runProviderColdBoot.js +47 -3
  2. package/lib/commonjs/ui/boot/runProviderColdBoot.js.map +1 -1
  3. package/lib/commonjs/ui/components/organizations/OrganizationClaimReview.js +208 -0
  4. package/lib/commonjs/ui/components/organizations/OrganizationClaimReview.js.map +1 -0
  5. package/lib/commonjs/ui/components/organizations/OrganizationClaims.js +77 -0
  6. package/lib/commonjs/ui/components/organizations/OrganizationClaims.js.map +1 -0
  7. package/lib/commonjs/ui/components/organizations/OrganizationDirectorySearch.js +218 -0
  8. package/lib/commonjs/ui/components/organizations/OrganizationDirectorySearch.js.map +1 -0
  9. package/lib/commonjs/ui/components/organizations/OrganizationSettings.js +147 -0
  10. package/lib/commonjs/ui/components/organizations/OrganizationSettings.js.map +1 -0
  11. package/lib/commonjs/ui/components/organizations/OrganizationTypePicker.js +34 -0
  12. package/lib/commonjs/ui/components/organizations/OrganizationTypePicker.js.map +1 -0
  13. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +14 -5
  14. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  15. package/lib/commonjs/ui/screens/CreateAccountScreen.js +148 -115
  16. package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -1
  17. package/lib/commonjs/ui/screens/ManageAccountScreen.js +13 -1
  18. package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
  19. package/lib/module/ui/boot/runProviderColdBoot.js +47 -3
  20. package/lib/module/ui/boot/runProviderColdBoot.js.map +1 -1
  21. package/lib/module/ui/components/organizations/OrganizationClaimReview.js +205 -0
  22. package/lib/module/ui/components/organizations/OrganizationClaimReview.js.map +1 -0
  23. package/lib/module/ui/components/organizations/OrganizationClaims.js +73 -0
  24. package/lib/module/ui/components/organizations/OrganizationClaims.js.map +1 -0
  25. package/lib/module/ui/components/organizations/OrganizationDirectorySearch.js +215 -0
  26. package/lib/module/ui/components/organizations/OrganizationDirectorySearch.js.map +1 -0
  27. package/lib/module/ui/components/organizations/OrganizationSettings.js +143 -0
  28. package/lib/module/ui/components/organizations/OrganizationSettings.js.map +1 -0
  29. package/lib/module/ui/components/organizations/OrganizationTypePicker.js +30 -0
  30. package/lib/module/ui/components/organizations/OrganizationTypePicker.js.map +1 -0
  31. package/lib/module/ui/screens/AccountSettingsScreen.js +14 -5
  32. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  33. package/lib/module/ui/screens/CreateAccountScreen.js +149 -115
  34. package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -1
  35. package/lib/module/ui/screens/ManageAccountScreen.js +13 -1
  36. package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
  37. package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/ui/components/organizations/OrganizationClaimReview.d.ts +3 -0
  39. package/lib/typescript/commonjs/ui/components/organizations/OrganizationClaimReview.d.ts.map +1 -0
  40. package/lib/typescript/commonjs/ui/components/organizations/OrganizationClaims.d.ts +2 -0
  41. package/lib/typescript/commonjs/ui/components/organizations/OrganizationClaims.d.ts.map +1 -0
  42. package/lib/typescript/commonjs/ui/components/organizations/OrganizationDirectorySearch.d.ts +6 -0
  43. package/lib/typescript/commonjs/ui/components/organizations/OrganizationDirectorySearch.d.ts.map +1 -0
  44. package/lib/typescript/commonjs/ui/components/organizations/OrganizationSettings.d.ts +5 -0
  45. package/lib/typescript/commonjs/ui/components/organizations/OrganizationSettings.d.ts.map +1 -0
  46. package/lib/typescript/commonjs/ui/components/organizations/OrganizationTypePicker.d.ts +7 -0
  47. package/lib/typescript/commonjs/ui/components/organizations/OrganizationTypePicker.d.ts.map +1 -0
  48. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +3 -2
  49. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/ui/hooks/queries/usePersonalPlans.d.ts +2 -2
  51. package/lib/typescript/commonjs/ui/icons/Ionicons.d.ts +1 -1
  52. package/lib/typescript/commonjs/ui/icons/MaterialCommunityIcons.d.ts +1 -1
  53. package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  54. package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  56. package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts.map +1 -1
  57. package/lib/typescript/module/ui/components/organizations/OrganizationClaimReview.d.ts +3 -0
  58. package/lib/typescript/module/ui/components/organizations/OrganizationClaimReview.d.ts.map +1 -0
  59. package/lib/typescript/module/ui/components/organizations/OrganizationClaims.d.ts +2 -0
  60. package/lib/typescript/module/ui/components/organizations/OrganizationClaims.d.ts.map +1 -0
  61. package/lib/typescript/module/ui/components/organizations/OrganizationDirectorySearch.d.ts +6 -0
  62. package/lib/typescript/module/ui/components/organizations/OrganizationDirectorySearch.d.ts.map +1 -0
  63. package/lib/typescript/module/ui/components/organizations/OrganizationSettings.d.ts +5 -0
  64. package/lib/typescript/module/ui/components/organizations/OrganizationSettings.d.ts.map +1 -0
  65. package/lib/typescript/module/ui/components/organizations/OrganizationTypePicker.d.ts +7 -0
  66. package/lib/typescript/module/ui/components/organizations/OrganizationTypePicker.d.ts.map +1 -0
  67. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +3 -2
  68. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  69. package/lib/typescript/module/ui/hooks/queries/usePersonalPlans.d.ts +2 -2
  70. package/lib/typescript/module/ui/icons/Ionicons.d.ts +1 -1
  71. package/lib/typescript/module/ui/icons/MaterialCommunityIcons.d.ts +1 -1
  72. package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  73. package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  74. package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  75. package/package.json +8 -8
  76. package/src/ui/boot/__tests__/coldBootNoIdpRedirect.test.ts +166 -3
  77. package/src/ui/boot/runProviderColdBoot.ts +49 -4
  78. package/src/ui/components/organizations/OrganizationClaimReview.tsx +218 -0
  79. package/src/ui/components/organizations/OrganizationClaims.tsx +67 -0
  80. package/src/ui/components/organizations/OrganizationDirectorySearch.tsx +228 -0
  81. package/src/ui/components/organizations/OrganizationSettings.tsx +131 -0
  82. package/src/ui/components/organizations/OrganizationTypePicker.tsx +31 -0
  83. package/src/ui/screens/AccountSettingsScreen.tsx +22 -4
  84. package/src/ui/screens/CreateAccountScreen.tsx +177 -124
  85. package/src/ui/screens/ManageAccountScreen.tsx +18 -0
@@ -0,0 +1,228 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { ActivityIndicator, View } from 'react-native';
3
+ import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query';
4
+ import {
5
+ createOrganizationSchema,
6
+ organizationClaimInputSchema,
7
+ type Organization,
8
+ type OrganizationType,
9
+ } from '@oxy.so/contracts';
10
+ import { Avatar } from '@oxy.so/bloom/avatar';
11
+ import { Button } from '@oxy.so/bloom/button';
12
+ import { SettingsListGroup, SettingsListItem } from '@oxy.so/bloom/settings-list';
13
+ import { TextField, TextFieldInput } from '@oxy.so/bloom/text-field';
14
+ import { Text } from '@oxy.so/bloom/typography';
15
+ import { useOxy } from '../../context/OxyContext';
16
+ import { useI18n } from '../../hooks/useI18n';
17
+ import { OrganizationTypePicker } from './OrganizationTypePicker';
18
+
19
+ /** Finding an existing identity never grants ownership or silently creates an account. */
20
+ export function OrganizationDirectorySearch({
21
+ onCreateAccount,
22
+ }: {
23
+ onCreateAccount: (name: string, type: OrganizationType) => void;
24
+ }) {
25
+ const { oxyServices, user, canUsePrivateApi } = useOxy();
26
+ const { t } = useI18n();
27
+ const queryClient = useQueryClient();
28
+ const [search, setSearch] = useState('');
29
+ const [query, setQuery] = useState('');
30
+ const [selected, setSelected] = useState<Organization | null>(null);
31
+ const [adding, setAdding] = useState(false);
32
+ const [type, setType] = useState<OrganizationType>('business');
33
+ const [evidence, setEvidence] = useState('');
34
+ const [validationError, setValidationError] = useState(false);
35
+ const personalReady = canUsePrivateApi && user?.kind === 'personal';
36
+ useEffect(() => {
37
+ const timer = setTimeout(() => setQuery(search.trim()), 300);
38
+ return () => clearTimeout(timer);
39
+ }, [search]);
40
+ const results = useInfiniteQuery({
41
+ queryKey: ['oxy', 'organizations', 'search', query],
42
+ queryFn: ({ pageParam }) =>
43
+ oxyServices.organizations.search({
44
+ query,
45
+ limit: 20,
46
+ ...(pageParam ? { cursor: pageParam } : {}),
47
+ }),
48
+ initialPageParam: undefined as string | undefined,
49
+ getNextPageParam: (page) => page.nextCursor ?? undefined,
50
+ enabled: query.length > 0,
51
+ retry: false,
52
+ });
53
+ const claim = useMutation({
54
+ mutationFn: () => {
55
+ if (!personalReady || !selected || selected.status !== 'active')
56
+ throw new Error('An active organization and personal session are required');
57
+ return oxyServices.organizations.claim(
58
+ selected.accountId,
59
+ organizationClaimInputSchema.parse({ evidenceReference: evidence }),
60
+ );
61
+ },
62
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['oxy', 'organizations'] }),
63
+ });
64
+ const create = useMutation({
65
+ mutationFn: () => {
66
+ if (!personalReady) throw new Error('A personal session is required');
67
+ return oxyServices.organizations.create(
68
+ createOrganizationSchema.parse({ name: search.trim(), organizationType: type }),
69
+ );
70
+ },
71
+ onSuccess: (organization) => {
72
+ setSelected(organization);
73
+ setAdding(false);
74
+ void queryClient.invalidateQueries({ queryKey: ['oxy', 'organizations'] });
75
+ },
76
+ });
77
+ const reset = () => {
78
+ setSelected(null);
79
+ setAdding(false);
80
+ setEvidence('');
81
+ claim.reset();
82
+ create.reset();
83
+ setValidationError(false);
84
+ };
85
+ if (selected)
86
+ return (
87
+ <SettingsListGroup title={selected.name}>
88
+ <View className="gap-space-16 p-space-16">
89
+ <Text>{t(`organizationDirectory.${selected.claimStatus}`)}</Text>
90
+ {selected.status === 'inactive' ? (
91
+ <Text>{t('organizationDirectory.inactive')}</Text>
92
+ ) : null}
93
+ {selected.claimStatus === 'unclaimed' ? (
94
+ <>
95
+ <Text>{t('organizationDirectory.claimHint')}</Text>
96
+ <TextField>
97
+ <TextFieldInput
98
+ label={t('organizationDirectory.evidence')}
99
+ value={evidence}
100
+ onChangeText={setEvidence}
101
+ maxLength={1000}
102
+ />
103
+ </TextField>
104
+ <Text>{t('organizationDirectory.evidenceHint')}</Text>
105
+ {claim.isSuccess ? (
106
+ <Text accessibilityRole="alert">{t('organizationDirectory.claimSent')}</Text>
107
+ ) : (
108
+ <Button
109
+ onPress={() => claim.mutate()}
110
+ disabled={
111
+ !personalReady ||
112
+ selected.status !== 'active' ||
113
+ !evidence.trim() ||
114
+ claim.isPending
115
+ }
116
+ loading={claim.isPending}
117
+ >
118
+ {t('organizationDirectory.claim')}
119
+ </Button>
120
+ )}
121
+ {claim.isError ? (
122
+ <Text accessibilityRole="alert">{t('organizationDirectory.claimFailed')}</Text>
123
+ ) : null}
124
+ </>
125
+ ) : (
126
+ <Text>{t('organizationDirectory.claimedHint')}</Text>
127
+ )}
128
+ <Button appearance="outline" onPress={reset} disabled={claim.isPending}>
129
+ {t('organizationDirectory.back')}
130
+ </Button>
131
+ </View>
132
+ </SettingsListGroup>
133
+ );
134
+ return (
135
+ <View className="gap-space-16">
136
+ <TextField>
137
+ <TextFieldInput
138
+ label={t('organizationDirectory.search')}
139
+ value={search}
140
+ onChangeText={(value) => {
141
+ setSearch(value);
142
+ setValidationError(false);
143
+ }}
144
+ maxLength={200}
145
+ />
146
+ </TextField>
147
+ {query && results.isPending ? <ActivityIndicator /> : null}
148
+ {results.isError ? (
149
+ <View>
150
+ <Text accessibilityRole="alert">{t('organizationDirectory.failed')}</Text>
151
+ <Button onPress={() => results.refetch()}>{t('organizationDirectory.retry')}</Button>
152
+ </View>
153
+ ) : null}
154
+ {results.data ? (
155
+ <SettingsListGroup title={t('organizationDirectory.title')}>
156
+ {results.data.pages
157
+ .flatMap((page) => page.items)
158
+ .map((organization) => (
159
+ <SettingsListItem
160
+ key={organization.accountId}
161
+ title={organization.name}
162
+ description={[
163
+ organization.websiteUrl,
164
+ organization.countryCode,
165
+ t(`organizationDirectory.${organization.claimStatus}`),
166
+ ]
167
+ .filter(Boolean)
168
+ .join(' · ')}
169
+ icon={<Avatar source={organization.avatar} name={organization.name} size={40} />}
170
+ onPress={() => setSelected(organization)}
171
+ />
172
+ ))}
173
+ {results.data.pages[0].items.length === 0 ? (
174
+ <Text>{t('organizationDirectory.empty')}</Text>
175
+ ) : null}
176
+ {results.hasNextPage ? (
177
+ <Button onPress={() => results.fetchNextPage()} disabled={results.isFetchingNextPage}>
178
+ {t('organizationDirectory.loadMore')}
179
+ </Button>
180
+ ) : null}
181
+ </SettingsListGroup>
182
+ ) : null}
183
+ {results.isSuccess && query === search.trim() ? (
184
+ <>
185
+ <Text>{t('organizationDirectory.newAccountHint')}</Text>
186
+ <Button onPress={() => onCreateAccount(search.trim(), type)}>
187
+ {t('organizationDirectory.newAccount')}
188
+ </Button>
189
+ {personalReady && !adding ? (
190
+ <Button appearance="outline" onPress={() => setAdding(true)}>
191
+ {t('organizationDirectory.addDirectory')}
192
+ </Button>
193
+ ) : null}
194
+ </>
195
+ ) : null}
196
+ {adding ? (
197
+ <>
198
+ <Text>{t('organizationDirectory.directoryHint')}</Text>
199
+ <OrganizationTypePicker value={type} onChange={setType} disabled={create.isPending} />
200
+ {validationError ? (
201
+ <Text accessibilityRole="alert">{t('organizationDirectory.invalid')}</Text>
202
+ ) : null}
203
+ {create.isError ? (
204
+ <Text accessibilityRole="alert">{t('organizationDirectory.saveFailed')}</Text>
205
+ ) : null}
206
+ <Button
207
+ disabled={
208
+ !personalReady || create.isPending || !results.isSuccess || query !== search.trim()
209
+ }
210
+ loading={create.isPending}
211
+ onPress={() => {
212
+ if (
213
+ !createOrganizationSchema.safeParse({ name: search.trim(), organizationType: type })
214
+ .success
215
+ ) {
216
+ setValidationError(true);
217
+ return;
218
+ }
219
+ create.mutate();
220
+ }}
221
+ >
222
+ {t('organizationDirectory.addDirectory')}
223
+ </Button>
224
+ </>
225
+ ) : null}
226
+ </View>
227
+ );
228
+ }
@@ -0,0 +1,131 @@
1
+ import { useState } from 'react';
2
+ import { ActivityIndicator, View } from 'react-native';
3
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
+ import {
5
+ updateOrganizationSchema,
6
+ type Organization,
7
+ type OrganizationType,
8
+ } from '@oxy.so/contracts';
9
+ import { Button } from '@oxy.so/bloom/button';
10
+ import { toast } from '@oxy.so/bloom/toast';
11
+ import { SettingsListGroup } from '@oxy.so/bloom/settings-list';
12
+ import { TextField, TextFieldInput } from '@oxy.so/bloom/text-field';
13
+ import { Text } from '@oxy.so/bloom/typography';
14
+ import { useOxy } from '../../context/OxyContext';
15
+ import { useI18n } from '../../hooks/useI18n';
16
+ import { OrganizationTypePicker } from './OrganizationTypePicker';
17
+
18
+ export function OrganizationSettings({
19
+ accountId,
20
+ canUpdate,
21
+ }: {
22
+ accountId: string;
23
+ canUpdate: boolean;
24
+ }) {
25
+ const { oxyServices, canUsePrivateApi } = useOxy();
26
+ const { t } = useI18n();
27
+ const organization = useQuery({
28
+ queryKey: ['oxy', 'organizations', accountId],
29
+ queryFn: () => oxyServices.organizations.get(accountId),
30
+ retry: false,
31
+ });
32
+ if (organization.isPending) return <ActivityIndicator />;
33
+ if (organization.isError)
34
+ return (
35
+ <View>
36
+ <Text accessibilityRole="alert">{t('organizationDirectory.failed')}</Text>
37
+ <Button onPress={() => organization.refetch()}>{t('organizationDirectory.retry')}</Button>
38
+ </View>
39
+ );
40
+ if (!canUpdate || !canUsePrivateApi)
41
+ return (
42
+ <SettingsListGroup title={t('organizationDirectory.title')}>
43
+ <Text>{t(`organizationDirectory.types.${organization.data.organizationType}`)}</Text>
44
+ <Text>{t('organizationDirectory.readonly')}</Text>
45
+ </SettingsListGroup>
46
+ );
47
+ return (
48
+ <OrganizationForm
49
+ key={`${accountId}:${organization.data.updatedAt}`}
50
+ organization={organization.data}
51
+ />
52
+ );
53
+ }
54
+
55
+ function OrganizationForm({ organization }: { organization: Organization }) {
56
+ const { oxyServices, canUsePrivateApi } = useOxy();
57
+ const { t } = useI18n();
58
+ const queryClient = useQueryClient();
59
+ const [legalName, setLegalName] = useState(organization.legalName ?? '');
60
+ const [website, setWebsite] = useState(organization.websiteUrl ?? '');
61
+ const [country, setCountry] = useState(organization.countryCode ?? '');
62
+ const [type, setType] = useState<OrganizationType>(organization.organizationType);
63
+ const [invalid, setInvalid] = useState(false);
64
+ const save = useMutation({
65
+ mutationFn: (input: ReturnType<typeof updateOrganizationSchema.parse>) => {
66
+ if (!canUsePrivateApi) throw new Error('An authenticated session is required');
67
+ return oxyServices.organizations.update(organization.accountId, input);
68
+ },
69
+ onSuccess: () => {
70
+ toast.success(t('organizationDirectory.saved'));
71
+ void queryClient.invalidateQueries({ queryKey: ['oxy', 'organizations'] });
72
+ void queryClient.invalidateQueries({ queryKey: ['accounts'] });
73
+ void queryClient.invalidateQueries({ queryKey: ['users'] });
74
+ },
75
+ });
76
+ return (
77
+ <SettingsListGroup title={t('organizationDirectory.title')}>
78
+ <View className="gap-space-16 p-space-16">
79
+ <TextField>
80
+ <TextFieldInput
81
+ label={t('organizationDirectory.legalName')}
82
+ value={legalName}
83
+ onChangeText={setLegalName}
84
+ maxLength={300}
85
+ />
86
+ </TextField>
87
+ <TextField>
88
+ <TextFieldInput
89
+ label={t('organizationDirectory.website')}
90
+ value={website}
91
+ onChangeText={setWebsite}
92
+ autoCapitalize="none"
93
+ maxLength={2048}
94
+ />
95
+ </TextField>
96
+ <TextField>
97
+ <TextFieldInput
98
+ label={t('organizationDirectory.country')}
99
+ value={country}
100
+ onChangeText={setCountry}
101
+ autoCapitalize="characters"
102
+ maxLength={2}
103
+ />
104
+ </TextField>
105
+ <OrganizationTypePicker value={type} onChange={setType} disabled={save.isPending} />
106
+ {invalid ? (
107
+ <Text accessibilityRole="alert">{t('organizationDirectory.invalid')}</Text>
108
+ ) : null}
109
+ {save.isError ? (
110
+ <Text accessibilityRole="alert">{t('organizationDirectory.saveFailed')}</Text>
111
+ ) : null}
112
+ <Button
113
+ disabled={save.isPending || !canUsePrivateApi}
114
+ loading={save.isPending}
115
+ onPress={() => {
116
+ const input = updateOrganizationSchema.safeParse({
117
+ legalName: legalName.trim() || null,
118
+ websiteUrl: website.trim() || null,
119
+ countryCode: country.trim().toUpperCase() || null,
120
+ organizationType: type,
121
+ });
122
+ setInvalid(!input.success);
123
+ if (input.success) save.mutate(input.data);
124
+ }}
125
+ >
126
+ {t('organizationDirectory.save')}
127
+ </Button>
128
+ </View>
129
+ </SettingsListGroup>
130
+ );
131
+ }
@@ -0,0 +1,31 @@
1
+ import { ORGANIZATION_TYPES, type OrganizationType } from '@oxy.so/contracts';
2
+ import { SettingsListGroup, SettingsListItem } from '@oxy.so/bloom/settings-list';
3
+ import { Text } from '@oxy.so/bloom/typography';
4
+ import { useI18n } from '../../hooks/useI18n';
5
+
6
+ export function OrganizationTypePicker({
7
+ value,
8
+ onChange,
9
+ disabled = false,
10
+ }: {
11
+ value: OrganizationType;
12
+ onChange: (value: OrganizationType) => void;
13
+ disabled?: boolean;
14
+ }) {
15
+ const { t } = useI18n();
16
+ return (
17
+ <SettingsListGroup title={t('organizationDirectory.type')}>
18
+ {ORGANIZATION_TYPES.map((type) => (
19
+ <SettingsListItem
20
+ key={type}
21
+ title={t(`organizationDirectory.types.${type}`)}
22
+ accessibilityLabel={t(`organizationDirectory.types.${type}`)}
23
+ disabled={disabled}
24
+ onPress={() => onChange(type)}
25
+ showChevron={false}
26
+ rightElement={value === type ? <Text>✓</Text> : undefined}
27
+ />
28
+ ))}
29
+ </SettingsListGroup>
30
+ );
31
+ }
@@ -1,4 +1,6 @@
1
1
  import type React from 'react';
2
+ import { ORGANIZATION_NAME_MAX_LENGTH, organizationNameSchema } from '@oxy.so/contracts';
3
+ import { OrganizationSettings } from '../components/organizations/OrganizationSettings';
2
4
  import { useCallback, useMemo, useState } from 'react';
3
5
  import { View, ActivityIndicator } from 'react-native';
4
6
  import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
@@ -52,12 +54,12 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
52
54
  const id = typeof accountId === 'string' ? accountId : '';
53
55
 
54
56
  const accountQuery = useQuery({
55
- queryKey: ['accounts', 'detail', id],
57
+ queryKey: ['accounts', 'detail', id, user?.id, canUsePrivateApi],
56
58
  queryFn: () => oxyServices.accounts.get(id),
57
59
  enabled: canUsePrivateApi && id.length > 0,
58
60
  });
59
61
 
60
- const node = accountQuery.data ?? null;
62
+ const node = canUsePrivateApi && accountQuery.isSuccess ? accountQuery.data : null;
61
63
  const permissions = node?.callerMembership?.permissions ?? [];
62
64
  const isImplicitOwner = node ? node.relationship !== 'member' : false;
63
65
  const can = useCallback(
@@ -148,6 +150,14 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
148
150
  setDisplayNameError('');
149
151
  return;
150
152
  }
153
+ if (node?.account.kind === 'organization') {
154
+ setDisplayNameError(
155
+ organizationNameSchema.safeParse(trimmed).success
156
+ ? ''
157
+ : t('organizationDirectory.invalidName'),
158
+ );
159
+ return;
160
+ }
151
161
  const nameParts = trimmed.split(/\s+/).filter(Boolean);
152
162
  const first = nameParts[0] || '';
153
163
  const last = nameParts.length > 1 ? nameParts.slice(1).join(' ') : '';
@@ -158,7 +168,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
158
168
  : '',
159
169
  );
160
170
  },
161
- [t],
171
+ [node?.account.kind, t],
162
172
  );
163
173
 
164
174
  const handleSave = useCallback(() => {
@@ -219,7 +229,11 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
219
229
  value={displayName}
220
230
  onChangeText={handleDisplayNameChange}
221
231
  isInvalid={Boolean(displayNameError)}
222
- maxLength={DISPLAY_NAME_MAX}
232
+ maxLength={
233
+ node?.account.kind === 'organization'
234
+ ? ORGANIZATION_NAME_MAX_LENGTH
235
+ : DISPLAY_NAME_MAX
236
+ }
223
237
  />
224
238
  </TextField>
225
239
  {displayNameError ? (
@@ -261,6 +275,10 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
261
275
  </View>
262
276
  ) : null}
263
277
 
278
+ {node?.account.kind === 'organization' ? (
279
+ <OrganizationSettings key={id} accountId={id} canUpdate={canUpdate} />
280
+ ) : null}
281
+
264
282
  {/* Members entry */}
265
283
  {canViewMembers ? (
266
284
  <SettingsListGroup title={t('accounts.settings.sections.access') || 'Access'}>