@oxy.so/services 11.2.0 → 11.4.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 +28 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/AccountInvitationAcceptance.js +97 -0
- package/lib/commonjs/ui/components/AccountInvitationAcceptance.js.map +1 -0
- package/lib/commonjs/ui/components/DisplayCurrencySelector.js +49 -0
- package/lib/commonjs/ui/components/DisplayCurrencySelector.js.map +1 -0
- package/lib/commonjs/ui/components/organizations/OrganizationClaimReview.js +208 -0
- package/lib/commonjs/ui/components/organizations/OrganizationClaimReview.js.map +1 -0
- package/lib/commonjs/ui/components/organizations/OrganizationClaims.js +77 -0
- package/lib/commonjs/ui/components/organizations/OrganizationClaims.js.map +1 -0
- package/lib/commonjs/ui/components/organizations/OrganizationDirectorySearch.js +218 -0
- package/lib/commonjs/ui/components/organizations/OrganizationDirectorySearch.js.map +1 -0
- package/lib/commonjs/ui/components/organizations/OrganizationSettings.js +147 -0
- package/lib/commonjs/ui/components/organizations/OrganizationSettings.js.map +1 -0
- package/lib/commonjs/ui/components/organizations/OrganizationTypePicker.js +34 -0
- package/lib/commonjs/ui/components/organizations/OrganizationTypePicker.js.map +1 -0
- package/lib/commonjs/ui/hooks/useDisplayCurrency.js +77 -0
- package/lib/commonjs/ui/hooks/useDisplayCurrency.js.map +1 -0
- package/lib/commonjs/ui/hooks/useDisplayPrice.js +59 -0
- package/lib/commonjs/ui/hooks/useDisplayPrice.js.map +1 -0
- package/lib/commonjs/ui/screens/AccountMembersScreen.js +63 -20
- package/lib/commonjs/ui/screens/AccountMembersScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js +14 -5
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/CreateAccountScreen.js +148 -115
- package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/ManageAccountScreen.js +13 -1
- package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/PreferencesScreen.js +4 -3
- package/lib/commonjs/ui/screens/PreferencesScreen.js.map +1 -1
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/AccountInvitationAcceptance.js +94 -0
- package/lib/module/ui/components/AccountInvitationAcceptance.js.map +1 -0
- package/lib/module/ui/components/DisplayCurrencySelector.js +44 -0
- package/lib/module/ui/components/DisplayCurrencySelector.js.map +1 -0
- package/lib/module/ui/components/organizations/OrganizationClaimReview.js +205 -0
- package/lib/module/ui/components/organizations/OrganizationClaimReview.js.map +1 -0
- package/lib/module/ui/components/organizations/OrganizationClaims.js +73 -0
- package/lib/module/ui/components/organizations/OrganizationClaims.js.map +1 -0
- package/lib/module/ui/components/organizations/OrganizationDirectorySearch.js +215 -0
- package/lib/module/ui/components/organizations/OrganizationDirectorySearch.js.map +1 -0
- package/lib/module/ui/components/organizations/OrganizationSettings.js +143 -0
- package/lib/module/ui/components/organizations/OrganizationSettings.js.map +1 -0
- package/lib/module/ui/components/organizations/OrganizationTypePicker.js +30 -0
- package/lib/module/ui/components/organizations/OrganizationTypePicker.js.map +1 -0
- package/lib/module/ui/hooks/useDisplayCurrency.js +72 -0
- package/lib/module/ui/hooks/useDisplayCurrency.js.map +1 -0
- package/lib/module/ui/hooks/useDisplayPrice.js +56 -0
- package/lib/module/ui/hooks/useDisplayPrice.js.map +1 -0
- package/lib/module/ui/screens/AccountMembersScreen.js +63 -20
- package/lib/module/ui/screens/AccountMembersScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js +14 -5
- package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/module/ui/screens/CreateAccountScreen.js +149 -115
- package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -1
- package/lib/module/ui/screens/ManageAccountScreen.js +13 -1
- package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/module/ui/screens/PreferencesScreen.js +4 -3
- package/lib/module/ui/screens/PreferencesScreen.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/AccountInvitationAcceptance.d.ts +5 -0
- package/lib/typescript/commonjs/ui/components/AccountInvitationAcceptance.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/DisplayCurrencySelector.d.ts +3 -0
- package/lib/typescript/commonjs/ui/components/DisplayCurrencySelector.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/organizations/OrganizationClaimReview.d.ts +3 -0
- package/lib/typescript/commonjs/ui/components/organizations/OrganizationClaimReview.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/organizations/OrganizationClaims.d.ts +2 -0
- package/lib/typescript/commonjs/ui/components/organizations/OrganizationClaims.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/organizations/OrganizationDirectorySearch.d.ts +6 -0
- package/lib/typescript/commonjs/ui/components/organizations/OrganizationDirectorySearch.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/organizations/OrganizationSettings.d.ts +5 -0
- package/lib/typescript/commonjs/ui/components/organizations/OrganizationSettings.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/organizations/OrganizationTypePicker.d.ts +7 -0
- package/lib/typescript/commonjs/ui/components/organizations/OrganizationTypePicker.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +10 -3
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/usePersonalPlans.d.ts +4 -3
- package/lib/typescript/commonjs/ui/hooks/queries/usePersonalPlans.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useDisplayCurrency.d.ts +11 -0
- package/lib/typescript/commonjs/ui/hooks/useDisplayCurrency.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/useDisplayPrice.d.ts +6 -0
- package/lib/typescript/commonjs/ui/hooks/useDisplayPrice.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/icons/Ionicons.d.ts +1 -1
- package/lib/typescript/commonjs/ui/icons/MaterialCommunityIcons.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/AccountMembersScreen.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/AccountMembersScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/PreferencesScreen.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/AccountInvitationAcceptance.d.ts +5 -0
- package/lib/typescript/module/ui/components/AccountInvitationAcceptance.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/DisplayCurrencySelector.d.ts +3 -0
- package/lib/typescript/module/ui/components/DisplayCurrencySelector.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/organizations/OrganizationClaimReview.d.ts +3 -0
- package/lib/typescript/module/ui/components/organizations/OrganizationClaimReview.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/organizations/OrganizationClaims.d.ts +2 -0
- package/lib/typescript/module/ui/components/organizations/OrganizationClaims.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/organizations/OrganizationDirectorySearch.d.ts +6 -0
- package/lib/typescript/module/ui/components/organizations/OrganizationDirectorySearch.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/organizations/OrganizationSettings.d.ts +5 -0
- package/lib/typescript/module/ui/components/organizations/OrganizationSettings.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/organizations/OrganizationTypePicker.d.ts +7 -0
- package/lib/typescript/module/ui/components/organizations/OrganizationTypePicker.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +10 -3
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/usePersonalPlans.d.ts +4 -3
- package/lib/typescript/module/ui/hooks/queries/usePersonalPlans.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useDisplayCurrency.d.ts +11 -0
- package/lib/typescript/module/ui/hooks/useDisplayCurrency.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/useDisplayPrice.d.ts +6 -0
- package/lib/typescript/module/ui/hooks/useDisplayPrice.d.ts.map +1 -0
- package/lib/typescript/module/ui/icons/Ionicons.d.ts +1 -1
- package/lib/typescript/module/ui/icons/MaterialCommunityIcons.d.ts +1 -1
- package/lib/typescript/module/ui/screens/AccountMembersScreen.d.ts +1 -1
- package/lib/typescript/module/ui/screens/AccountMembersScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/PreferencesScreen.d.ts.map +1 -1
- package/package.json +9 -8
- package/src/index.ts +5 -0
- package/src/ui/components/AccountInvitationAcceptance.tsx +90 -0
- package/src/ui/components/DisplayCurrencySelector.tsx +38 -0
- package/src/ui/components/__tests__/AccountInvitationAcceptance.test.tsx +61 -0
- package/src/ui/components/organizations/OrganizationClaimReview.tsx +218 -0
- package/src/ui/components/organizations/OrganizationClaims.tsx +67 -0
- package/src/ui/components/organizations/OrganizationDirectorySearch.tsx +228 -0
- package/src/ui/components/organizations/OrganizationSettings.tsx +131 -0
- package/src/ui/components/organizations/OrganizationTypePicker.tsx +31 -0
- package/src/ui/hooks/useDisplayCurrency.ts +64 -0
- package/src/ui/hooks/useDisplayPrice.ts +40 -0
- package/src/ui/screens/AccountMembersScreen.tsx +71 -27
- package/src/ui/screens/AccountSettingsScreen.tsx +22 -4
- package/src/ui/screens/CreateAccountScreen.tsx +177 -124
- package/src/ui/screens/ManageAccountScreen.tsx +18 -0
- package/src/ui/screens/PreferencesScreen.tsx +2 -0
|
@@ -30,13 +30,8 @@ const ASSIGNABLE_ROLES: AssignableRole[] = ['admin', 'editor', 'developer', 'bil
|
|
|
30
30
|
const errorMessage = (error: unknown, fallback: string): string =>
|
|
31
31
|
error instanceof Error && error.message ? error.message : fallback;
|
|
32
32
|
|
|
33
|
-
const isUserNotFound = (error: unknown): boolean => {
|
|
34
|
-
const message = error instanceof Error ? error.message.toLowerCase() : '';
|
|
35
|
-
return message.includes('user not found') || message.includes('not found');
|
|
36
|
-
};
|
|
37
|
-
|
|
38
33
|
/**
|
|
39
|
-
* Manage the members of an account (invite by
|
|
34
|
+
* Manage the members of an account (invite by email, change roles,
|
|
40
35
|
* remove, transfer ownership). Gated on the caller's `callerMembership`
|
|
41
36
|
* permissions. Receives the target `accountId` via navigation props.
|
|
42
37
|
*/
|
|
@@ -86,6 +81,27 @@ const AccountMembersScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, acco
|
|
|
86
81
|
queryFn: () => oxyServices.accounts.members.list(id),
|
|
87
82
|
enabled: canUsePrivateApi && id.length > 0 && canRead,
|
|
88
83
|
});
|
|
84
|
+
const invitationsQuery = useQuery({
|
|
85
|
+
queryKey: ['account-invitations', id],
|
|
86
|
+
queryFn: () => oxyServices.accounts.invitations.list(id),
|
|
87
|
+
enabled: canUsePrivateApi && !!id && canRead,
|
|
88
|
+
});
|
|
89
|
+
const invitationAction = useMutation({
|
|
90
|
+
mutationFn: async ({
|
|
91
|
+
invitationId,
|
|
92
|
+
action,
|
|
93
|
+
}: {
|
|
94
|
+
invitationId: string;
|
|
95
|
+
action: 'resend' | 'cancel';
|
|
96
|
+
}) => {
|
|
97
|
+
if (action === 'resend') await oxyServices.accounts.invitations.resend(id, invitationId);
|
|
98
|
+
else await oxyServices.accounts.invitations.cancel(id, invitationId);
|
|
99
|
+
},
|
|
100
|
+
onSuccess: () => {
|
|
101
|
+
void invitationsQuery.refetch();
|
|
102
|
+
},
|
|
103
|
+
onError: (error) => toast.error(errorMessage(error, 'Could not update invitation')),
|
|
104
|
+
});
|
|
89
105
|
const members = useMemo<AccountMember[]>(() => membersQuery.data ?? [], [membersQuery.data]);
|
|
90
106
|
const memberUserIds = useMemo(
|
|
91
107
|
() => [...new Set(members.map((member) => member.memberUserId).filter((id) => id.length > 0))],
|
|
@@ -126,6 +142,7 @@ const AccountMembersScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, acco
|
|
|
126
142
|
// tables — mirror the SDK's per-account prefix sweep so every cached member
|
|
127
143
|
// list refetches, not only the account that was mutated.
|
|
128
144
|
queryClient.invalidateQueries({ queryKey: ['accounts', 'members'] });
|
|
145
|
+
queryClient.invalidateQueries({ queryKey: ['account-invitations', id] });
|
|
129
146
|
queryClient.invalidateQueries({ queryKey: ['accounts', 'detail', id] });
|
|
130
147
|
}, [queryClient, id]);
|
|
131
148
|
|
|
@@ -137,24 +154,20 @@ const AccountMembersScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, acco
|
|
|
137
154
|
const inviteMutation = useMutation({
|
|
138
155
|
mutationKey: ['accounts', 'members', 'invite', id],
|
|
139
156
|
mutationFn: (input: { usernameOrEmail: string; role: AssignableRole }) =>
|
|
140
|
-
oxyServices.accounts.
|
|
157
|
+
oxyServices.accounts.invitations.create(id, {
|
|
158
|
+
email: input.usernameOrEmail,
|
|
159
|
+
role: input.role,
|
|
160
|
+
}),
|
|
141
161
|
onSuccess: () => {
|
|
142
162
|
setInviteIdentifier('');
|
|
143
163
|
setInviteRole('editor');
|
|
144
164
|
setInviteError(null);
|
|
145
165
|
invalidateMembers();
|
|
146
|
-
toast.success(
|
|
166
|
+
toast.success('Invitation sent');
|
|
147
167
|
},
|
|
148
168
|
onError: (error) => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
setInviteError(message);
|
|
152
|
-
toast.error(message);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
toast.error(
|
|
156
|
-
errorMessage(error, t('accounts.members.toasts.addFailed') || 'Failed to add member'),
|
|
157
|
-
);
|
|
169
|
+
invalidateMembers();
|
|
170
|
+
toast.error(errorMessage(error, 'Could not send invitation'));
|
|
158
171
|
},
|
|
159
172
|
});
|
|
160
173
|
|
|
@@ -207,14 +220,12 @@ const AccountMembersScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, acco
|
|
|
207
220
|
const handleInvite = useCallback(() => {
|
|
208
221
|
const usernameOrEmail = inviteIdentifier.trim();
|
|
209
222
|
if (!usernameOrEmail) {
|
|
210
|
-
setInviteError(
|
|
211
|
-
t('accounts.members.errors.identifierRequired') || 'Enter a username or email to invite',
|
|
212
|
-
);
|
|
223
|
+
setInviteError('Enter an email to invite');
|
|
213
224
|
return;
|
|
214
225
|
}
|
|
215
226
|
setInviteError(null);
|
|
216
227
|
inviteMutation.mutate({ usernameOrEmail, role: inviteRole });
|
|
217
|
-
}, [inviteIdentifier, inviteRole, inviteMutation
|
|
228
|
+
}, [inviteIdentifier, inviteRole, inviteMutation]);
|
|
218
229
|
|
|
219
230
|
const handleChangeRole = useCallback(
|
|
220
231
|
(member: AccountMember, role: AccountRole) => {
|
|
@@ -282,13 +293,11 @@ const AccountMembersScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, acco
|
|
|
282
293
|
{/* Invite form */}
|
|
283
294
|
{canInvite ? (
|
|
284
295
|
<View className="gap-space-12 p-space-16 rounded-radius-20 bg-fill">
|
|
285
|
-
<Text className="text-body font-semibold text-text">
|
|
286
|
-
{t('accounts.members.invite.title') || 'Add a member'}
|
|
287
|
-
</Text>
|
|
296
|
+
<Text className="text-body font-semibold text-text">{'Invite a member'}</Text>
|
|
288
297
|
<TextField isInvalid={!!inviteError}>
|
|
289
298
|
<TextFieldInput
|
|
290
299
|
floatingLabel
|
|
291
|
-
label={
|
|
300
|
+
label={'Email'}
|
|
292
301
|
value={inviteIdentifier}
|
|
293
302
|
onChangeText={(value) => {
|
|
294
303
|
setInviteIdentifier(value);
|
|
@@ -332,14 +341,49 @@ const AccountMembersScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, acco
|
|
|
332
341
|
onPress={handleInvite}
|
|
333
342
|
disabled={inviteMutation.isPending || !inviteIdentifier.trim()}
|
|
334
343
|
loading={inviteMutation.isPending}
|
|
335
|
-
accessibilityLabel={
|
|
344
|
+
accessibilityLabel={'Send invitation'}
|
|
336
345
|
className="w-full"
|
|
337
346
|
>
|
|
338
|
-
{
|
|
347
|
+
{'Send invitation'}
|
|
339
348
|
</Button>
|
|
340
349
|
</View>
|
|
341
350
|
) : null}
|
|
342
351
|
|
|
352
|
+
{canRead ? (
|
|
353
|
+
<View className="gap-space-12">
|
|
354
|
+
<Text className="text-body font-semibold text-text">Pending invitations</Text>
|
|
355
|
+
{invitationsQuery.isError ? <Text>Could not load invitations.</Text> : null}
|
|
356
|
+
{(invitationsQuery.data ?? []).map((invitation) => (
|
|
357
|
+
<View key={invitation.id} className="gap-space-8 p-space-16 rounded-radius-20 bg-fill">
|
|
358
|
+
<Text>
|
|
359
|
+
{invitation.email} · {roleLabel(invitation.role)} · {invitation.status}
|
|
360
|
+
</Text>
|
|
361
|
+
<Text>Expires {new Date(invitation.expiresAt).toLocaleDateString()}</Text>
|
|
362
|
+
{canInvite ? (
|
|
363
|
+
<Button
|
|
364
|
+
disabled={invitationAction.isPending}
|
|
365
|
+
onPress={() =>
|
|
366
|
+
invitationAction.mutate({ invitationId: invitation.id, action: 'resend' })
|
|
367
|
+
}
|
|
368
|
+
>
|
|
369
|
+
Resend
|
|
370
|
+
</Button>
|
|
371
|
+
) : null}
|
|
372
|
+
{canRemove ? (
|
|
373
|
+
<Button
|
|
374
|
+
disabled={invitationAction.isPending}
|
|
375
|
+
onPress={() =>
|
|
376
|
+
invitationAction.mutate({ invitationId: invitation.id, action: 'cancel' })
|
|
377
|
+
}
|
|
378
|
+
>
|
|
379
|
+
Cancel invitation
|
|
380
|
+
</Button>
|
|
381
|
+
) : null}
|
|
382
|
+
</View>
|
|
383
|
+
))}
|
|
384
|
+
</View>
|
|
385
|
+
) : null}
|
|
386
|
+
|
|
343
387
|
{/* Member list */}
|
|
344
388
|
{canRead ? (
|
|
345
389
|
<View className="gap-space-8">
|
|
@@ -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
|
|
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={
|
|
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'}>
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ORGANIZATION_NAME_MAX_LENGTH,
|
|
4
|
+
organizationNameSchema,
|
|
5
|
+
type OrganizationType,
|
|
6
|
+
} from '@oxy.so/contracts';
|
|
7
|
+
import { OrganizationDirectorySearch } from '../components/organizations/OrganizationDirectorySearch';
|
|
8
|
+
import { OrganizationTypePicker } from '../components/organizations/OrganizationTypePicker';
|
|
2
9
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
3
10
|
import { View, ActivityIndicator } from 'react-native';
|
|
4
11
|
import Ionicons from '../icons/Ionicons';
|
|
@@ -114,19 +121,21 @@ const ACCOUNT_CATEGORY_OPTIONS: readonly AccountCategoryId[] = SELECTABLE_ACCOUN
|
|
|
114
121
|
*/
|
|
115
122
|
const CreateAccountScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, parentAccountId }) => {
|
|
116
123
|
const bloomTheme = useTheme();
|
|
117
|
-
const { oxyServices, createAccount, switchToAccount } = useOxy();
|
|
124
|
+
const { oxyServices, createAccount, switchToAccount, user, canUsePrivateApi } = useOxy();
|
|
118
125
|
const { t, locale } = useI18n();
|
|
119
126
|
|
|
120
|
-
useSurfaceHeader({
|
|
121
|
-
title: t('accounts.create.title') || 'Create account',
|
|
122
|
-
subtitle:
|
|
123
|
-
t('accounts.create.subtitle') ||
|
|
124
|
-
'Create an account you control. It will have its own profile, members, and apps.',
|
|
125
|
-
});
|
|
126
|
-
|
|
127
127
|
const parentId = typeof parentAccountId === 'string' ? parentAccountId : undefined;
|
|
128
128
|
|
|
129
129
|
const [kind, setKind] = useState<CreatableAccountKind>('project');
|
|
130
|
+
const [organizationType, setOrganizationType] = useState<OrganizationType>('business');
|
|
131
|
+
const [newOrganization, setNewOrganization] = useState(false);
|
|
132
|
+
const findingOrganization = kind === 'organization' && !newOrganization;
|
|
133
|
+
useSurfaceHeader({
|
|
134
|
+
title: t(findingOrganization ? 'organizationDirectory.title' : 'accounts.create.title'),
|
|
135
|
+
subtitle: t(
|
|
136
|
+
findingOrganization ? 'organizationDirectory.searchHint' : 'accounts.create.subtitle',
|
|
137
|
+
),
|
|
138
|
+
});
|
|
130
139
|
/**
|
|
131
140
|
* ORDER IS THE DATA: index 0 is the primary category. Selecting appends,
|
|
132
141
|
* de-selecting removes, and neither sorts — so the list the user assembles is
|
|
@@ -254,6 +263,7 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, paren
|
|
|
254
263
|
const handleKindChange = useCallback(
|
|
255
264
|
(nextKind: CreatableAccountKind) => {
|
|
256
265
|
setKind(nextKind);
|
|
266
|
+
setNewOrganization(false);
|
|
257
267
|
if (!username) return;
|
|
258
268
|
const proposed = nextKind === 'bot' ? applyBotUsernameSuffix(username) : username;
|
|
259
269
|
setUsername(proposed);
|
|
@@ -270,6 +280,14 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, paren
|
|
|
270
280
|
setDisplayNameError('');
|
|
271
281
|
return;
|
|
272
282
|
}
|
|
283
|
+
if (kind === 'organization') {
|
|
284
|
+
setDisplayNameError(
|
|
285
|
+
organizationNameSchema.safeParse(trimmed).success
|
|
286
|
+
? ''
|
|
287
|
+
: t('organizationDirectory.invalidName'),
|
|
288
|
+
);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
273
291
|
const nameParts = trimmed.split(/\s+/);
|
|
274
292
|
const firstName = nameParts[0] || '';
|
|
275
293
|
const lastName = nameParts.length > 1 ? nameParts.slice(1).join(' ') : '';
|
|
@@ -280,7 +298,7 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, paren
|
|
|
280
298
|
: '',
|
|
281
299
|
);
|
|
282
300
|
},
|
|
283
|
-
[t],
|
|
301
|
+
[kind, t],
|
|
284
302
|
);
|
|
285
303
|
|
|
286
304
|
/**
|
|
@@ -301,10 +319,12 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, paren
|
|
|
301
319
|
}, []);
|
|
302
320
|
|
|
303
321
|
const canCreate =
|
|
322
|
+
canUsePrivateApi &&
|
|
304
323
|
usernameStatus === 'available' &&
|
|
305
324
|
displayName.trim().length > 0 &&
|
|
306
325
|
!displayNameError &&
|
|
307
|
-
!isCreating
|
|
326
|
+
!isCreating &&
|
|
327
|
+
(kind !== 'organization' || newOrganization);
|
|
308
328
|
|
|
309
329
|
const handleCreate = useCallback(async () => {
|
|
310
330
|
if (!canCreate) return;
|
|
@@ -313,6 +333,7 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, paren
|
|
|
313
333
|
try {
|
|
314
334
|
const input: CreateAccountInput = {
|
|
315
335
|
kind,
|
|
336
|
+
...(kind === 'organization' ? { organizationType } : {}),
|
|
316
337
|
username,
|
|
317
338
|
name: { displayName: displayName.trim() },
|
|
318
339
|
bio: bio.trim() || undefined,
|
|
@@ -352,6 +373,7 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, paren
|
|
|
352
373
|
}, [
|
|
353
374
|
canCreate,
|
|
354
375
|
kind,
|
|
376
|
+
organizationType,
|
|
355
377
|
accountCategories,
|
|
356
378
|
username,
|
|
357
379
|
displayName,
|
|
@@ -405,128 +427,159 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, paren
|
|
|
405
427
|
})}
|
|
406
428
|
</SettingsListGroup>
|
|
407
429
|
|
|
408
|
-
{
|
|
430
|
+
{kind === 'organization' && !newOrganization ? (
|
|
431
|
+
<OrganizationDirectorySearch
|
|
432
|
+
key={user?.id}
|
|
433
|
+
onCreateAccount={(name, type) => {
|
|
434
|
+
handleDisplayNameChange(name);
|
|
435
|
+
setOrganizationType(type);
|
|
436
|
+
setNewOrganization(true);
|
|
437
|
+
}}
|
|
438
|
+
/>
|
|
439
|
+
) : (
|
|
440
|
+
<>
|
|
441
|
+
{kind === 'organization' ? (
|
|
442
|
+
<>
|
|
443
|
+
<Button appearance="outline" onPress={() => setNewOrganization(false)}>
|
|
444
|
+
{t('organizationDirectory.back')}
|
|
445
|
+
</Button>
|
|
446
|
+
<OrganizationTypePicker
|
|
447
|
+
value={organizationType}
|
|
448
|
+
onChange={setOrganizationType}
|
|
449
|
+
disabled={isCreating}
|
|
450
|
+
/>
|
|
451
|
+
</>
|
|
452
|
+
) : null}
|
|
453
|
+
{/* Categories — multi-select, shown for every kind this screen can create
|
|
409
454
|
(they are all non-personal). The badge on a selected row is its
|
|
410
455
|
POSITION, so the primary is legible as "1" rather than being a rule the
|
|
411
456
|
user has to be told. */}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
457
|
+
<View className="gap-space-4">
|
|
458
|
+
<SettingsListGroup title={t('accounts.create.accountCategory.label')}>
|
|
459
|
+
{ACCOUNT_CATEGORY_OPTIONS.map((option) => {
|
|
460
|
+
const position = accountCategories.indexOf(option);
|
|
461
|
+
const selected = position >= 0;
|
|
462
|
+
const label = accountCategoryLabel(locale, option);
|
|
463
|
+
return (
|
|
464
|
+
<SettingsListItem
|
|
465
|
+
key={option}
|
|
466
|
+
title={label}
|
|
467
|
+
disabled={!selected && accountCategories.length >= MAX_ACCOUNT_CATEGORIES}
|
|
468
|
+
onPress={() => toggleAccountCategory(option)}
|
|
469
|
+
showChevron={false}
|
|
470
|
+
rightElement={
|
|
471
|
+
selected ? (
|
|
472
|
+
<Text className="text-caption-1 font-semibold text-primary">
|
|
473
|
+
{position + 1}
|
|
474
|
+
</Text>
|
|
475
|
+
) : undefined
|
|
476
|
+
}
|
|
477
|
+
accessibilityLabel={label}
|
|
478
|
+
/>
|
|
479
|
+
);
|
|
480
|
+
})}
|
|
481
|
+
</SettingsListGroup>
|
|
482
|
+
<Text className="text-caption font-caption text-text-tertiary px-space-4">
|
|
483
|
+
{t('accounts.create.accountCategory.hint', { max: MAX_ACCOUNT_CATEGORIES })}
|
|
484
|
+
</Text>
|
|
485
|
+
</View>
|
|
486
|
+
|
|
487
|
+
{/* Details — a grouped section card hosting the form fields */}
|
|
488
|
+
<SettingsListGroup title={t('accounts.create.detailsSection') || 'Details'}>
|
|
489
|
+
<View className="p-space-16 gap-space-16">
|
|
490
|
+
{/* Username */}
|
|
491
|
+
<View className="gap-space-8">
|
|
492
|
+
<TextField isInvalid={usernameIsInvalid}>
|
|
493
|
+
<TextFieldInput
|
|
494
|
+
floatingLabel
|
|
495
|
+
label={t('accounts.create.username.label') || 'Username'}
|
|
496
|
+
value={username}
|
|
497
|
+
onChangeText={handleUsernameChange}
|
|
498
|
+
isInvalid={usernameIsInvalid}
|
|
499
|
+
autoCapitalize="none"
|
|
500
|
+
autoCorrect={false}
|
|
501
|
+
autoComplete="off"
|
|
502
|
+
maxLength={USERNAME_MAX_LENGTH}
|
|
503
|
+
/>
|
|
504
|
+
</TextField>
|
|
505
|
+
{usernameStatus === 'checking' || usernameMessage ? (
|
|
506
|
+
<View className="flex-row items-center gap-space-4 px-space-4">
|
|
507
|
+
{usernameStatus === 'checking' ? (
|
|
508
|
+
<ActivityIndicator size="small" color={bloomTheme.colors.primary} />
|
|
509
|
+
) : usernameStatus === 'available' ? (
|
|
510
|
+
<Ionicons
|
|
511
|
+
name="checkmark-circle"
|
|
512
|
+
size={16}
|
|
513
|
+
color={bloomTheme.colors.success}
|
|
514
|
+
/>
|
|
515
|
+
) : usernameIsInvalid ? (
|
|
516
|
+
<Ionicons name="alert-circle" size={16} color={bloomTheme.colors.negative} />
|
|
517
|
+
) : null}
|
|
518
|
+
{usernameMessage ? (
|
|
519
|
+
<Text className="text-caption font-caption" style={{ color: statusColor }}>
|
|
520
|
+
{usernameMessage}
|
|
521
|
+
</Text>
|
|
522
|
+
) : null}
|
|
523
|
+
</View>
|
|
468
524
|
) : null}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
525
|
+
</View>
|
|
526
|
+
|
|
527
|
+
{/* Display Name */}
|
|
528
|
+
<View className="gap-space-4">
|
|
529
|
+
<TextField isInvalid={Boolean(displayNameError)}>
|
|
530
|
+
<TextFieldInput
|
|
531
|
+
floatingLabel
|
|
532
|
+
label={t('accounts.create.displayName.label') || 'Display name'}
|
|
533
|
+
value={displayName}
|
|
534
|
+
onChangeText={handleDisplayNameChange}
|
|
535
|
+
isInvalid={Boolean(displayNameError)}
|
|
536
|
+
maxLength={
|
|
537
|
+
kind === 'organization' ? ORGANIZATION_NAME_MAX_LENGTH : DISPLAY_NAME_MAX
|
|
538
|
+
}
|
|
539
|
+
/>
|
|
540
|
+
</TextField>
|
|
541
|
+
{displayNameError ? (
|
|
542
|
+
<Text className="text-caption font-caption text-negative px-space-4">
|
|
543
|
+
{displayNameError}
|
|
472
544
|
</Text>
|
|
473
545
|
) : null}
|
|
474
546
|
</View>
|
|
475
|
-
) : null}
|
|
476
|
-
</View>
|
|
477
|
-
|
|
478
|
-
{/* Display Name */}
|
|
479
|
-
<View className="gap-space-4">
|
|
480
|
-
<TextField isInvalid={Boolean(displayNameError)}>
|
|
481
|
-
<TextFieldInput
|
|
482
|
-
floatingLabel
|
|
483
|
-
label={t('accounts.create.displayName.label') || 'Display name'}
|
|
484
|
-
value={displayName}
|
|
485
|
-
onChangeText={handleDisplayNameChange}
|
|
486
|
-
isInvalid={Boolean(displayNameError)}
|
|
487
|
-
maxLength={DISPLAY_NAME_MAX}
|
|
488
|
-
/>
|
|
489
|
-
</TextField>
|
|
490
|
-
{displayNameError ? (
|
|
491
|
-
<Text className="text-caption font-caption text-negative px-space-4">
|
|
492
|
-
{displayNameError}
|
|
493
|
-
</Text>
|
|
494
|
-
) : null}
|
|
495
|
-
</View>
|
|
496
547
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
548
|
+
{/* Bio */}
|
|
549
|
+
<View className="gap-space-4">
|
|
550
|
+
<TextField>
|
|
551
|
+
<TextFieldInput
|
|
552
|
+
floatingLabel
|
|
553
|
+
label={t('accounts.create.bio.label') || 'Bio (optional)'}
|
|
554
|
+
value={bio}
|
|
555
|
+
onChangeText={setBio}
|
|
556
|
+
maxLength={BIO_MAX}
|
|
557
|
+
multiline
|
|
558
|
+
numberOfLines={3}
|
|
559
|
+
textAlignVertical="top"
|
|
560
|
+
/>
|
|
561
|
+
</TextField>
|
|
562
|
+
<Text className="text-caption font-caption text-text-tertiary px-space-4 text-right">
|
|
563
|
+
{bio.length}/{BIO_MAX}
|
|
564
|
+
</Text>
|
|
565
|
+
</View>
|
|
566
|
+
</View>
|
|
567
|
+
</SettingsListGroup>
|
|
568
|
+
|
|
569
|
+
{/* Create Button */}
|
|
570
|
+
<Button
|
|
571
|
+
appearance="solid"
|
|
572
|
+
tone="accent"
|
|
573
|
+
onPress={handleCreate}
|
|
574
|
+
disabled={!canCreate}
|
|
575
|
+
loading={isCreating}
|
|
576
|
+
accessibilityLabel={title}
|
|
577
|
+
className="w-full"
|
|
578
|
+
>
|
|
579
|
+
{title}
|
|
580
|
+
</Button>
|
|
581
|
+
</>
|
|
582
|
+
)}
|
|
530
583
|
</View>
|
|
531
584
|
);
|
|
532
585
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { OrganizationClaimReview } from '../components/organizations/OrganizationClaimReview';
|
|
2
|
+
import { OrganizationClaims } from '../components/organizations/OrganizationClaims';
|
|
1
3
|
import type React from 'react';
|
|
2
4
|
import { useCallback, useMemo, useState } from 'react';
|
|
3
5
|
import { View, StyleSheet, ActivityIndicator, Linking, Platform } from 'react-native';
|
|
@@ -396,6 +398,22 @@ const ManageAccountScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, navig
|
|
|
396
398
|
/>
|
|
397
399
|
</SettingsListGroup>
|
|
398
400
|
|
|
401
|
+
{user?.kind === 'organization' ? (
|
|
402
|
+
<SettingsListGroup title={t('organizationDirectory.title')}>
|
|
403
|
+
<SettingsListItem
|
|
404
|
+
title={t('organizationDirectory.view')}
|
|
405
|
+
onPress={() => navigate?.('AccountSettings', { accountId: user.id })}
|
|
406
|
+
/>
|
|
407
|
+
</SettingsListGroup>
|
|
408
|
+
) : null}
|
|
409
|
+
|
|
410
|
+
{user?.kind === 'personal' ? (
|
|
411
|
+
<>
|
|
412
|
+
<OrganizationClaims key={user.id} />
|
|
413
|
+
<OrganizationClaimReview key={`review:${user.id}`} />
|
|
414
|
+
</>
|
|
415
|
+
) : null}
|
|
416
|
+
|
|
399
417
|
{/* Sessions section */}
|
|
400
418
|
<SettingsListGroup title={t('manageAccount.sections.sessions') || 'Sessions & devices'}>
|
|
401
419
|
{deviceSessionsLoading ? (
|
|
@@ -5,6 +5,7 @@ import { Switch } from '@oxy.so/bloom/switch';
|
|
|
5
5
|
import { useTheme } from '@oxy.so/bloom/theme';
|
|
6
6
|
import type { UserPreferences } from '@oxy.so/core';
|
|
7
7
|
import { getNativeLanguageName } from '@oxy.so/core';
|
|
8
|
+
import { DisplayCurrencySelector } from '../components/DisplayCurrencySelector';
|
|
8
9
|
import type { BaseScreenProps } from '../types/navigation';
|
|
9
10
|
import { SettingsIcon } from '../components/SettingsIcon';
|
|
10
11
|
import { useI18n } from '../hooks/useI18n';
|
|
@@ -158,6 +159,7 @@ const PreferencesScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, navigat
|
|
|
158
159
|
</SettingsListGroup>
|
|
159
160
|
|
|
160
161
|
<SettingsListGroup title={t('preferences.sections.region') || 'Region'}>
|
|
162
|
+
<DisplayCurrencySelector />
|
|
161
163
|
<SettingsListItem
|
|
162
164
|
icon={<SettingsIcon name="clock-outline" color={bloomTheme.colors.info} />}
|
|
163
165
|
title={t('preferences.items.timezone.title') || 'Timezone'}
|