@oxyhq/services 5.21.6 → 5.21.7

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 (129) hide show
  1. package/lib/commonjs/crypto/keyManager.js +67 -22
  2. package/lib/commonjs/crypto/keyManager.js.map +1 -1
  3. package/lib/commonjs/index.js +66 -0
  4. package/lib/commonjs/index.js.map +1 -1
  5. package/lib/commonjs/ui/components/fileManagement/AnimatedButton.js +57 -0
  6. package/lib/commonjs/ui/components/fileManagement/AnimatedButton.js.map +1 -0
  7. package/lib/commonjs/ui/components/profile/EditBioModal.js +24 -156
  8. package/lib/commonjs/ui/components/profile/EditBioModal.js.map +1 -1
  9. package/lib/commonjs/ui/components/profile/EditDisplayNameModal.js +28 -178
  10. package/lib/commonjs/ui/components/profile/EditDisplayNameModal.js.map +1 -1
  11. package/lib/commonjs/ui/components/profile/EditEmailModal.js +32 -159
  12. package/lib/commonjs/ui/components/profile/EditEmailModal.js.map +1 -1
  13. package/lib/commonjs/ui/components/profile/EditLocationModal.js +45 -227
  14. package/lib/commonjs/ui/components/profile/EditLocationModal.js.map +1 -1
  15. package/lib/commonjs/ui/components/profile/EditUsernameModal.js +30 -155
  16. package/lib/commonjs/ui/components/profile/EditUsernameModal.js.map +1 -1
  17. package/lib/commonjs/ui/hooks/mutations/mutationFactory.js +177 -0
  18. package/lib/commonjs/ui/hooks/mutations/mutationFactory.js.map +1 -0
  19. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +10 -123
  20. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  21. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +2 -32
  22. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
  23. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js +2 -31
  24. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js.map +1 -1
  25. package/lib/commonjs/ui/hooks/useFileFiltering.js +76 -0
  26. package/lib/commonjs/ui/hooks/useFileFiltering.js.map +1 -0
  27. package/lib/commonjs/ui/screens/FileManagementScreen.js +2 -2
  28. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  29. package/lib/commonjs/ui/utils/authHelpers.js +164 -0
  30. package/lib/commonjs/ui/utils/authHelpers.js.map +1 -0
  31. package/lib/commonjs/ui/utils/avatarUtils.js +18 -61
  32. package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
  33. package/lib/module/crypto/keyManager.js +67 -22
  34. package/lib/module/crypto/keyManager.js.map +1 -1
  35. package/lib/module/index.js +6 -0
  36. package/lib/module/index.js.map +1 -1
  37. package/lib/module/ui/components/fileManagement/AnimatedButton.js +50 -0
  38. package/lib/module/ui/components/fileManagement/AnimatedButton.js.map +1 -0
  39. package/lib/module/ui/components/profile/EditBioModal.js +24 -156
  40. package/lib/module/ui/components/profile/EditBioModal.js.map +1 -1
  41. package/lib/module/ui/components/profile/EditDisplayNameModal.js +28 -178
  42. package/lib/module/ui/components/profile/EditDisplayNameModal.js.map +1 -1
  43. package/lib/module/ui/components/profile/EditEmailModal.js +32 -159
  44. package/lib/module/ui/components/profile/EditEmailModal.js.map +1 -1
  45. package/lib/module/ui/components/profile/EditLocationModal.js +45 -227
  46. package/lib/module/ui/components/profile/EditLocationModal.js.map +1 -1
  47. package/lib/module/ui/components/profile/EditUsernameModal.js +30 -155
  48. package/lib/module/ui/components/profile/EditUsernameModal.js.map +1 -1
  49. package/lib/module/ui/hooks/mutations/mutationFactory.js +173 -0
  50. package/lib/module/ui/hooks/mutations/mutationFactory.js.map +1 -0
  51. package/lib/module/ui/hooks/mutations/useAccountMutations.js +10 -122
  52. package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  53. package/lib/module/ui/hooks/queries/useAccountQueries.js +2 -32
  54. package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
  55. package/lib/module/ui/hooks/queries/useServicesQueries.js +2 -31
  56. package/lib/module/ui/hooks/queries/useServicesQueries.js.map +1 -1
  57. package/lib/module/ui/hooks/useFileFiltering.js +72 -0
  58. package/lib/module/ui/hooks/useFileFiltering.js.map +1 -0
  59. package/lib/module/ui/screens/FileManagementScreen.js +2 -2
  60. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  61. package/lib/module/ui/utils/authHelpers.js +154 -0
  62. package/lib/module/ui/utils/authHelpers.js.map +1 -0
  63. package/lib/module/ui/utils/avatarUtils.js +18 -61
  64. package/lib/module/ui/utils/avatarUtils.js.map +1 -1
  65. package/lib/typescript/commonjs/crypto/keyManager.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/index.d.ts +6 -0
  67. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/ui/components/fileManagement/AnimatedButton.d.ts +16 -0
  69. package/lib/typescript/commonjs/ui/components/fileManagement/AnimatedButton.d.ts.map +1 -0
  70. package/lib/typescript/commonjs/ui/components/profile/EditBioModal.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/ui/components/profile/EditDisplayNameModal.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/ui/components/profile/EditEmailModal.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/ui/components/profile/EditLocationModal.d.ts +1 -0
  74. package/lib/typescript/commonjs/ui/components/profile/EditLocationModal.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/ui/components/profile/EditUsernameModal.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/ui/hooks/mutations/mutationFactory.d.ts +76 -0
  77. package/lib/typescript/commonjs/ui/hooks/mutations/mutationFactory.d.ts.map +1 -0
  78. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +29 -4
  79. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts +1 -1
  81. package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/ui/hooks/queries/useServicesQueries.d.ts +1 -1
  83. package/lib/typescript/commonjs/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/ui/hooks/useFileFiltering.d.ts +29 -0
  85. package/lib/typescript/commonjs/ui/hooks/useFileFiltering.d.ts.map +1 -0
  86. package/lib/typescript/commonjs/ui/utils/authHelpers.d.ts +99 -0
  87. package/lib/typescript/commonjs/ui/utils/authHelpers.d.ts.map +1 -0
  88. package/lib/typescript/commonjs/ui/utils/avatarUtils.d.ts.map +1 -1
  89. package/lib/typescript/module/crypto/keyManager.d.ts.map +1 -1
  90. package/lib/typescript/module/index.d.ts +6 -0
  91. package/lib/typescript/module/index.d.ts.map +1 -1
  92. package/lib/typescript/module/ui/components/fileManagement/AnimatedButton.d.ts +16 -0
  93. package/lib/typescript/module/ui/components/fileManagement/AnimatedButton.d.ts.map +1 -0
  94. package/lib/typescript/module/ui/components/profile/EditBioModal.d.ts.map +1 -1
  95. package/lib/typescript/module/ui/components/profile/EditDisplayNameModal.d.ts.map +1 -1
  96. package/lib/typescript/module/ui/components/profile/EditEmailModal.d.ts.map +1 -1
  97. package/lib/typescript/module/ui/components/profile/EditLocationModal.d.ts +1 -0
  98. package/lib/typescript/module/ui/components/profile/EditLocationModal.d.ts.map +1 -1
  99. package/lib/typescript/module/ui/components/profile/EditUsernameModal.d.ts.map +1 -1
  100. package/lib/typescript/module/ui/hooks/mutations/mutationFactory.d.ts +76 -0
  101. package/lib/typescript/module/ui/hooks/mutations/mutationFactory.d.ts.map +1 -0
  102. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +29 -4
  103. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  104. package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts +1 -1
  105. package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
  106. package/lib/typescript/module/ui/hooks/queries/useServicesQueries.d.ts +1 -1
  107. package/lib/typescript/module/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
  108. package/lib/typescript/module/ui/hooks/useFileFiltering.d.ts +29 -0
  109. package/lib/typescript/module/ui/hooks/useFileFiltering.d.ts.map +1 -0
  110. package/lib/typescript/module/ui/utils/authHelpers.d.ts +99 -0
  111. package/lib/typescript/module/ui/utils/authHelpers.d.ts.map +1 -0
  112. package/lib/typescript/module/ui/utils/avatarUtils.d.ts.map +1 -1
  113. package/package.json +1 -1
  114. package/src/crypto/keyManager.ts +23 -22
  115. package/src/index.ts +25 -0
  116. package/src/ui/components/fileManagement/AnimatedButton.tsx +56 -0
  117. package/src/ui/components/profile/EditBioModal.tsx +38 -176
  118. package/src/ui/components/profile/EditDisplayNameModal.tsx +48 -195
  119. package/src/ui/components/profile/EditEmailModal.tsx +49 -180
  120. package/src/ui/components/profile/EditLocationModal.tsx +76 -263
  121. package/src/ui/components/profile/EditUsernameModal.tsx +47 -175
  122. package/src/ui/hooks/mutations/mutationFactory.ts +215 -0
  123. package/src/ui/hooks/mutations/useAccountMutations.ts +48 -136
  124. package/src/ui/hooks/queries/useAccountQueries.ts +6 -33
  125. package/src/ui/hooks/queries/useServicesQueries.ts +6 -32
  126. package/src/ui/hooks/useFileFiltering.ts +115 -0
  127. package/src/ui/screens/FileManagementScreen.tsx +2 -2
  128. package/src/ui/utils/authHelpers.ts +183 -0
  129. package/src/ui/utils/avatarUtils.ts +25 -65
