@oxyhq/services 5.16.7 → 5.16.9

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 (28) hide show
  1. package/lib/commonjs/ui/context/OxyContext.js +57 -2
  2. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  3. package/lib/commonjs/ui/screens/AccountOverviewScreen.js +5 -7
  4. package/lib/commonjs/ui/screens/AccountOverviewScreen.js.map +1 -1
  5. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +3 -3
  6. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  7. package/lib/commonjs/ui/utils/fileManagement.js +88 -0
  8. package/lib/commonjs/ui/utils/fileManagement.js.map +1 -1
  9. package/lib/module/ui/context/OxyContext.js +57 -2
  10. package/lib/module/ui/context/OxyContext.js.map +1 -1
  11. package/lib/module/ui/screens/AccountOverviewScreen.js +5 -7
  12. package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
  13. package/lib/module/ui/screens/AccountSettingsScreen.js +3 -3
  14. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  15. package/lib/module/ui/utils/fileManagement.js +87 -0
  16. package/lib/module/ui/utils/fileManagement.js.map +1 -1
  17. package/lib/typescript/ui/context/OxyContext.d.ts +1 -0
  18. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  19. package/lib/typescript/ui/screens/AccountOverviewScreen.d.ts.map +1 -1
  20. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  21. package/lib/typescript/ui/utils/fileManagement.d.ts +48 -0
  22. package/lib/typescript/ui/utils/fileManagement.d.ts.map +1 -1
  23. package/package.json +6 -2
  24. package/src/ui/context/OxyContext.tsx +62 -11
  25. package/src/ui/screens/AccountOverviewScreen.tsx +4 -3
  26. package/src/ui/screens/AccountSettingsScreen.tsx +3 -5
  27. package/src/ui/screens/FileManagementScreen.tsx +1 -1
  28. package/src/ui/utils/fileManagement.ts +105 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/services",
3
- "version": "5.16.7",
3
+ "version": "5.16.9",
4
4
  "description": "Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -161,7 +161,8 @@
161
161
  "react-native-gesture-handler": "~2.28.0",
162
162
  "react-native-reanimated": "~4.1.1",
163
163
  "react-native-safe-area-context": "~5.6.0",
164
- "react-native-svg": ">=13.0.0"
164
+ "react-native-svg": ">=13.0.0",
165
+ "expo-document-picker": "~14.0.0"
165
166
  },
166
167
  "peerDependenciesMeta": {
167
168
  "@expo/vector-icons": {
@@ -190,6 +191,9 @@
190
191
  },
191
192
  "@react-navigation/native": {
192
193
  "optional": true
194
+ },
195
+ "expo-document-picker": {
196
+ "optional": true
193
197
  }
194
198
  },
195
199
  "react-native-builder-bob": {
@@ -30,6 +30,8 @@ import { useQueryClient } from '@tanstack/react-query';
30
30
  import { clearQueryCache } from '../hooks/queryClient';
31
31
  import { useAccountStore } from '../stores/accountStore';
32
32
  import { KeyManager } from '../../crypto/keyManager';
33
+ import { translate } from '../../i18n';
34
+ import { queryKeys } from '../hooks/queries/queryKeys';
33
35
 
34
36
  export interface OxyContextState {
35
37
  user: User | null;
@@ -84,6 +86,7 @@ export interface OxyContextState {
84
86
  oxyServices: OxyServices;
85
87
  useFollow?: UseFollowHook;
86
88
  showBottomSheet?: (screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> }) => void;
89
+ openAvatarPicker: () => void;
87
90
  }
88
91
 
89
92
  const OxyContext = createContext<OxyContextState | null>(null);
@@ -323,7 +326,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
323
326
  const clearAllAccountData = useCallback(async (): Promise<void> => {
324
327
  // Clear TanStack Query cache (in-memory)
325
328
  queryClient.clear();
326
-
329
+
327
330
  // Clear persisted query cache
328
331
  if (storage) {
329
332
  try {
@@ -334,10 +337,10 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
334
337
  }
335
338
  }
336
339
  }
337
-
340
+
338
341
  // Clear session state (sessions, activeSessionId, storage)
339
342
  await clearSessionState();
340
-
343
+
341
344
  // Clear identity sync state from storage
342
345
  if (storage) {
343
346
  try {
@@ -348,14 +351,14 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
348
351
  }
349
352
  }
350
353
  }
351
-
354
+
352
355
  // Reset auth store identity sync state
353
356
  useAuthStore.getState().setIdentitySynced(false);
354
357
  useAuthStore.getState().setSyncing(false);
355
-
358
+
356
359
  // Reset account store
357
360
  useAccountStore.getState().reset();
358
-
361
+
359
362
  // Clear HTTP service cache
360
363
  oxyServices.clearCache();
361
364
  }, [queryClient, storage, clearSessionState, logger, oxyServices]);
