@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.
- package/lib/commonjs/index.js +21 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/authChooser/requestSurfaces.js +14 -6
- package/lib/commonjs/ui/components/authChooser/requestSurfaces.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +16 -3
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/userCache.js +164 -10
- package/lib/commonjs/ui/hooks/queries/userCache.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js +6 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/CreateAccountScreen.js +73 -40
- package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -1
- package/lib/commonjs/ui/utils/avatarUtils.js +5 -0
- package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
- package/lib/module/index.js +4 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/authChooser/requestSurfaces.js +15 -7
- package/lib/module/ui/components/authChooser/requestSurfaces.js.map +1 -1
- package/lib/module/ui/hooks/mutations/useAccountMutations.js +16 -3
- package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/module/ui/hooks/queries/userCache.js +161 -10
- package/lib/module/ui/hooks/queries/userCache.js.map +1 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js +6 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/module/ui/screens/CreateAccountScreen.js +74 -41
- package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -1
- package/lib/module/ui/utils/avatarUtils.js +5 -0
- package/lib/module/ui/utils/avatarUtils.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/authChooser/requestSurfaces.d.ts +6 -1
- package/lib/typescript/commonjs/ui/components/authChooser/requestSurfaces.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/userCache.d.ts +79 -2
- package/lib/typescript/commonjs/ui/hooks/queries/userCache.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/utils/avatarUtils.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/authChooser/requestSurfaces.d.ts +6 -1
- package/lib/typescript/module/ui/components/authChooser/requestSurfaces.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/userCache.d.ts +79 -2
- package/lib/typescript/module/ui/hooks/queries/userCache.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/utils/avatarUtils.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +15 -3
- package/src/ui/components/authChooser/requestSurfaces.tsx +12 -4
- package/src/ui/hooks/mutations/useAccountMutations.ts +20 -3
- package/src/ui/hooks/queries/__tests__/userCacheClear.test.ts +301 -0
- package/src/ui/hooks/queries/userCache.ts +211 -10
- package/src/ui/screens/AccountSettingsScreen.tsx +9 -1
- package/src/ui/screens/CreateAccountScreen.tsx +66 -36
- 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
|
|
6
|
-
import { DISPLAY_NAME_INVALID_MESSAGE, isValidDisplayName, MAX_DISPLAY_NAME_LENGTH,
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
255
|
+
name: { displayName: displayName.trim() },
|
|
237
256
|
bio: bio.trim() || undefined,
|
|
238
|
-
|
|
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,
|
|
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
|
-
{/*
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
331
|
+
{/* Categories — multi-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={
|
|
319
|
-
|
|
344
|
+
title={label}
|
|
345
|
+
disabled={!selected && accountCategories.length >= MAX_ACCOUNT_CATEGORIES}
|
|
346
|
+
onPress={() => toggleAccountCategory(option)}
|
|
320
347
|
showChevron={false}
|
|
321
348
|
rightElement={selected ? (
|
|
322
|
-
<
|
|
349
|
+
<Text className="text-caption-1 font-semibold text-primary">{position + 1}</Text>
|
|
323
350
|
) : undefined}
|
|
324
|
-
accessibilityLabel={
|
|
351
|
+
accessibilityLabel={label}
|
|
325
352
|
/>
|
|
326
353
|
);
|
|
327
354
|
})}
|
|
328
355
|
</SettingsListGroup>
|
|
329
|
-
|
|
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.
|