@oxyhq/services 26.2.0 → 27.0.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 (59) hide show
  1. package/lib/commonjs/index.js +21 -1
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/components/authChooser/requestSurfaces.js +14 -6
  4. package/lib/commonjs/ui/components/authChooser/requestSurfaces.js.map +1 -1
  5. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +16 -3
  6. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  7. package/lib/commonjs/ui/hooks/queries/userCache.js +164 -10
  8. package/lib/commonjs/ui/hooks/queries/userCache.js.map +1 -1
  9. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +6 -1
  10. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  11. package/lib/commonjs/ui/screens/CreateAccountScreen.js +73 -40
  12. package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -1
  13. package/lib/commonjs/ui/utils/avatarUtils.js +5 -0
  14. package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
  15. package/lib/module/index.js +4 -2
  16. package/lib/module/index.js.map +1 -1
  17. package/lib/module/ui/components/authChooser/requestSurfaces.js +15 -7
  18. package/lib/module/ui/components/authChooser/requestSurfaces.js.map +1 -1
  19. package/lib/module/ui/hooks/mutations/useAccountMutations.js +16 -3
  20. package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  21. package/lib/module/ui/hooks/queries/userCache.js +161 -10
  22. package/lib/module/ui/hooks/queries/userCache.js.map +1 -1
  23. package/lib/module/ui/screens/AccountSettingsScreen.js +6 -1
  24. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  25. package/lib/module/ui/screens/CreateAccountScreen.js +74 -41
  26. package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -1
  27. package/lib/module/ui/utils/avatarUtils.js +5 -0
  28. package/lib/module/ui/utils/avatarUtils.js.map +1 -1
  29. package/lib/typescript/commonjs/index.d.ts +2 -2
  30. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/ui/components/authChooser/requestSurfaces.d.ts +6 -1
  32. package/lib/typescript/commonjs/ui/components/authChooser/requestSurfaces.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +1 -1
  34. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/ui/hooks/queries/userCache.d.ts +79 -2
  36. package/lib/typescript/commonjs/ui/hooks/queries/userCache.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/ui/utils/avatarUtils.d.ts.map +1 -1
  40. package/lib/typescript/module/index.d.ts +2 -2
  41. package/lib/typescript/module/index.d.ts.map +1 -1
  42. package/lib/typescript/module/ui/components/authChooser/requestSurfaces.d.ts +6 -1
  43. package/lib/typescript/module/ui/components/authChooser/requestSurfaces.d.ts.map +1 -1
  44. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +1 -1
  45. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  46. package/lib/typescript/module/ui/hooks/queries/userCache.d.ts +79 -2
  47. package/lib/typescript/module/ui/hooks/queries/userCache.d.ts.map +1 -1
  48. package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  49. package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  50. package/lib/typescript/module/ui/utils/avatarUtils.d.ts.map +1 -1
  51. package/package.json +4 -4
  52. package/src/index.ts +15 -3
  53. package/src/ui/components/authChooser/requestSurfaces.tsx +12 -4
  54. package/src/ui/hooks/mutations/useAccountMutations.ts +20 -3
  55. package/src/ui/hooks/queries/__tests__/userCacheClear.test.ts +301 -0
  56. package/src/ui/hooks/queries/userCache.ts +211 -10
  57. package/src/ui/screens/AccountSettingsScreen.tsx +9 -1
  58. package/src/ui/screens/CreateAccountScreen.tsx +66 -36
  59. package/src/ui/utils/avatarUtils.ts +9 -0
@@ -2,8 +2,8 @@ import type React from 'react';
2
2
  import { useState, useCallback, useRef, useEffect } from 'react';
3
3
  import { View, ActivityIndicator } from 'react-native';
4
4
  import Ionicons from '@expo/vector-icons/Ionicons';
5
- import type { AccountKind, CreateAccountInput, OrganizationCategory } from '@oxyhq/core';
6
- import { DISPLAY_NAME_INVALID_MESSAGE, isValidDisplayName, MAX_DISPLAY_NAME_LENGTH, ORGANIZATION_CATEGORIES } from '@oxyhq/core';
5
+ import type { AccountCategoryId, AccountKind, CreateAccountInput } from '@oxyhq/core';
6
+ import { DISPLAY_NAME_INVALID_MESSAGE, isValidDisplayName, MAX_ACCOUNT_CATEGORIES, MAX_DISPLAY_NAME_LENGTH, SELECTABLE_ACCOUNT_CATEGORY_IDS } from '@oxyhq/core';
7
7
  import type { BaseScreenProps } from '../types/navigation';