@@ -369,7 +372,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
369
372
  ): Promise<void> => {
370
373
  // First, clear all account data
371
374
  await clearAllAccountData();
372
-
375
+
373
376
  // Then delete the identity keys
374
377
  await KeyManager.deleteIdentity(skipBackup, force, userConfirmed);
375
378
  }, [clearAllAccountData]);
@@ -381,7 +384,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
381
384
 
382
385
  let wasOffline = false;
383
386
  let checkTimeout: NodeJS.Timeout | null = null;
384
-
387
+
385
388
  // Circuit breaker and exponential backoff state
386
389
  const stateRef = {
387
390
  consecutiveFailures: 0,
@@ -438,10 +441,10 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
438
441
  } catch (error) {
439
442
  // Network check failed - we're offline
440
443
  wasOffline = true;
441
-
444
+
442
445
  // Increment failure count and apply exponential backoff
443
446
  stateRef.consecutiveFailures++;
444
-
447
+
445
448
  // Calculate new interval with exponential backoff, capped at maxInterval
446
449
  const backoffMultiplier = Math.min(
447
450
  Math.pow(2, stateRef.consecutiveFailures - 1),
@@ -451,7 +454,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
451
454
  stateRef.baseInterval * backoffMultiplier,
452
455
  stateRef.maxInterval
453
456
  );
454
-
457
+
455
458
  // If we hit the circuit breaker threshold, use max interval
456
459
  if (stateRef.consecutiveFailures >= stateRef.maxFailures) {
457
460
  stateRef.currentInterval = stateRef.maxInterval;
@@ -615,6 +618,52 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
615
618
  [],
616
619
  );
617
620
 
621
+ // Create openAvatarPicker function
622
+ const openAvatarPicker = useCallback(() => {
623
+ showBottomSheetForContext({
624
+ screen: 'FileManagement' as RouteName,
625
+ props: {
626
+ selectMode: true,
627
+ multiSelect: false,
628
+ disabledMimeTypes: ['video/', 'audio/', 'application/pdf'],
629
+ afterSelect: 'none', // Don't navigate away - stay on current screen
630
+ onSelect: async (file: any) => {
631
+ if (!file.contentType.startsWith('image/')) {
632
+ toast.error(translate(currentLanguage, 'editProfile.toasts.selectImage') || 'Please select an image file');
633
+ return;
634
+ }
635
+ try {
636
+ // Update file visibility to public for avatar (skip if temporary asset ID)
637
+ if (file.id && !file.id.startsWith('temp-')) {
638
+ try {
639
+ await oxyServices.assetUpdateVisibility(file.id, 'public');
640
+ console.log('[OxyContext] Avatar visibility updated to public');
641
+ } catch (visError: any) {
642
+ // Only log non-404 errors (404 means asset doesn't exist yet, which is OK)
643
+ if (visError?.response?.status !== 404) {
644
+ console.warn('[OxyContext] Failed to update avatar visibility, continuing anyway:', visError);
645
+ }
646
+ }
647
+ }
648
+
649
+ // Update user profile directly
650
+ await oxyServices.updateProfile({ avatar: file.id });
651
+
652
+ // Invalidate queries to refresh user data
653
+ queryClient.invalidateQueries({ queryKey: queryKeys.accounts.current() });
654
+ if (activeSessionId) {
655
+ queryClient.invalidateQueries({ queryKey: queryKeys.users.profile(activeSessionId) });
656
+ }
657
+
658
+ toast.success(translate(currentLanguage, 'editProfile.toasts.avatarUpdated') || 'Avatar updated');
659
+ } catch (e: any) {
660
+ toast.error(e.message || translate(currentLanguage, 'editProfile.toasts.updateAvatarFailed') || 'Failed to update avatar');
661
+ }
662
+ },
663
+ },
664
+ });
665
+ }, [oxyServices, currentLanguage, showBottomSheetForContext, queryClient, activeSessionId]);
666
+
618
667
  const contextValue: OxyContextState = useMemo(() => ({
619
668
  user,
620
669
  sessions,
@@ -654,6 +703,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
654
703
  oxyServices,
655
704
  useFollow: useFollowHook,
656
705
  showBottomSheet: showBottomSheetForContext,
706
+ openAvatarPicker,
657
707
  }), [
658
708
  activeSessionId,
659
709
  createIdentity,
@@ -689,6 +739,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
689
739
  useFollowHook,
690
740
  user,
691
741
  showBottomSheetForContext,
742
+ openAvatarPicker,
692
743
  ]);
693
744
 
694
745
  return (
@@ -77,6 +77,7 @@ const AccountOverviewScreen: React.FC<BaseScreenProps> = ({
77
77
  activeSessionId,
78
78
  oxyServices,
79
79
  isAuthenticated,
80
+ openAvatarPicker,
80
81
  } = useOxy();
81
82
  const { t } = useI18n();
82
83
  const [showMoreAccounts, setShowMoreAccounts] = useState(false);
@@ -108,10 +109,10 @@ const AccountOverviewScreen: React.FC<BaseScreenProps> = ({
108
109
  return undefined;
109
110
  }, [user?.avatar, oxyServices]);
110
111
 
111
- // Handle avatar press to navigate to EditProfile
112
+ // Handle avatar press - use openAvatarPicker from context
112
113
  const handleAvatarPress = useCallback(() => {
113
- navigate?.('EditProfile', { initialSection: 'profilePicture', initialField: 'avatar' });
114
- }, [navigate]);
114
+ openAvatarPicker();
115
+ }, [openAvatarPicker]);
115
116
 
116
117
  // Play Lottie animation once when component mounts
117
118
  useEffect(() => {
@@ -75,12 +75,12 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string;
75
75
  } = useOxy();
76
76
  const { t } = useI18n();
77
77
  const normalizedTheme = normalizeTheme(theme);
78
-
78
+
79
79
  // Use TanStack Query for user data
80
80
  const { data: user, isLoading: userLoading } = useCurrentUser({ enabled: isAuthenticated });
81
81
  const updateProfileMutation = useUpdateProfile();
82
82
  const uploadAvatarMutation = useUploadAvatar();
83
-
83
+
84
84
  // Fallback to store for backward compatibility
85
85
  const userFromStore = useAuthStore((state) => state.user);
86
86
  const finalUser = user || userFromStore;
@@ -503,9 +503,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string;
503
503
  });
