@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.
Files changed (141) hide show
  1. package/lib/commonjs/index.js +28 -0
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/components/AccountInvitationAcceptance.js +97 -0
  4. package/lib/commonjs/ui/components/AccountInvitationAcceptance.js.map +1 -0
  5. package/lib/commonjs/ui/components/DisplayCurrencySelector.js +49 -0
  6. package/lib/commonjs/ui/components/DisplayCurrencySelector.js.map +1 -0
  7. package/lib/commonjs/ui/components/organizations/OrganizationClaimReview.js +208 -0
  8. package/lib/commonjs/ui/components/organizations/OrganizationClaimReview.js.map +1 -0
  9. package/lib/commonjs/ui/components/organizations/OrganizationClaims.js +77 -0
  10. package/lib/commonjs/ui/components/organizations/OrganizationClaims.js.map +1 -0
  11. package/lib/commonjs/ui/components/organizations/OrganizationDirectorySearch.js +218 -0
  12. package/lib/commonjs/ui/components/organizations/OrganizationDirectorySearch.js.map +1 -0
  13. package/lib/commonjs/ui/components/organizations/OrganizationSettings.js +147 -0
  14. package/lib/commonjs/ui/components/organizations/OrganizationSettings.js.map +1 -0
  15. package/lib/commonjs/ui/components/organizations/OrganizationTypePicker.js +34 -0
  16. package/lib/commonjs/ui/components/organizations/OrganizationTypePicker.js.map +1 -0
  17. package/lib/commonjs/ui/hooks/useDisplayCurrency.js +77 -0
  18. package/lib/commonjs/ui/hooks/useDisplayCurrency.js.map +1 -0
  19. package/lib/commonjs/ui/hooks/useDisplayPrice.js +59 -0
  20. package/lib/commonjs/ui/hooks/useDisplayPrice.js.map +1 -0
  21. package/lib/commonjs/ui/screens/AccountMembersScreen.js +63 -20
  22. package/lib/commonjs/ui/screens/AccountMembersScreen.js.map +1 -1
  23. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +14 -5
  24. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  25. package/lib/commonjs/ui/screens/CreateAccountScreen.js +148 -115
  26. package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -1
  27. package/lib/commonjs/ui/screens/ManageAccountScreen.js +13 -1
  28. package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
  29. package/lib/commonjs/ui/screens/PreferencesScreen.js +4 -3
  30. package/lib/commonjs/ui/screens/PreferencesScreen.js.map +1 -1
  31. package/lib/module/index.js +4 -0
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/ui/components/AccountInvitationAcceptance.js +94 -0
  34. package/lib/module/ui/components/AccountInvitationAcceptance.js.map +1 -0
  35. package/lib/module/ui/components/DisplayCurrencySelector.js +44 -0
  36. package/lib/module/ui/components/DisplayCurrencySelector.js.map +1 -0
  37. package/lib/module/ui/components/organizations/OrganizationClaimReview.js +205 -0
  38. package/lib/module/ui/components/organizations/OrganizationClaimReview.js.map +1 -0
  39. package/lib/module/ui/components/organizations/OrganizationClaims.js +73 -0
  40. package/lib/module/ui/components/organizations/OrganizationClaims.js.map +1 -0
  41. package/lib/module/ui/components/organizations/OrganizationDirectorySearch.js +215 -0
  42. package/lib/module/ui/components/organizations/OrganizationDirectorySearch.js.map +1 -0
  43. package/lib/module/ui/components/organizations/OrganizationSettings.js +143 -0
  44. package/lib/module/ui/components/organizations/OrganizationSettings.js.map +1 -0
  45. package/lib/module/ui/components/organizations/OrganizationTypePicker.js +30 -0
  46. package/lib/module/ui/components/organizations/OrganizationTypePicker.js.map +1 -0
  47. package/lib/module/ui/hooks/useDisplayCurrency.js +72 -0
  48. package/lib/module/ui/hooks/useDisplayCurrency.js.map +1 -0
  49. package/lib/module/ui/hooks/useDisplayPrice.js +56 -0
  50. package/lib/module/ui/hooks/useDisplayPrice.js.map +1 -0
  51. package/lib/module/ui/screens/AccountMembersScreen.js +63 -20
  52. package/lib/module/ui/screens/AccountMembersScreen.js.map +1 -1
  53. package/lib/module/ui/screens/AccountSettingsScreen.js +14 -5
  54. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  55. package/lib/module/ui/screens/CreateAccountScreen.js +149 -115
  56. package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -1
  57. package/lib/module/ui/screens/ManageAccountScreen.js +13 -1
  58. package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
  59. package/lib/module/ui/screens/PreferencesScreen.js +4 -3
  60. package/lib/module/ui/screens/PreferencesScreen.js.map +1 -1
  61. package/lib/typescript/commonjs/index.d.ts +4 -0
  62. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/ui/components/AccountInvitationAcceptance.d.ts +5 -0
  64. package/lib/typescript/commonjs/ui/components/AccountInvitationAcceptance.d.ts.map +1 -0
  65. package/lib/typescript/commonjs/ui/components/DisplayCurrencySelector.d.ts +3 -0
  66. package/lib/typescript/commonjs/ui/components/DisplayCurrencySelector.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/ui/components/organizations/OrganizationClaimReview.d.ts +3 -0
  68. package/lib/typescript/commonjs/ui/components/organizations/OrganizationClaimReview.d.ts.map +1 -0
  69. package/lib/typescript/commonjs/ui/components/organizations/OrganizationClaims.d.ts +2 -0
  70. package/lib/typescript/commonjs/ui/components/organizations/OrganizationClaims.d.ts.map +1 -0
  71. package/lib/typescript/commonjs/ui/components/organizations/OrganizationDirectorySearch.d.ts +6 -0
  72. package/lib/typescript/commonjs/ui/components/organizations/OrganizationDirectorySearch.d.ts.map +1 -0
  73. package/lib/typescript/commonjs/ui/components/organizations/OrganizationSettings.d.ts +5 -0
  74. package/lib/typescript/commonjs/ui/components/organizations/OrganizationSettings.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/ui/components/organizations/OrganizationTypePicker.d.ts +7 -0
  76. package/lib/typescript/commonjs/ui/components/organizations/OrganizationTypePicker.d.ts.map +1 -0
  77. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +10 -3
  78. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/ui/hooks/queries/usePersonalPlans.d.ts +4 -3
  80. package/lib/typescript/commonjs/ui/hooks/queries/usePersonalPlans.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/ui/hooks/useDisplayCurrency.d.ts +11 -0
  82. package/lib/typescript/commonjs/ui/hooks/useDisplayCurrency.d.ts.map +1 -0
  83. package/lib/typescript/commonjs/ui/hooks/useDisplayPrice.d.ts +6 -0
  84. package/lib/typescript/commonjs/ui/hooks/useDisplayPrice.d.ts.map +1 -0
  85. package/lib/typescript/commonjs/ui/icons/Ionicons.d.ts +1 -1
  86. package/lib/typescript/commonjs/ui/icons/MaterialCommunityIcons.d.ts +1 -1
  87. package/lib/typescript/commonjs/ui/screens/AccountMembersScreen.d.ts +1 -1
  88. package/lib/typescript/commonjs/ui/screens/AccountMembersScreen.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/ui/screens/PreferencesScreen.d.ts.map +1 -1
  93. package/lib/typescript/module/index.d.ts +4 -0
  94. package/lib/typescript/module/index.d.ts.map +1 -1
  95. package/lib/typescript/module/ui/components/AccountInvitationAcceptance.d.ts +5 -0
  96. package/lib/typescript/module/ui/components/AccountInvitationAcceptance.d.ts.map +1 -0
  97. package/lib/typescript/module/ui/components/DisplayCurrencySelector.d.ts +3 -0
  98. package/lib/typescript/module/ui/components/DisplayCurrencySelector.d.ts.map +1 -0
  99. package/lib/typescript/module/ui/components/organizations/OrganizationClaimReview.d.ts +3 -0
  100. package/lib/typescript/module/ui/components/organizations/OrganizationClaimReview.d.ts.map +1 -0
  101. package/lib/typescript/module/ui/components/organizations/OrganizationClaims.d.ts +2 -0
  102. package/lib/typescript/module/ui/components/organizations/OrganizationClaims.d.ts.map +1 -0
  103. package/lib/typescript/module/ui/components/organizations/OrganizationDirectorySearch.d.ts +6 -0
  104. package/lib/typescript/module/ui/components/organizations/OrganizationDirectorySearch.d.ts.map +1 -0
  105. package/lib/typescript/module/ui/components/organizations/OrganizationSettings.d.ts +5 -0
  106. package/lib/typescript/module/ui/components/organizations/OrganizationSettings.d.ts.map +1 -0
  107. package/lib/typescript/module/ui/components/organizations/OrganizationTypePicker.d.ts +7 -0
  108. package/lib/typescript/module/ui/components/organizations/OrganizationTypePicker.d.ts.map +1 -0
  109. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +10 -3
  110. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  111. package/lib/typescript/module/ui/hooks/queries/usePersonalPlans.d.ts +4 -3
  112. package/lib/typescript/module/ui/hooks/queries/usePersonalPlans.d.ts.map +1 -1
  113. package/lib/typescript/module/ui/hooks/useDisplayCurrency.d.ts +11 -0
  114. package/lib/typescript/module/ui/hooks/useDisplayCurrency.d.ts.map +1 -0
  115. package/lib/typescript/module/ui/hooks/useDisplayPrice.d.ts +6 -0
  116. package/lib/typescript/module/ui/hooks/useDisplayPrice.d.ts.map +1 -0
  117. package/lib/typescript/module/ui/icons/Ionicons.d.ts +1 -1
  118. package/lib/typescript/module/ui/icons/MaterialCommunityIcons.d.ts +1 -1
  119. package/lib/typescript/module/ui/screens/AccountMembersScreen.d.ts +1 -1
  120. package/lib/typescript/module/ui/screens/AccountMembersScreen.d.ts.map +1 -1
  121. package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  122. package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  123. package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  124. package/lib/typescript/module/ui/screens/PreferencesScreen.d.ts.map +1 -1
  125. package/package.json +9 -8
  126. package/src/index.ts +5 -0
  127. package/src/ui/components/AccountInvitationAcceptance.tsx +90 -0
  128. package/src/ui/components/DisplayCurrencySelector.tsx +38 -0
  129. package/src/ui/components/__tests__/AccountInvitationAcceptance.test.tsx +61 -0
  130. package/src/ui/components/organizations/OrganizationClaimReview.tsx +218 -0
  131. package/src/ui/components/organizations/OrganizationClaims.tsx +67 -0
  132. package/src/ui/components/organizations/OrganizationDirectorySearch.tsx +228 -0
  133. package/src/ui/components/organizations/OrganizationSettings.tsx +131 -0
  134. package/src/ui/components/organizations/OrganizationTypePicker.tsx +31 -0
  135. package/src/ui/hooks/useDisplayCurrency.ts +64 -0
  136. package/src/ui/hooks/useDisplayPrice.ts +40 -0
  137. package/src/ui/screens/AccountMembersScreen.tsx +71 -27
  138. package/src/ui/screens/AccountSettingsScreen.tsx +22 -4
  139. package/src/ui/screens/CreateAccountScreen.tsx +177 -124
  140. package/src/ui/screens/ManageAccountScreen.tsx +18 -0
  141. 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 username/email, change roles,
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.members.invite(id, input),
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(t('accounts.members.toasts.added') || 'Member added');
166
+ toast.success('Invitation sent');
147
167
  },