8
8
  import { useI18n } from '../hooks/useI18n';
9
9
  import { useSurfaceHeader } from '../hooks/useSurfaceHeader';
@@ -83,23 +83,25 @@ const kindDescription = (
83
83
  }
84
84
  };
85
85
 
86
- const organizationCategoryLabel = (
86
+ /**
87
+ * The visible label for a category id.
88
+ *
89
+ * A lookup, deliberately not a `switch` with a `default`: a `default` would let
90
+ * an id with no translation render as some OTHER category's label, silently and
91
+ * only in the languages that are missing it. Falling back to the raw id is ugly
92
+ * and correct — it is visibly wrong, and it names the missing key.
93
+ */
94
+ const accountCategoryLabel = (
87
95
  t: (key: string, vars?: Record<string, string | number>) => string,
88
- category: OrganizationCategory,
89
- ): string => {
90
- switch (category) {
91
- case 'agency':
92
- return t('accounts.organizationCategory.agency');
93
- case 'cooperative':
94
- return t('accounts.organizationCategory.cooperative');
95
- case 'landlord':
96
- return t('accounts.organizationCategory.landlord');
97
- default:
98
- return t('accounts.organizationCategory.other');
99
- }
100
- };
96
+ category: AccountCategoryId,
97
+ ): string => t(`accounts.accountCategory.${category}`) || category;
101
98
 
102
- const ORGANIZATION_CATEGORY_OPTIONS: OrganizationCategory[] = [...ORGANIZATION_CATEGORIES];
99
+ /**
100
+ * Only the SELECTABLE ids are offered. The full `ACCOUNT_CATEGORY_IDS` still
101
+ * contains withdrawn ones so that accounts already carrying them keep working —
102
+ * offering them here would be how an account newly acquires one.
103
+ */
104
+ const ACCOUNT_CATEGORY_OPTIONS: readonly AccountCategoryId[] = SELECTABLE_ACCOUNT_CATEGORY_IDS;
103
105
 
104
106
  /**
105
107
  * Create a new account in the unified account graph (an organization, project,
@@ -125,7 +127,12 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
125
127
  const parentId = typeof parentAccountId === 'string' ? parentAccountId : undefined;
126
128
 
127
129
  const [kind, setKind] = useState<CreatableAccountKind>('project');
128
- const [organizationCategory, setOrganizationCategory] = useState<OrganizationCategory>('agency');
130
+ /**
131
+ * ORDER IS THE DATA: index 0 is the primary category. Selecting appends,
132
+ * de-selecting removes, and neither sorts — so the list the user assembles is
133
+ * the list that is sent, and the first one they picked stays the primary.
134
+ */
135
+ const [accountCategories, setAccountCategories] = useState<AccountCategoryId[]>([]);
129
136
  const [username, setUsername] = useState('');
130
137
  const [displayName, setDisplayName] = useState('');
131
138
  const [displayNameError, setDisplayNameError] = useState('');
@@ -215,6 +222,23 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
215
222
  );
216
223
  }, [t]);
217
224
 
225
+ /**
226
+ * Append on select, splice out on de-select. Never sorts — appending is what
227
+ * makes the FIRST category the user chose the primary one, and a sort would
228
+ * silently reassign that. De-selecting the primary promotes whatever the user
229
+ * picked next, which is the only interpretation that does not invent a choice
230
+ * on their behalf.
231
+ */
232
+ const toggleAccountCategory = useCallback((category: AccountCategoryId) => {
233
+ setAccountCategories((current) => {
234
+ if (current.includes(category)) {
235
+ return current.filter((entry) => entry !== category);
236
+ }
237
+ if (current.length >= MAX_ACCOUNT_CATEGORIES) return current;
238
+ return [...current, category];
239
+ });
240
+ }, []);
241
+
218
242
  const canCreate = usernameStatus === 'available'
219
243
  && displayName.trim().length > 0
220
244
  && !displayNameError
@@ -225,17 +249,14 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
225
249
 
226
250
  setIsCreating(true);
227
251
  try {
228
- // Split display name into first/last
229
- const nameParts = displayName.trim().split(/\s+/);
230
- const firstName = nameParts[0] || '';
231
- const lastName = nameParts.length > 1 ? nameParts.slice(1).join(' ') : undefined;
232
-
233
252
  const input: CreateAccountInput = {
234
253
  kind,
235
254
  username,
236
- name: { first: firstName, last: lastName },
255
+ name: { displayName: displayName.trim() },
237
256
  bio: bio.trim() || undefined,
238
- ...(kind === 'organization' ? { organizationCategory } : null),
257
+ // Sent in the user's own order, or omitted entirely when empty — the
258
+ // API distinguishes "no categories" from "not stated" only by absence.
259
+ ...(accountCategories.length > 0 ? { accountCategories } : null),
239
260
  ...(parentId ? { parentAccountId: parentId } : null),
240
261
  };
241
262
  const account = await createAccount(input);
@@ -265,7 +286,7 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
265
286
  } finally {
266
287
  setIsCreating(false);
267
288
  }