@@ -1,183 +1,55 @@
1
- import React, { useState, useEffect } from 'react';
2
- import {
3
- View,
4
- Text,
5
- TextInput,
6
- TouchableOpacity,
7
- StyleSheet,
8
- Modal,
9
- Platform,
10
- } from 'react-native';
11
- import { Ionicons } from '@expo/vector-icons';
12
- import { useThemeStyles } from '../../hooks/useThemeStyles';
13
- import { useColorScheme } from '../../hooks/use-color-scheme';
14
- import { useI18n } from '../../hooks/useI18n';
15
- import { fontFamilies } from '../../styles/fonts';
1
+ import React from 'react';
2
+ import { EditFieldModal } from './EditFieldModal';
16
3
  import { useProfileEditing } from '../../hooks/useProfileEditing';
4
+ import { useI18n } from '../../hooks/useI18n';
17
5
 
18
6
  interface EditUsernameModalProps {
19
- visible: boolean;
20
- onClose: () => void;
21
- initialValue?: string;
22
- theme?: 'light' | 'dark';
23
- onSave?: () => void;
7
+ visible: boolean;
8
+ onClose: () => void;
9
+ initialValue?: string;
10
+ theme?: 'light' | 'dark';
11
+ onSave?: () => void;
24
12
  }
25
13
 
