@oxyhq/services 5.16.7 → 5.16.8

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 +50 -2
  2. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  3. package/lib/commonjs/ui/screens/AccountOverviewScreen.js +6 -6
  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 +50 -2
  10. package/lib/module/ui/context/OxyContext.js.map +1 -1
  11. package/lib/module/ui/screens/AccountOverviewScreen.js +6 -6
  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 +49 -0
  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.8",
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,9 @@ 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';
35
+ import { useUpdateProfile } from '../hooks/mutations/useAccountMutations';
33
36
 
34
37
  export interface OxyContextState {
35
38
  user: User | null;
@@ -84,6 +87,7 @@ export interface OxyContextState {
84
87
  oxyServices: OxyServices;
85
88
  useFollow?: UseFollowHook;
86
89
  showBottomSheet?: (screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> }) => void;
90
+ openAvatarPicker: () => void;
87
91
  }
88
92
 
89
93
  const OxyContext = createContext<OxyContextState | null>(null);
@@ -482,6 +486,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
482
486
 
483
487
  const useFollowHook = loadUseFollowHook();
484
488
 
489
+ // Create update profile mutation for avatar picker
490
+ const updateProfileMutation = useUpdateProfile();
491
+
485
492
  const restoreSessionsFromStorage = useCallback(async (): Promise<void> => {
486
493
  if (!storage) {
487
494
  return;
@@ -615,6 +622,46 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
615
622
  [],
616
623
  );
617
624
 
625
+ // Create openAvatarPicker function
626
+ const openAvatarPicker = useCallback(() => {
627
+ showBottomSheetForContext({
628
+ screen: 'FileManagement' as RouteName,
629
+ props: {
630
+ selectMode: true,
631
+ multiSelect: false,
632
+ disabledMimeTypes: ['video/', 'audio/', 'application/pdf'],
633
+ afterSelect: 'none', // Don't navigate away - stay on current screen
634
+ onSelect: async (file: any) => {
635
+ if (!file.contentType.startsWith('image/')) {
636
+ toast.error(translate(currentLanguage, 'editProfile.toasts.selectImage') || 'Please select an image file');
637
+ return;
638
+ }
639
+ try {
640
+ // Update file visibility to public for avatar (skip if temporary asset ID)
641
+ if (file.id && !file.id.startsWith('temp-')) {
642
+ try {
643
+ await oxyServices.assetUpdateVisibility(file.id, 'public');
644
+ console.log('[OxyContext] Avatar visibility updated to public');
645
+ } catch (visError: any) {
646
+ // Only log non-404 errors (404 means asset doesn't exist yet, which is OK)
647
+ if (visError?.response?.status !== 404) {
648
+ console.warn('[OxyContext] Failed to update avatar visibility, continuing anyway:', visError);
649
+ }
650
+ }
651
+ }
652
+
653
+ // Update user profile using mutation hook (provides optimistic updates, error handling, retry)
654
+ await updateProfileMutation.mutateAsync({ avatar: file.id });
655
+
656
+ toast.success(translate(currentLanguage, 'editProfile.toasts.avatarUpdated') || 'Avatar updated');
657
+ } catch (e: any) {
658
+ toast.error(e.message || translate(currentLanguage, 'editProfile.toasts.updateAvatarFailed') || 'Failed to update avatar');
659
+ }
660
+ },
661
+ },
662
+ });
663
+ }, [oxyServices, currentLanguage, showBottomSheetForContext, updateProfileMutation]);
664
+
618
665
  const contextValue: OxyContextState = useMemo(() => ({
619
666
  user,
620
667
  sessions,
@@ -654,6 +701,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
654
701
  oxyServices,
655
702
  useFollow: useFollowHook,
656
703
  showBottomSheet: showBottomSheetForContext,
704
+ openAvatarPicker,
657
705
  }), [
658
706
  activeSessionId,
659
707
  createIdentity,
@@ -689,6 +737,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
689
737
  useFollowHook,
690
738
  user,
691
739
  showBottomSheetForContext,
740
+ openAvatarPicker,
692
741
  ]);
693
742
 
694
743
  return (
@@ -108,10 +108,11 @@ const AccountOverviewScreen: React.FC<BaseScreenProps> = ({
108
108
  return undefined;
109
109
  }, [user?.avatar, oxyServices]);
110
110
 
111
- // Handle avatar press to navigate to EditProfile
111
+ // Handle avatar press - use openAvatarPicker from context
112
+ const { openAvatarPicker } = useOxy();
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
+