268
- }, [canCreate, kind, organizationCategory, username, displayName, bio, parentId, createAccount, switchToAccount, onClose, t]);
289
+ }, [canCreate, kind, accountCategories, username, displayName, bio, parentId, createAccount, switchToAccount, onClose, t]);
269
290
 
270
291
  // Status icon + color shown alongside the username field message
271
292
  const usernameIsInvalid = usernameStatus === 'taken' || usernameStatus === 'invalid';
@@ -307,26 +328,35 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
307
328
  })}
308
329
  </SettingsListGroup>
309
330
 
310
- {/* Organization category grouped selection rows, shown only for organizations */}
311
- {kind === 'organization' ? (
312
- <SettingsListGroup title={t('accounts.create.organizationCategory.label')}>
313
- {ORGANIZATION_CATEGORY_OPTIONS.map((option) => {
314
- const selected = option === organizationCategory;
331
+ {/* Categoriesmulti-select, shown for every kind this screen can create
332
+ (they are all non-personal). The badge on a selected row is its
333
+ POSITION, so the primary is legible as "1" rather than being a rule the
334
+ user has to be told. */}
335
+ <View className="gap-space-4">
336
+ <SettingsListGroup title={t('accounts.create.accountCategory.label')}>
337
+ {ACCOUNT_CATEGORY_OPTIONS.map((option) => {
338
+ const position = accountCategories.indexOf(option);
339
+ const selected = position >= 0;
340
+ const label = accountCategoryLabel(t, option);
315
341
  return (
316
342
  <SettingsListItem
317
343
  key={option}
318
- title={organizationCategoryLabel(t, option)}
319
- onPress={() => setOrganizationCategory(option)}
344
+ title={label}
345
+ disabled={!selected && accountCategories.length >= MAX_ACCOUNT_CATEGORIES}
346
+ onPress={() => toggleAccountCategory(option)}
320
347
  showChevron={false}
321
348
  rightElement={selected ? (
322
- <Ionicons name="checkmark-circle" size={20} color={bloomTheme.colors.primary} />
349
+ <Text className="text-caption-1 font-semibold text-primary">{position + 1}</Text>
323
350
  ) : undefined}
324
- accessibilityLabel={organizationCategoryLabel(t, option)}
351
+ accessibilityLabel={label}
325
352
  />
326
353
  );
327
354
  })}
328
355
  </SettingsListGroup>
329
- ) : null}
356
+ <Text className="text-caption font-caption text-text-tertiary px-space-4">
357
+ {t('accounts.create.accountCategory.hint', { max: MAX_ACCOUNT_CATEGORIES })}
358
+ </Text>
359
+ </View>
330
360
 
331
361
  {/* Details — a grouped section card hosting the form fields */}
332
362
  <SettingsListGroup title={t('accounts.create.detailsSection') || 'Details'}>
@@ -5,6 +5,10 @@ import { useAccountStore } from '../stores/accountStore';
5
5
  import { useAuthStore } from '../stores/authStore';
6
6
  import type { QueryClient } from '@tanstack/react-query';
7
7
  import { queryKeys, invalidateUserQueries, invalidateAccountQueries } from '../hooks/queries/queryKeys';
8
+ import {
9
+ clearedFieldsFromProfileUpdate,
10
+ upsertCachedUser,
11
+ } from '../hooks/queries/userCache';
8
12
 
9
13
  /**
10
14
  * Refreshes avatar in accountStore with cache-busted URL to force image reload.
@@ -60,6 +64,11 @@ export async function updateProfileWithAvatar(
60
64
  // Update authStore so frontend components see the changes immediately
61
65
  useAuthStore.getState().setUser(data);
62
66
 
67
+ const cleared = clearedFieldsFromProfileUpdate(updates);
68
+ upsertCachedUser(queryClient, data, data.id, {
69
+ cleared: cleared.length > 0 ? cleared : undefined,
70
+ });
71
+
63
72
  // If avatar was updated, refresh accountStore with a cache-busted URL. An
64
73
  // EMPTY avatar is a removal: clear both fields so the switcher falls back to
65
74
  // initials instead of pointing at a download URL for the empty file id.