26
14
  export const EditUsernameModal: React.FC<EditUsernameModalProps> = ({
27
- visible,
28
- onClose,
29
- initialValue = '',
30
- theme = 'light',
31
- onSave,
15
+ visible,
16
+ onClose,
17
+ initialValue = '',
18
+ theme = 'light',
19
+ onSave,
32
20
  }) => {
33
- const { t } = useI18n();
34
- const colorScheme = useColorScheme();
35
- const themeStyles = useThemeStyles(theme || 'light', colorScheme);
36
- const colors = themeStyles.colors;
37
- const { updateField, isSaving } = useProfileEditing();
38
-
39
- const [username, setUsername] = useState(initialValue);
40
-
41
- useEffect(() => {
42
- if (visible) {
43
- setUsername(initialValue);
44
- }
45
- }, [visible, initialValue]);
46
-
47
- const handleSave = async () => {
48
- const success = await updateField('username', username);
49
- if (success) {
50
- onSave?.();
51
- onClose();
52
- }
53
- };
54
-
55
- return (
56
- <Modal
57
- visible={visible}
58
- animationType="slide"
59
- transparent={true}
60
- onRequestClose={onClose}
61
- >
62
- <View style={styles.modalOverlay}>
63
- <View style={[styles.modalContent, { backgroundColor: colors.background }]}>
64
- <View style={styles.modalHeader}>
65
- <TouchableOpacity onPress={onClose} style={styles.closeButton}>
66
- <Ionicons name="close" size={24} color={colors.text} />
67
- </TouchableOpacity>
68
- <Text style={[styles.modalTitle, { color: colors.text }]}>
69
- {t('editProfile.items.username.title') || 'Username'}
70
- </Text>
71
- <TouchableOpacity
72
- onPress={handleSave}
73
- disabled={isSaving || !username.trim()}
74
- style={[styles.saveButton, { opacity: (isSaving || !username.trim()) ? 0.5 : 1 }]}
75
- >
76
- <Text style={[styles.saveButtonText, { color: colors.tint }]}>
77
- {isSaving ? 'Saving...' : 'Save'}
78
- </Text>
79
- </TouchableOpacity>
80
- </View>
81
-
82
- <View style={styles.modalBody}>
83
- <View style={styles.inputGroup}>
84
- <Text style={[styles.label, { color: colors.text }]}>
85
- {t('editProfile.items.username.label') || 'Username'}
86
- </Text>
87
- <TextInput
88
- style={[
89
- styles.input,
90
- {
91
- backgroundColor: colors.card,
92
- color: colors.text,
93
- borderColor: colors.border,
94
- },
95
- ]}
96
- value={username}
97
- onChangeText={setUsername}
98
- placeholder={t('editProfile.items.username.placeholder') || 'Choose a username'}
99
- placeholderTextColor={colors.secondaryText}
100
- autoFocus
101
- autoCapitalize="none"
102
- autoCorrect={false}
103
- selectionColor={colors.tint}
104
- />
105
- </View>
106
- </View>
107
- </View>
108
- </View>
109
- </Modal>
110
- );
21
+ const { t } = useI18n();
22
+ const { updateField } = useProfileEditing();
23
+
24
+ return (
25
+ <EditFieldModal
26
+ visible={visible}
27
+ onClose={onClose}
28
+ title={t('editProfile.items.username.title') || 'Username'}
29
+ theme={theme}
30
+ onSave={onSave}
31
+ variant="single"
32
+ fields={[
33
+ {
34
+ key: 'username',
35
+ label: t('editProfile.items.username.label') || 'Username',
36
+ initialValue,
37
+ placeholder: t('editProfile.items.username.placeholder') || 'Choose a username',
38
+ validation: (value) => {
39
+ if (!value.trim()) {
40
+ return t('editProfile.items.username.required') || 'Username is required';
41
+ }
42
+ return undefined;
43
+ },
44
+ inputProps: {
45
+ autoCapitalize: 'none',
46
+ autoCorrect: false,
47
+ },
48
+ },
49
+ ]}
50
+ onSubmit={async (data) => {
51
+ return await updateField('username', data.username as string);
52
+ }}
53
+ />
54
+ );
111
55
  };