504
504
  };
505
505
 
506
- const openAvatarPicker = useCallback(() => {
507
- toast.info?.(t('editProfile.toasts.avatarPickerUnavailable') || 'Avatar picker is not available in this build.');
508
- }, [t]);
506
+ const { openAvatarPicker } = useOxy();
509
507
 
510
508
  // Handlers to open modals
511
509
  const handleOpenDisplayNameModal = useCallback(() => setShowEditDisplayNameModal(true), []);
@@ -769,7 +769,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
769
769
 
770
770
  try {
771
771
  setIsPickingDocument(true);
772
-
772
+
773
773
  // Use expo-document-picker (works on all platforms including web)
774
774
  // On web, it uses the native file input and provides File objects directly
775
775
  const result = await DocumentPicker.getDocumentAsync({
@@ -1,6 +1,8 @@
1
1
  import { Alert } from 'react-native';
2
2
  import type { FileMetadata } from '../../models/interfaces';
3
3
  import { File as ExpoFile } from 'expo-file-system';
4
+ import { toast } from '../../lib/sonner';
5
+ import type { RouteName } from '../navigation/routes';
4
6
 
5
7
  /**
6
8
  * Format file size in bytes to human-readable string
@@ -188,3 +190,106 @@ export async function uploadFileRaw(
188
190
  return await oxyServices.uploadRawFile(file, visibility);
189
191
  }
190
192
 
193
+ /**
194
+ * Configuration for creating an avatar picker handler
195
+ */
196
+ export interface AvatarPickerConfig {
197
+ /** Navigation function from BaseScreenProps */
198
+ navigate?: (screen: RouteName, props?: Record<string, unknown>) => void;
199
+ /** OxyServices instance */
200
+ oxyServices: any;
201
+ /** TanStack Query mutation for updating profile */
202
+ updateProfileMutation: {
203
+ mutateAsync: (updates: { avatar: string }) => Promise<any>;
204
+ };
205
+ /** Callback to update local avatar state */
206
+ onAvatarSelected?: (fileId: string) => void;
207
+ /** i18n translation function */
208
+ t: (key: string) => string | undefined;
209
+ /** Optional context name for logging (e.g., 'AccountSettings', 'WelcomeNewUser') */
210
+ contextName?: string;
211
+ }
212
+
213
+ /**
214
+ * Creates a reusable avatar picker handler function.
215
+ *
216
+ * This function navigates to the FileManagement screen and handles:
217
+ * - Image file validation
218
+ * - File visibility update to public
219
+ * - Profile avatar update via mutation
220
+ * - Success/error toast notifications
221
+ *
222
+ * @example
223
+ * ```tsx
224
+ * const openAvatarPicker = createAvatarPickerHandler({
225
+ * navigate,
226
+ * oxyServices,
227
+ * updateProfileMutation,
228
+ * onAvatarSelected: setAvatarFileId,
229
+ * t,
230
+ * contextName: 'AccountSettings'
231
+ * });
232
+ *
233
+ * <TouchableOpacity onPress={openAvatarPicker}>
234
+ * <Text>Change Avatar</Text>
235
+ * </TouchableOpacity>
236
+ * ```
237
+ */
238
+ export function createAvatarPickerHandler(config: AvatarPickerConfig): () => void {
239
+ const {
240
+ navigate,
241
+ oxyServices,
242
+ updateProfileMutation,
243
+ onAvatarSelected,
244
+ t,
245
+ contextName = 'AvatarPicker'
246
+ } = config;
247
+
248
+ return () => {
249
+ if (!navigate) {
250
+ console.warn(`[${contextName}] navigate function is not available`);
251
+ return;
252
+ }
253
+
254
+ navigate('FileManagement', {
255
+ selectMode: true,
256
+ multiSelect: false,
257
+ disabledMimeTypes: ['video/', 'audio/', 'application/pdf'],
258
+ afterSelect: 'none', // Don't navigate away - stay on current screen
259
+ onSelect: async (file: any) => {
260
+ if (!file.contentType.startsWith('image/')) {
261
+ toast.error(t('editProfile.toasts.selectImage') || 'Please select an image file');
262
+ return;
263
+ }
264
+
265
+ try {
266
+ // Update file visibility to public for avatar (skip if temporary asset ID)
267
+ if (file.id && !file.id.startsWith('temp-')) {
268
+ try {
269
+ await oxyServices.assetUpdateVisibility(file.id, 'public');
270
+ console.log(`[${contextName}] Avatar visibility updated to public`);
271
+ } catch (visError: any) {
272
+ // Only log non-404 errors (404 means asset doesn't exist yet, which is OK)
273
+ if (visError?.response?.status !== 404) {
274
+ console.warn(`[${contextName}] Failed to update avatar visibility, continuing anyway:`, visError);
275
+ }
276
+ }
277
+ }
278
+
279
+ // Update local state if callback provided
280
+ if (onAvatarSelected) {
281
+ onAvatarSelected(file.id);
282
+ }
283
+
284
+ // Update user using TanStack Query mutation
285
+ await updateProfileMutation.mutateAsync({ avatar: file.id });
286
+
287
+ toast.success(t('editProfile.toasts.avatarUpdated') || 'Avatar updated');
288
+ } catch (e: any) {
289
+ toast.error(e.message || t('editProfile.toasts.updateAvatarFailed') || 'Failed to update avatar');
290
+ }
291
+ }
292
+ });
293
+ };
294
+ }
295
+