148
168
  onError: (error) => {
149
- if (isUserNotFound(error)) {
150
- const message = t('accounts.members.errors.userNotFound') || 'User not found';
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, t]);
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={t('accounts.members.invite.identifierLabel') || 'Username or email'}
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={t('accounts.members.invite.submit') || 'Add member'}
344
+ accessibilityLabel={'Send invitation'}
336
345
  className="w-full"
337
346
  >
338
- {t('accounts.members.invite.submit') || 'Add member'}
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 ?? 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'}>
@@ -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
- {/* Categories — multi-select, shown for every kind this screen can create
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
- <View className="gap-space-4">
413
- <SettingsListGroup title={t('accounts.create.accountCategory.label')}>
414
- {ACCOUNT_CATEGORY_OPTIONS.map((option) => {
415
- const position = accountCategories.indexOf(option);
416
- const selected = position >= 0;
417
- const label = accountCategoryLabel(locale, option);
418
- return (
419
- <SettingsListItem
420
- key={option}
421
- title={label}
422
- disabled={!selected && accountCategories.length >= MAX_ACCOUNT_CATEGORIES}
423
- onPress={() => toggleAccountCategory(option)}
424
- showChevron={false}
425
- rightElement={
426
- selected ? (
427
- <Text className="text-caption-1 font-semibold text-primary">
428
- {position + 1}
429
- </Text>
430
- ) : undefined
431
- }
432
- accessibilityLabel={label}
433
- />
434
- );
435
- })}
436
- </SettingsListGroup>
437
- <Text className="text-caption font-caption text-text-tertiary px-space-4">
438
- {t('accounts.create.accountCategory.hint', { max: MAX_ACCOUNT_CATEGORIES })}
439
- </Text>
440
- </View>
441
-
442
- {/* Details — a grouped section card hosting the form fields */}
443
- <SettingsListGroup title={t('accounts.create.detailsSection') || 'Details'}>
444
- <View className="p-space-16 gap-space-16">
445
- {/* Username */}
446
- <View className="gap-space-8">
447
- <TextField isInvalid={usernameIsInvalid}>
448
- <TextFieldInput
449
- floatingLabel
450
- label={t('accounts.create.username.label') || 'Username'}
451
- value={username}
452
- onChangeText={handleUsernameChange}
453
- isInvalid={usernameIsInvalid}
454
- autoCapitalize="none"
455
- autoCorrect={false}
456
- autoComplete="off"
457
- maxLength={USERNAME_MAX_LENGTH}
458
- />
459
- </TextField>
460
- {usernameStatus === 'checking' || usernameMessage ? (
461
- <View className="flex-row items-center gap-space-4 px-space-4">
462
- {usernameStatus === 'checking' ? (
463
- <ActivityIndicator size="small" color={bloomTheme.colors.primary} />
464
- ) : usernameStatus === 'available' ? (
465
- <Ionicons name="checkmark-circle" size={16} color={bloomTheme.colors.success} />
466
- ) : usernameIsInvalid ? (
467
- <Ionicons name="alert-circle" size={16} color={bloomTheme.colors.negative} />
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
- {usernameMessage ? (
470
- <Text className="text-caption font-caption" style={{ color: statusColor }}>
471
- {usernameMessage}
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
- {/* Bio */}
498
- <View className="gap-space-4">
499
- <TextField>
500
- <TextFieldInput
501
- floatingLabel
502
- label={t('accounts.create.bio.label') || 'Bio (optional)'}
503
- value={bio}
504
- onChangeText={setBio}
505
- maxLength={BIO_MAX}
506
- multiline
507
- numberOfLines={3}
508
- textAlignVertical="top"
509
- />
510
- </TextField>
511
- <Text className="text-caption font-caption text-text-tertiary px-space-4 text-right">
512
- {bio.length}/{BIO_MAX}
513
- </Text>
514
- </View>
515
- </View>
516
- </SettingsListGroup>
517
-
518
- {/* Create Button */}
519
- <Button
520
- appearance="solid"
521
- tone="accent"
522
- onPress={handleCreate}
523
- disabled={!canCreate}
524
- loading={isCreating}
525
- accessibilityLabel={title}
526
- className="w-full"
527
- >
528
- {title}
529
- </Button>
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'}