112
-
113
- const styles = StyleSheet.create({
114
- modalOverlay: {
115
- flex: 1,
116
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
117
- justifyContent: 'flex-end',
118
- },
119
- modalContent: {
120
- borderTopLeftRadius: 20,
121
- borderTopRightRadius: 20,
122
- paddingTop: Platform.OS === 'ios' ? 20 : 16,
123
- maxHeight: '80%',
124
- },
125
- modalHeader: {
126
- flexDirection: 'row',
127
- alignItems: 'center',
128
- justifyContent: 'space-between',
129
- paddingHorizontal: 16,
130
- paddingBottom: 16,
131
- borderBottomWidth: StyleSheet.hairlineWidth,
132
- borderBottomColor: '#E5E5EA',
133
- },
134
- closeButton: {
135
- width: 40,
136
- height: 40,
137
- alignItems: 'center',
138
- justifyContent: 'center',
139
- },
140
- modalTitle: {
141
- fontSize: 18,
142
- fontWeight: '600',
143
- fontFamily: fontFamilies.phuduSemiBold,
144
- flex: 1,
145
- textAlign: 'center',
146
- },
147
- saveButton: {
148
- paddingHorizontal: 16,
149
- paddingVertical: 8,
150
- },
151
- saveButtonText: {
152
- fontSize: 16,
153
- fontWeight: '600',
154
- fontFamily: fontFamilies.phuduSemiBold,
155
- },
156
- modalBody: {
157
- padding: 16,
158
- gap: 16,
159
- },
160
- inputGroup: {
161
- gap: 8,
162
- },
163
- label: {
164
- fontSize: 14,
165
- fontWeight: '600',
166
- fontFamily: fontFamilies.phuduSemiBold,
167
- },
168
- input: {
169
- borderWidth: StyleSheet.hairlineWidth,
170
- borderRadius: 12,
171
- padding: 16,
172
- fontSize: 16,
173
- minHeight: 52,
174
- },
175
- });
176
-
177
-
178
-
179
-
180
-
181
-
182
-
183
-
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Mutation Factory - Creates standardized mutations with optimistic updates
3
+ *
4
+ * This factory reduces boilerplate code for mutations that follow the common pattern:
5
+ * 1. Cancel outgoing queries
6
+ * 2. Snapshot previous data
7
+ * 3. Apply optimistic update
8
+ * 4. On error: rollback and show toast
9
+ * 5. On success: update cache, stores, and invalidate queries
10
+ */
11
+
12
+ import { QueryClient, UseMutationOptions } from '@tanstack/react-query';
13
+ import type { User } from '../../../models/interfaces';
14
+ import { queryKeys, invalidateAccountQueries, invalidateUserQueries } from '../queries/queryKeys';
15
+ import { toast } from '../../../lib/sonner';
16
+ import { useAuthStore } from '../../stores/authStore';
17
+
18
+ /**
19
+ * Configuration for creating a standard profile mutation
20
+ */
21
+ export interface ProfileMutationConfig<TData, TVariables> {
22
+ /** The mutation function that makes the API call */
23
+ mutationFn: (variables: TVariables) => Promise<TData>;
24
+ /** Query keys to cancel before mutation */
25
+ cancelQueryKeys?: unknown[][];
26
+ /** Function to apply optimistic update to the user data */
27
+ optimisticUpdate?: (previousUser: User, variables: TVariables) => Partial<User>;
28
+ /** Error message to show on failure */
29
+ errorMessage?: string | ((error: Error) => string);
30
+ /** Success message to show (optional) */
31
+ successMessage?: string;
32
+ /** Whether to update authStore on success (default: true) */
33
+ updateAuthStore?: boolean;
34
+ /** Whether to invalidate user queries on success (default: true) */
35
+ invalidateUserQueries?: boolean;
36
+ /** Whether to invalidate account queries on success (default: true) */
37
+ invalidateAccountQueries?: boolean;
38
+ /** Custom onSuccess handler */
39
+ onSuccess?: (data: TData, variables: TVariables, queryClient: QueryClient) => void;
40
+ }
41
+
42
+ /**
43
+ * Creates a standard profile mutation with optimistic updates
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const updateProfile = createProfileMutation({
48
+ * mutationFn: (updates) => oxyServices.updateProfile(updates),
49
+ * optimisticUpdate: (user, updates) => updates,
50
+ * errorMessage: 'Failed to update profile',
51
+ * });
52
+ * ```
53
+ */
54
+ export function createProfileMutation<TVariables>(
55
+ config: ProfileMutationConfig<User, TVariables>,
56
+ queryClient: QueryClient,
57
+ activeSessionId: string | null
58
+ ): UseMutationOptions<User, Error, TVariables, { previousUser?: User }> {
59
+ const {
60
+ mutationFn,
61
+ cancelQueryKeys = [],
62
+ optimisticUpdate,
63
+ errorMessage = 'Operation failed',
64
+ successMessage,
65
+ updateAuthStore = true,
66
+ invalidateUserQueries: shouldInvalidateUserQueries = true,
67
+ invalidateAccountQueries: shouldInvalidateAccountQueries = true,
68
+ onSuccess: customOnSuccess,
69
+ } = config;
70
+
71
+ return {
72
+ mutationFn,
73
+
74
+ onMutate: async (variables) => {
75
+ // Cancel queries that might conflict
76
+ await queryClient.cancelQueries({ queryKey: queryKeys.accounts.current() });
77
+ for (const key of cancelQueryKeys) {
78
+ await queryClient.cancelQueries({ queryKey: key });
79
+ }
80
+
81
+ // Snapshot previous user data
82
+ const previousUser = queryClient.getQueryData<User>(queryKeys.accounts.current());
83
+
84
+ // Apply optimistic update if provided
85
+ if (previousUser && optimisticUpdate) {
86
+ const updates = optimisticUpdate(previousUser, variables);
87
+ const optimisticUser = { ...previousUser, ...updates };
88
+
89
+ queryClient.setQueryData<User>(queryKeys.accounts.current(), optimisticUser);
90
+
91
+ if (activeSessionId) {
92
+ queryClient.setQueryData<User>(queryKeys.users.profile(activeSessionId), optimisticUser);
93
+ }
94
+ }
95
+
96
+ return { previousUser };
97
+ },
98
+
99
+ onError: (error, _variables, context) => {
100
+ // Rollback optimistic update
101
+ if (context?.previousUser) {
102
+ queryClient.setQueryData(queryKeys.accounts.current(), context.previousUser);
103
+ if (activeSessionId) {
104
+ queryClient.setQueryData(queryKeys.users.profile(activeSessionId), context.previousUser);
105
+ }
106
+ }
107
+
108
+ // Show error toast
109
+ const message = typeof errorMessage === 'function'
110
+ ? errorMessage(error)
111
+ : (error instanceof Error ? error.message : errorMessage);
112
+ toast.error(message);
113
+ },
114
+
115
+ onSuccess: (data, variables) => {
116
+ // Update cache with server response
117
+ queryClient.setQueryData(queryKeys.accounts.current(), data);
118
+ if (activeSessionId) {
119
+ queryClient.setQueryData(queryKeys.users.profile(activeSessionId), data);
120
+ }
121
+
122
+ // Update authStore for immediate UI updates
123
+ if (updateAuthStore) {
124
+ useAuthStore.getState().setUser(data);
125
+ }
126
+
127
+ // Invalidate related queries
128
+ if (shouldInvalidateUserQueries) {
129
+ invalidateUserQueries(queryClient);
130
+ }
131
+ if (shouldInvalidateAccountQueries) {
132
+ invalidateAccountQueries(queryClient);
133
+ }
134
+
135
+ // Show success toast if configured
136
+ if (successMessage) {
137
+ toast.success(successMessage);
138
+ }
139
+
140
+ // Call custom onSuccess handler
141
+ if (customOnSuccess) {
142
+ customOnSuccess(data, variables, queryClient);
143
+ }
144
+ },
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Configuration for creating a generic mutation (non-profile)
150
+ */
151
+ export interface GenericMutationConfig<TData, TVariables, TContext> {
152
+ /** The mutation function */
153
+ mutationFn: (variables: TVariables) => Promise<TData>;
154
+ /** Query key for optimistic data */
155
+ queryKey: unknown[];
156
+ /** Function to create optimistic data */
157
+ optimisticData?: (previous: TData | undefined, variables: TVariables) => TData;
158
+ /** Error message */
159
+ errorMessage?: string;
160
+ /** Success message */
161
+ successMessage?: string;
162
+ /** Additional queries to invalidate on success */
163
+ invalidateQueries?: unknown[][];
164
+ }
165
+
166
+ /**
167
+ * Creates a generic mutation with optimistic updates
168
+ */
169
+ export function createGenericMutation<TData, TVariables>(
170
+ config: GenericMutationConfig<TData, TVariables, { previous?: TData }>,
171
+ queryClient: QueryClient
172
+ ): UseMutationOptions<TData, Error, TVariables, { previous?: TData }> {
173
+ const {
174
+ mutationFn,
175
+ queryKey,
176
+ optimisticData,
177
+ errorMessage = 'Operation failed',
178
+ successMessage,
179
+ invalidateQueries = [],
180
+ } = config;
181
+
182
+ return {
183
+ mutationFn,
184
+
185
+ onMutate: async (variables) => {
186
+ await queryClient.cancelQueries({ queryKey });
187
+ const previous = queryClient.getQueryData<TData>(queryKey);
188
+
189
+ if (optimisticData) {
190
+ queryClient.setQueryData<TData>(queryKey, optimisticData(previous, variables));
191
+ }
192
+
193
+ return { previous };
194
+ },
195
+
196
+ onError: (error, _variables, context) => {
197
+ if (context?.previous !== undefined) {
198
+ queryClient.setQueryData(queryKey, context.previous);
199
+ }
200
+ toast.error(error instanceof Error ? error.message : errorMessage);
201
+ },
202
+
203
+ onSuccess: (data) => {
204
+ queryClient.setQueryData(queryKey, data);
205
+
206
+ for (const key of invalidateQueries) {
207
+ queryClient.invalidateQueries({ queryKey: key });
208
+ }
209
+
210
+ if (successMessage) {
211
+ toast.success(successMessage);
212
+ }
213
+ },
214
+ };
215
+ }
@@ -5,6 +5,7 @@ import { useOxy } from '../../context/OxyContext';
5
5
  import { toast } from '../../../lib/sonner';
6
6
  import { refreshAvatarInStore } from '../../utils/avatarUtils';
7
7
  import { useAuthStore } from '../../stores/authStore';
8
+ import { authenticatedApiCall } from '../../utils/authHelpers';
8
9
 
9
10
  /**
10
11
  * Update user profile with optimistic updates and offline queue support
@@ -15,38 +16,11 @@ export const useUpdateProfile = () => {
15
16
 
16
17
  return useMutation({
17
18
  mutationFn: async (updates: Partial<User>) => {
18
- // Ensure we have a valid token before making the request
19
- if (!oxyServices.hasValidToken() && activeSessionId) {
20
- try {
21
- // Try to get token for the session
22
- await oxyServices.getTokenBySession(activeSessionId);
23
- } catch (tokenError) {
24
- // If getting token fails, might be an offline session - try syncing
25
- const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
26
- if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
27
- // Session sync should be handled by the app layer
28
- throw new Error('Session needs to be synced. Please try again.');
29
- } else {
30
- throw tokenError;
31
- }
32
- }
33
- }
34
-
35
- try {
36
- return await oxyServices.updateProfile(updates);
37
- } catch (error: any) {
38
- const errorMessage = error?.message || '';
39
- const status = error?.status || error?.response?.status;
40
-
41
- // Handle authentication errors
42
- if (status === 401 || errorMessage.includes('Authentication required') || errorMessage.includes('Invalid or missing authorization header')) {
43
- // Session sync should be handled by the app layer
44
- throw new Error('Authentication failed. Please sign in again.');
45
- }
46
-
47
- // TanStack Query will automatically retry on network errors
48
- throw error;
49
- }
19
+ return authenticatedApiCall<User>(
20
+ oxyServices,
21
+ activeSessionId,
22
+ () => oxyServices.updateProfile(updates)
23
+ );
50
24
  },
51
25
  // Optimistic update
52
26
  onMutate: async (updates) => {
@@ -116,22 +90,7 @@ export const useUploadAvatar = () => {
116
90
 
117
91
  return useMutation({
118
92
  mutationFn: async (file: { uri: string; type?: string; name?: string; size?: number }) => {
119
- // Ensure we have a valid token before making the request
120
- if (!oxyServices.hasValidToken() && activeSessionId) {
121
- try {
122
- await oxyServices.getTokenBySession(activeSessionId);
123
- } catch (tokenError) {
124
- const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
125
- if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
126
- // Session sync should be handled by the app layer
127
- throw new Error('Session needs to be synced. Please try again.');
128
- } else {
129
- throw tokenError;
130
- }
131
- }
132
- }
133
-
134
- try {
93
+ return authenticatedApiCall<User>(oxyServices, activeSessionId, async () => {
135
94
  // Upload file first
136
95
  const uploadResult = await oxyServices.assetUpload(file as any, 'public');
137
96
  const fileId = uploadResult?.file?.id || uploadResult?.id || uploadResult;
@@ -142,19 +101,7 @@ export const useUploadAvatar = () => {
142
101
 
143
102
  // Update profile with file ID
144
103
  return await oxyServices.updateProfile({ avatar: fileId });
145
- } catch (error: any) {
146
- const errorMessage = error?.message || '';
147
- const status = error?.status || error?.response?.status;
148
-
149
- // Handle authentication errors
150
- if (status === 401 || errorMessage.includes('Authentication required') || errorMessage.includes('Invalid or missing authorization header')) {
151
- // Session sync should be handled by the app layer
152
- throw new Error('Authentication failed. Please sign in again.');
153
- }
154
-
155
- // TanStack Query will automatically retry on network errors
156
- throw error;
157
- }
104
+ });
158
105
  },
159
106
  onMutate: async (file) => {
160
107
  await queryClient.cancelQueries({ queryKey: queryKeys.accounts.current() });
@@ -267,38 +214,11 @@ export const useUpdatePrivacySettings = () => {
267
214
  throw new Error('User ID is required');
268
215
  }
269
216
 
270
- // Ensure we have a valid token before making the request
271
- if (!oxyServices.hasValidToken() && activeSessionId) {
272
- try {
273
- // Try to get token for the session
274
- await oxyServices.getTokenBySession(activeSessionId);
275
- } catch (tokenError) {
276
- // If getting token fails, might be an offline session - try syncing
277
- const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
278
- if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
279
- // Session sync should be handled by the app layer
280
- throw new Error('Session needs to be synced. Please try again.');
281
- } else {
282
- throw tokenError;
283
- }
284
- }
285
- }
286
-
287
- try {
288
- return await oxyServices.updatePrivacySettings(settings, targetUserId);
289
- } catch (error: any) {
290
- const errorMessage = error?.message || '';
291
- const status = error?.status || error?.response?.status;
292
-
293
- // Handle authentication errors
294
- if (status === 401 || errorMessage.includes('Authentication required') || errorMessage.includes('Invalid or missing authorization header')) {
295
- // Session sync should be handled by the app layer
296
- throw new Error('Authentication failed. Please sign in again.');
297
- }
298
-
299
- // TanStack Query will automatically retry on network errors
300
- throw error;
301
- }
217
+ return authenticatedApiCall<Record<string, unknown>>(
218
+ oxyServices,
219
+ activeSessionId,
220
+ () => oxyServices.updatePrivacySettings(settings, targetUserId)
221
+ );
302
222
  },
303
223
  // Optimistic update
304
224
  onMutate: async ({ settings, userId }) => {
@@ -354,12 +274,12 @@ export const useUpdatePrivacySettings = () => {
354
274
  // Also update account query if it contains privacy settings
355
275
  const currentUser = queryClient.getQueryData<User>(queryKeys.accounts.current());
356
276
  if (currentUser) {
357
- const updatedUser = {
277
+ const updatedUser: User = {
358
278
  ...currentUser,
359
- privacySettings: data,
279
+ privacySettings: data as { [key: string]: unknown },
360
280
  };
361
281
  queryClient.setQueryData<User>(queryKeys.accounts.current(), updatedUser);
362
-
282
+
363
283
  // Update authStore so frontend components see the changes immediately
364
284
  useAuthStore.getState().setUser(updatedUser);
365
285
  }
@@ -376,6 +296,25 @@ export const useUpdatePrivacySettings = () => {
376
296
  });
377
297
  };
378
298
 
299
+ /** Uploaded file data structure from API */
300
+ interface UploadedFile {
301
+ id: string;
302
+ originalName?: string;
303
+ sha256?: string;
304
+ mime?: string;
305
+ size?: number;
306
+ createdAt?: string;
307
+ metadata?: Record<string, unknown>;
308
+ variants?: Array<{ type: string; key: string; width?: number; height?: number; readyAt?: string; metadata?: Record<string, unknown> }>;
309
+ }
310
+
311
+ /** Upload result type that supports both single file and batch responses */
312
+ interface UploadResult {
313
+ file?: UploadedFile;
314
+ files?: UploadedFile[];
315
+ id?: string;
316
+ }
317
+
379
318
  /**
380
319
  * Upload file with authentication handling and progress tracking
381
320
  */
@@ -383,49 +322,22 @@ export const useUploadFile = () => {
383
322
  const { oxyServices, activeSessionId } = useOxy();
384
323
 
385
324
  return useMutation({
386
- mutationFn: async ({
387
- file,
388
- visibility,
389
- metadata,
390
- onProgress
391
- }: {
392
- file: File;
393
- visibility?: 'private' | 'public' | 'unlisted';
325
+ mutationFn: async ({
326
+ file,
327
+ visibility,
328
+ metadata,
329
+ onProgress
330
+ }: {
331
+ file: File;
332
+ visibility?: 'private' | 'public' | 'unlisted';
394
333
  metadata?: Record<string, any>;
395
334
  onProgress?: (progress: number) => void;
396
335
  }) => {
397
- // Ensure we have a valid token before making the request
398
- if (!oxyServices.hasValidToken() && activeSessionId) {
399
- try {
400
- // Try to get token for the session
401
- await oxyServices.getTokenBySession(activeSessionId);
402
- } catch (tokenError) {
403
- // If getting token fails, might be an offline session - try syncing
404
- const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
405
- if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
406
- // Session sync should be handled by the app layer
407
- throw new Error('Session needs to be synced. Please try again.');
408
- } else {
409
- throw tokenError;
410
- }
411
- }
412
- }
413
-
414
- try {
415
- return await oxyServices.assetUpload(file as any, visibility, metadata, onProgress);
416
- } catch (error: any) {
417
- const errorMessage = error?.message || '';
418
- const status = error?.status || error?.response?.status;
419
-
420
- // Handle authentication errors
421
- if (status === 401 || errorMessage.includes('Authentication required') || errorMessage.includes('Invalid or missing authorization header')) {
422
- // Session sync should be handled by the app layer
423
- throw new Error('Authentication failed. Please sign in again.');
424
- }
425
-
426
- // TanStack Query will automatically retry on network errors
427
- throw error;
428
- }
336
+ return authenticatedApiCall<UploadResult>(
337
+ oxyServices,
338
+ activeSessionId,
339
+ () => oxyServices.assetUpload(file as any, visibility, metadata, onProgress)
340
+ );
429
341
  },
430
342
  });
431
343
  };