@oxyhq/services 5.21.5 → 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 (177) 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/BottomSheetRouter.js +100 -286
  6. package/lib/commonjs/ui/components/BottomSheetRouter.js.map +1 -1
  7. package/lib/commonjs/ui/components/GroupedItem.js +0 -3
  8. package/lib/commonjs/ui/components/GroupedItem.js.map +1 -1
  9. package/lib/commonjs/ui/components/OxyProvider.js +14 -19
  10. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  11. package/lib/commonjs/ui/components/fileManagement/AnimatedButton.js +57 -0
  12. package/lib/commonjs/ui/components/fileManagement/AnimatedButton.js.map +1 -0
  13. package/lib/commonjs/ui/components/profile/EditBioModal.js +24 -156
  14. package/lib/commonjs/ui/components/profile/EditBioModal.js.map +1 -1
  15. package/lib/commonjs/ui/components/profile/EditDisplayNameModal.js +28 -178
  16. package/lib/commonjs/ui/components/profile/EditDisplayNameModal.js.map +1 -1
  17. package/lib/commonjs/ui/components/profile/EditEmailModal.js +32 -159
  18. package/lib/commonjs/ui/components/profile/EditEmailModal.js.map +1 -1
  19. package/lib/commonjs/ui/components/profile/EditLocationModal.js +45 -227
  20. package/lib/commonjs/ui/components/profile/EditLocationModal.js.map +1 -1
  21. package/lib/commonjs/ui/components/profile/EditUsernameModal.js +30 -155
  22. package/lib/commonjs/ui/components/profile/EditUsernameModal.js.map +1 -1
  23. package/lib/commonjs/ui/hooks/mutations/mutationFactory.js +177 -0
  24. package/lib/commonjs/ui/hooks/mutations/mutationFactory.js.map +1 -0
  25. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +10 -123
  26. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  27. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +2 -32
  28. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
  29. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js +2 -31
  30. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js.map +1 -1
  31. package/lib/commonjs/ui/hooks/useFileFiltering.js +76 -0
  32. package/lib/commonjs/ui/hooks/useFileFiltering.js.map +1 -0
  33. package/lib/commonjs/ui/navigation/bottomSheetManager.js +43 -145
  34. package/lib/commonjs/ui/navigation/bottomSheetManager.js.map +1 -1
  35. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +0 -2
  36. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  37. package/lib/commonjs/ui/screens/FileManagementScreen.js +2 -2
  38. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  39. package/lib/commonjs/ui/utils/authHelpers.js +164 -0
  40. package/lib/commonjs/ui/utils/authHelpers.js.map +1 -0
  41. package/lib/commonjs/ui/utils/avatarUtils.js +18 -61
  42. package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
  43. package/lib/module/crypto/keyManager.js +67 -22
  44. package/lib/module/crypto/keyManager.js.map +1 -1
  45. package/lib/module/index.js +6 -0
  46. package/lib/module/index.js.map +1 -1
  47. package/lib/module/ui/components/BottomSheetRouter.js +102 -284
  48. package/lib/module/ui/components/BottomSheetRouter.js.map +1 -1
  49. package/lib/module/ui/components/GroupedItem.js +0 -3
  50. package/lib/module/ui/components/GroupedItem.js.map +1 -1
  51. package/lib/module/ui/components/OxyProvider.js +14 -19
  52. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  53. package/lib/module/ui/components/fileManagement/AnimatedButton.js +50 -0
  54. package/lib/module/ui/components/fileManagement/AnimatedButton.js.map +1 -0
  55. package/lib/module/ui/components/profile/EditBioModal.js +24 -156
  56. package/lib/module/ui/components/profile/EditBioModal.js.map +1 -1
  57. package/lib/module/ui/components/profile/EditDisplayNameModal.js +28 -178
  58. package/lib/module/ui/components/profile/EditDisplayNameModal.js.map +1 -1
  59. package/lib/module/ui/components/profile/EditEmailModal.js +32 -159
  60. package/lib/module/ui/components/profile/EditEmailModal.js.map +1 -1
  61. package/lib/module/ui/components/profile/EditLocationModal.js +45 -227
  62. package/lib/module/ui/components/profile/EditLocationModal.js.map +1 -1
  63. package/lib/module/ui/components/profile/EditUsernameModal.js +30 -155
  64. package/lib/module/ui/components/profile/EditUsernameModal.js.map +1 -1
  65. package/lib/module/ui/hooks/mutations/mutationFactory.js +173 -0
  66. package/lib/module/ui/hooks/mutations/mutationFactory.js.map +1 -0
  67. package/lib/module/ui/hooks/mutations/useAccountMutations.js +10 -122
  68. package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  69. package/lib/module/ui/hooks/queries/useAccountQueries.js +2 -32
  70. package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
  71. package/lib/module/ui/hooks/queries/useServicesQueries.js +2 -31
  72. package/lib/module/ui/hooks/queries/useServicesQueries.js.map +1 -1
  73. package/lib/module/ui/hooks/useFileFiltering.js +72 -0
  74. package/lib/module/ui/hooks/useFileFiltering.js.map +1 -0
  75. package/lib/module/ui/navigation/bottomSheetManager.js +37 -135
  76. package/lib/module/ui/navigation/bottomSheetManager.js.map +1 -1
  77. package/lib/module/ui/screens/AccountSettingsScreen.js +0 -2
  78. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  79. package/lib/module/ui/screens/FileManagementScreen.js +2 -2
  80. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  81. package/lib/module/ui/utils/authHelpers.js +154 -0
  82. package/lib/module/ui/utils/authHelpers.js.map +1 -0
  83. package/lib/module/ui/utils/avatarUtils.js +18 -61
  84. package/lib/module/ui/utils/avatarUtils.js.map +1 -1
  85. package/lib/typescript/commonjs/crypto/keyManager.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/index.d.ts +6 -0
  87. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/ui/components/BottomSheetRouter.d.ts +2 -7
  89. package/lib/typescript/commonjs/ui/components/BottomSheetRouter.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/ui/components/GroupedItem.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/ui/components/fileManagement/AnimatedButton.d.ts +16 -0
  93. package/lib/typescript/commonjs/ui/components/fileManagement/AnimatedButton.d.ts.map +1 -0
  94. package/lib/typescript/commonjs/ui/components/profile/EditBioModal.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/ui/components/profile/EditDisplayNameModal.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/ui/components/profile/EditEmailModal.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/ui/components/profile/EditLocationModal.d.ts +1 -0
  98. package/lib/typescript/commonjs/ui/components/profile/EditLocationModal.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/ui/components/profile/EditUsernameModal.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/ui/hooks/mutations/mutationFactory.d.ts +76 -0
  101. package/lib/typescript/commonjs/ui/hooks/mutations/mutationFactory.d.ts.map +1 -0
  102. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +29 -4
  103. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts +1 -1
  105. package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/ui/hooks/queries/useServicesQueries.d.ts +1 -1
  107. package/lib/typescript/commonjs/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/ui/hooks/useFileFiltering.d.ts +29 -0
  109. package/lib/typescript/commonjs/ui/hooks/useFileFiltering.d.ts.map +1 -0
  110. package/lib/typescript/commonjs/ui/navigation/bottomSheetManager.d.ts +11 -60
  111. package/lib/typescript/commonjs/ui/navigation/bottomSheetManager.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/ui/utils/authHelpers.d.ts +99 -0
  114. package/lib/typescript/commonjs/ui/utils/authHelpers.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/ui/utils/avatarUtils.d.ts.map +1 -1
  116. package/lib/typescript/module/crypto/keyManager.d.ts.map +1 -1
  117. package/lib/typescript/module/index.d.ts +6 -0
  118. package/lib/typescript/module/index.d.ts.map +1 -1
  119. package/lib/typescript/module/ui/components/BottomSheetRouter.d.ts +2 -7
  120. package/lib/typescript/module/ui/components/BottomSheetRouter.d.ts.map +1 -1
  121. package/lib/typescript/module/ui/components/GroupedItem.d.ts.map +1 -1
  122. package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
  123. package/lib/typescript/module/ui/components/fileManagement/AnimatedButton.d.ts +16 -0
  124. package/lib/typescript/module/ui/components/fileManagement/AnimatedButton.d.ts.map +1 -0
  125. package/lib/typescript/module/ui/components/profile/EditBioModal.d.ts.map +1 -1
  126. package/lib/typescript/module/ui/components/profile/EditDisplayNameModal.d.ts.map +1 -1
  127. package/lib/typescript/module/ui/components/profile/EditEmailModal.d.ts.map +1 -1
  128. package/lib/typescript/module/ui/components/profile/EditLocationModal.d.ts +1 -0
  129. package/lib/typescript/module/ui/components/profile/EditLocationModal.d.ts.map +1 -1
  130. package/lib/typescript/module/ui/components/profile/EditUsernameModal.d.ts.map +1 -1
  131. package/lib/typescript/module/ui/hooks/mutations/mutationFactory.d.ts +76 -0
  132. package/lib/typescript/module/ui/hooks/mutations/mutationFactory.d.ts.map +1 -0
  133. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +29 -4
  134. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  135. package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts +1 -1
  136. package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
  137. package/lib/typescript/module/ui/hooks/queries/useServicesQueries.d.ts +1 -1
  138. package/lib/typescript/module/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
  139. package/lib/typescript/module/ui/hooks/useFileFiltering.d.ts +29 -0
  140. package/lib/typescript/module/ui/hooks/useFileFiltering.d.ts.map +1 -0
  141. package/lib/typescript/module/ui/navigation/bottomSheetManager.d.ts +11 -60
  142. package/lib/typescript/module/ui/navigation/bottomSheetManager.d.ts.map +1 -1
  143. package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  144. package/lib/typescript/module/ui/utils/authHelpers.d.ts +99 -0
  145. package/lib/typescript/module/ui/utils/authHelpers.d.ts.map +1 -0
  146. package/lib/typescript/module/ui/utils/avatarUtils.d.ts.map +1 -1
  147. package/package.json +2 -2
  148. package/src/crypto/keyManager.ts +23 -22
  149. package/src/index.ts +25 -0
  150. package/src/ui/components/BottomSheetRouter.tsx +97 -319
  151. package/src/ui/components/GroupedItem.tsx +0 -4
  152. package/src/ui/components/OxyProvider.tsx +13 -18
  153. package/src/ui/components/fileManagement/AnimatedButton.tsx +56 -0
  154. package/src/ui/components/profile/EditBioModal.tsx +38 -176
  155. package/src/ui/components/profile/EditDisplayNameModal.tsx +48 -195
  156. package/src/ui/components/profile/EditEmailModal.tsx +49 -180
  157. package/src/ui/components/profile/EditLocationModal.tsx +76 -263
  158. package/src/ui/components/profile/EditUsernameModal.tsx +47 -175
  159. package/src/ui/hooks/mutations/mutationFactory.ts +215 -0
  160. package/src/ui/hooks/mutations/useAccountMutations.ts +48 -136
  161. package/src/ui/hooks/queries/useAccountQueries.ts +6 -33
  162. package/src/ui/hooks/queries/useServicesQueries.ts +6 -32
  163. package/src/ui/hooks/useFileFiltering.ts +115 -0
  164. package/src/ui/navigation/bottomSheetManager.ts +43 -150
  165. package/src/ui/screens/AccountSettingsScreen.tsx +0 -2
  166. package/src/ui/screens/FileManagementScreen.tsx +2 -2
  167. package/src/ui/utils/authHelpers.ts +183 -0
  168. package/src/ui/utils/avatarUtils.ts +25 -65
  169. package/lib/commonjs/ui/hooks/use-haptic-press.js +0 -21
  170. package/lib/commonjs/ui/hooks/use-haptic-press.js.map +0 -1
  171. package/lib/module/ui/hooks/use-haptic-press.js +0 -17
  172. package/lib/module/ui/hooks/use-haptic-press.js.map +0 -1
  173. package/lib/typescript/commonjs/ui/hooks/use-haptic-press.d.ts +0 -8
  174. package/lib/typescript/commonjs/ui/hooks/use-haptic-press.d.ts.map +0 -1
  175. package/lib/typescript/module/ui/hooks/use-haptic-press.d.ts +0 -8
  176. package/lib/typescript/module/ui/hooks/use-haptic-press.d.ts.map +0 -1
  177. package/src/ui/hooks/use-haptic-press.ts +0 -15
@@ -1,8 +1,8 @@
1
1
  import { useQuery, useQueries } from '@tanstack/react-query';
2
2
  import type { User } from '../../../models/interfaces';
3
- import type { OxyServices } from '../../../core';
4
3
  import { queryKeys } from './queryKeys';
5
4
  import { useOxy } from '../../context/OxyContext';
5
+ import { authenticatedApiCall } from '../../utils/authHelpers';
6
6
 
7
7
  /**
8
8
  * Get user profile by session ID
@@ -138,38 +138,11 @@ export const usePrivacySettings = (userId?: string, options?: { enabled?: boolea
138
138
  throw new Error('User ID is required');
139
139
  }
140
140
 
141
- // Ensure we have a valid token before making the request
142
- if (!oxyServices.hasValidToken() && activeSessionId) {
143
- try {
144
- // Try to get token for the session
145
- await oxyServices.getTokenBySession(activeSessionId);
146
- } catch (tokenError) {
147
- // If getting token fails, might be an offline session - try syncing
148
- const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
149
- if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
150
- // Session sync should be handled by the app layer (e.g., accounts app's useIdentity hook)
151
- throw new Error('Session needs to be synced. Please try again.');
152
- } else {
153
- throw tokenError;
154
- }
155
- }
156
- }
157
-
158
- try {
159
- return await oxyServices.getPrivacySettings(targetUserId);
160
- } catch (error: any) {
161
- const errorMessage = error?.message || '';
162
- const status = error?.status || error?.response?.status;
163
-
164
- // Handle authentication errors
165
- if (status === 401 || errorMessage.includes('Authentication required') || errorMessage.includes('Invalid or missing authorization header')) {
166
- // Session sync should be handled by the app layer
167
- throw new Error('Authentication failed. Please sign in again.');
168
- }
169
-
170
- // TanStack Query will automatically retry on network errors
171
- throw error;
172
- }
141
+ return authenticatedApiCall(
142
+ oxyServices,
143
+ activeSessionId,
144
+ () => oxyServices.getPrivacySettings(targetUserId)
145
+ );
173
146
  },
174
147
  enabled: (options?.enabled !== false) && !!targetUserId,
175
148
  staleTime: 2 * 60 * 1000, // 2 minutes
@@ -3,6 +3,7 @@ import type { ClientSession } from '../../../models/session';
3
3
  import { queryKeys } from './queryKeys';
4
4
  import { useOxy } from '../../context/OxyContext';
5
5
  import { fetchSessionsWithFallback, mapSessionsToClient } from '../../utils/sessionHelpers';
6
+ import { authenticatedApiCall } from '../../utils/authHelpers';
6
7
 
7
8
  /**
8
9
  * Get all active sessions for the current user
@@ -94,38 +95,11 @@ export const useUserDevices = (options?: { enabled?: boolean }) => {
94
95
  return useQuery({
95
96
  queryKey: queryKeys.devices.list(),
96
97
  queryFn: async () => {
97
- // Ensure we have a valid token before making the request
98
- if (!oxyServices.hasValidToken() && activeSessionId) {
99
- try {
100
- // Try to get token for the session
101
- await oxyServices.getTokenBySession(activeSessionId);
102
- } catch (tokenError) {
103
- // If getting token fails, might be an offline session - try syncing
104
- const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
105
- if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
106
- // Session sync should be handled by the app layer
107
- throw new Error('Session needs to be synced. Please try again.');
108
- } else {
109
- throw tokenError;
110
- }
111
- }
112
- }
113
-
114
- try {
115
- return await oxyServices.getUserDevices();
116
- } catch (error: any) {
117
- const errorMessage = error?.message || '';
118
- const status = error?.status || error?.response?.status;
119
-
120
- // Handle authentication errors
121
- if (status === 401 || errorMessage.includes('Authentication required') || errorMessage.includes('Invalid or missing authorization header')) {
122
- // Session sync should be handled by the app layer
123
- throw new Error('Authentication failed. Please sign in again.');
124
- }
125
-
126
- // TanStack Query will automatically retry on network errors
127
- throw error;
128
- }
98
+ return authenticatedApiCall(
99
+ oxyServices,
100
+ activeSessionId,
101
+ () => oxyServices.getUserDevices()
102
+ );
129
103
  },
130
104
  enabled: (options?.enabled !== false) && isAuthenticated,
131
105
  staleTime: 5 * 60 * 1000,
@@ -0,0 +1,115 @@
1
+ import { useMemo, useState, useCallback } from 'react';
2
+ import type { FileMetadata } from '../../models/interfaces';
3
+
4
+ export type ViewMode = 'all' | 'photos' | 'videos' | 'documents' | 'audio';
5
+ export type SortBy = 'date' | 'size' | 'name' | 'type';
6
+ export type SortOrder = 'asc' | 'desc';
7
+
8
+ interface UseFileFilteringOptions {
9
+ files: FileMetadata[];
10
+ initialViewMode?: ViewMode;
11
+ initialSortBy?: SortBy;
12
+ initialSortOrder?: SortOrder;
13
+ }
14
+
15
+ interface UseFileFilteringReturn {
16
+ filteredFiles: FileMetadata[];
17
+ viewMode: ViewMode;
18
+ setViewMode: (mode: ViewMode) => void;
19
+ searchQuery: string;
20
+ setSearchQuery: (query: string) => void;
21
+ sortBy: SortBy;
22
+ setSortBy: (sort: SortBy) => void;
23
+ sortOrder: SortOrder;
24
+ setSortOrder: (order: SortOrder) => void;
25
+ toggleSortOrder: () => void;
26
+ }
27
+
28
+ /**
29
+ * Hook for file filtering, sorting, and search functionality
30
+ * Extracts common file management logic for reuse across components
31
+ */
32
+ export function useFileFiltering({
33
+ files,
34
+ initialViewMode = 'all',
35
+ initialSortBy = 'date',
36
+ initialSortOrder = 'desc',
37
+ }: UseFileFilteringOptions): UseFileFilteringReturn {
38
+ const [viewMode, setViewMode] = useState<ViewMode>(initialViewMode);
39
+ const [searchQuery, setSearchQuery] = useState('');
40
+ const [sortBy, setSortBy] = useState<SortBy>(initialSortBy);
41
+ const [sortOrder, setSortOrder] = useState<SortOrder>(initialSortOrder);
42
+
43
+ const toggleSortOrder = useCallback(() => {
44
+ setSortOrder((prev) => (prev === 'asc' ? 'desc' : 'asc'));
45
+ }, []);
46
+
47
+ const filteredFiles = useMemo(() => {
48
+ // Filter by view mode
49
+ let filteredByMode = files;
50
+ if (viewMode === 'photos') {
51
+ filteredByMode = files.filter((file) => file.contentType.startsWith('image/'));
52
+ } else if (viewMode === 'videos') {
53
+ filteredByMode = files.filter((file) => file.contentType.startsWith('video/'));
54
+ } else if (viewMode === 'documents') {
55
+ filteredByMode = files.filter(
56
+ (file) =>
57
+ file.contentType.includes('pdf') ||
58
+ file.contentType.includes('document') ||
59
+ file.contentType.includes('text') ||
60
+ file.contentType.includes('msword') ||
61
+ file.contentType.includes('excel') ||
62
+ file.contentType.includes('spreadsheet') ||
63
+ file.contentType.includes('presentation') ||
64
+ file.contentType.includes('powerpoint')
65
+ );
66
+ } else if (viewMode === 'audio') {
67
+ filteredByMode = files.filter((file) => file.contentType.startsWith('audio/'));
68
+ }
69
+
70
+ // Filter by search query
71
+ let filtered = filteredByMode;
72
+ if (searchQuery.trim()) {
73
+ const query = searchQuery.toLowerCase();
74
+ filtered = filteredByMode.filter(
75
+ (file) =>
76
+ file.filename.toLowerCase().includes(query) ||
77
+ file.contentType.toLowerCase().includes(query) ||
78
+ (file.metadata?.description &&
79
+ file.metadata.description.toLowerCase().includes(query))
80
+ );
81
+ }
82
+
83
+ // Sort files
84
+ const sorted = [...filtered].sort((a, b) => {
85
+ let comparison = 0;
86
+ if (sortBy === 'date') {
87
+ const dateA = new Date(a.uploadDate || 0).getTime();
88
+ const dateB = new Date(b.uploadDate || 0).getTime();
89
+ comparison = dateA - dateB;
90
+ } else if (sortBy === 'size') {
91
+ comparison = (a.length || 0) - (b.length || 0);
92
+ } else if (sortBy === 'name') {
93
+ comparison = (a.filename || '').localeCompare(b.filename || '');
94
+ } else if (sortBy === 'type') {
95
+ comparison = (a.contentType || '').localeCompare(b.contentType || '');
96
+ }
97
+ return sortOrder === 'asc' ? comparison : -comparison;
98
+ });
99
+
100
+ return sorted;
101
+ }, [files, searchQuery, viewMode, sortBy, sortOrder]);
102
+
103
+ return {
104
+ filteredFiles,
105
+ viewMode,
106
+ setViewMode,
107
+ searchQuery,
108
+ setSearchQuery,
109
+ sortBy,
110
+ setSortBy,
111
+ sortOrder,
112
+ setSortOrder,
113
+ toggleSortOrder,
114
+ };
115
+ }
@@ -3,189 +3,82 @@ import { isValidRoute } from './routes';
3
3
  import { createStore } from 'zustand/vanilla';
4
4
 
5
5
  /**
6
- * Bottom Sheet Manager - Pure state management module
7
- *
8
- * Uses Zustand (vanilla) for robust state management without React dependencies.
9
- * This ensures the manager can be imported safely anywhere.
6
+ * Bottom Sheet State Manager
10
7
  */
11
8
 
12
- export interface NavigationHistoryEntry {
13
- screen: RouteName;
14
- props: Record<string, unknown>;
15
- step?: number; // For step-based screens
16
- }
17
-
18
- export interface BottomSheetRouterState {
9
+ export interface BottomSheetState {
19
10
  currentScreen: RouteName | null;
20
11
  screenProps: Record<string, unknown>;
21
- currentStep?: number; // Current step in step-based screen
22
- navigationHistory: NavigationHistoryEntry[];
12
+ currentStep?: number;
13
+ history: Array<{ screen: RouteName; props: Record<string, unknown>; step?: number }>;
23
14
  isOpen: boolean;
24
15
  }
25
16
 
26
- // Initial state
27
- const initialState: BottomSheetRouterState = {
17
+ const initialState: BottomSheetState = {
28
18
  currentScreen: null,
29
19
  screenProps: {},
30
20
  currentStep: undefined,
31
- navigationHistory: [],
21
+ history: [],
32
22
  isOpen: false,
33
23
  };
34
24
 
35
- // Create vanilla store
36
- export const bottomSheetStore = createStore<BottomSheetRouterState>(() => initialState);
25
+ export const bottomSheetStore = createStore<BottomSheetState>(() => initialState);
37
26
 
38
- // Use a generic ref type to avoid importing React types
39
- type BottomSheetRefObject = { current: { present: () => void; dismiss: () => void } | null } | null;
40
- let bottomSheetRef: BottomSheetRefObject = null;
41
-
42
- /**
43
- * Set the bottom sheet ref so showBottomSheet can control it
44
- */
45
- export const setBottomSheetRef = (ref: BottomSheetRefObject) => {
46
- bottomSheetRef = ref;
47
- };
27
+ export const getState = () => bottomSheetStore.getState();
48
28
 
49
- /**
50
- * Update the bottom sheet state
51
- * (Kept for backward compatibility, but prefer using store directly if possible)
52
- */
53
- export const updateBottomSheetState = (updates: Partial<BottomSheetRouterState>) => {
54
- bottomSheetStore.setState((state) => ({ ...state, ...updates }));
55
- };
29
+ export const showBottomSheet = (
30
+ screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> },
31
+ ): void => {
32
+ const screen = typeof screenOrConfig === 'string' ? screenOrConfig : screenOrConfig.screen;
33
+ const props = typeof screenOrConfig === 'string' ? {} : (screenOrConfig.props || {});
56
34
 
57
- /**
58
- * Subscribe to bottom sheet state changes
59
- * (Wrapper around store.subscribe for backward compatibility)
60
- */
61
- export const subscribeToBottomSheetState = (listener: (state: BottomSheetRouterState) => void) => {
62
- return bottomSheetStore.subscribe(listener);
63
- };
35
+ if (!isValidRoute(screen)) {
36
+ if (__DEV__) console.warn(`[BottomSheet] Invalid route: ${screen}`);
37
+ return;
38
+ }
64
39
 
65
- /**
66
- * Get the current bottom sheet state
67
- * (Wrapper around store.getState for backward compatibility)
68
- */
69
- export const getBottomSheetState = (): BottomSheetRouterState => {
70
- return bottomSheetStore.getState();
71
- };
40
+ const state = bottomSheetStore.getState();
72
41
 
73
- /**
74
- * Show the bottom sheet with a specific screen (internal - no route validation)
75
- */
76
- export const managerShowBottomSheet = (
77
- screen: RouteName,
78
- props?: Record<string, unknown>,
79
- options?: { addToHistory?: boolean; step?: number },
80
- ): void => {
81
- const currentState = bottomSheetStore.getState();
82
- const addToHistory = options?.addToHistory !== false; // Default to true
83
-
84
- // If adding to history and there's a current screen, push it to history
85
- if (addToHistory && currentState.currentScreen) {
86
- const historyEntry: NavigationHistoryEntry = {
87
- screen: currentState.currentScreen,
88
- props: { ...currentState.screenProps },
89
- step: currentState.currentStep,
90
- };
91
-
92
- // We need to create a new array for immutability
93
- const newHistory = [...currentState.navigationHistory, historyEntry];
94
- bottomSheetStore.setState({ navigationHistory: newHistory });
42
+ // Push current screen to history if navigating to different screen
43
+ if (state.currentScreen && state.currentScreen !== screen) {
44
+ bottomSheetStore.setState({
45
+ history: [...state.history, {
46
+ screen: state.currentScreen,
47
+ props: state.screenProps,
48
+ step: state.currentStep,
49
+ }],
50
+ });
95
51
  }
96
-
97
- // Determine the new step
98
- const newStep = options?.step ??
99
- (props?.initialStep !== undefined ? props.initialStep :
100
- (addToHistory ? undefined : currentState.currentStep));
101
-
52
+
102
53
  bottomSheetStore.setState({
103
54
  currentScreen: screen,
104
- screenProps: props || {},
105
- currentStep: newStep !== null && newStep !== undefined ? (newStep as number) : undefined,
55
+ screenProps: props,
56
+ currentStep: typeof props.initialStep === 'number' ? props.initialStep : undefined,
106
57
  isOpen: true,
107
58
  });
108
-
109
- // Present the sheet after state update
110
- if (bottomSheetRef?.current) {
111
- bottomSheetRef.current.present();
112
- }
113
59
  };
114
60
 
115
- /**
116
- * Close the bottom sheet (internal)
117
- */
118
- export const managerCloseBottomSheet = (): void => {
119
- bottomSheetStore.setState({
120
- currentScreen: null,
121
- screenProps: {},
122
- currentStep: undefined,
123
- navigationHistory: [],
124
- isOpen: false,
125
- });
126
-
127
- if (bottomSheetRef?.current) {
128
- bottomSheetRef.current.dismiss();
129
- }
61
+ export const closeBottomSheet = (): void => {
62
+ bottomSheetStore.setState(initialState);
130
63
  };
131
64
 
132
- /**
133
- * Go back in navigation history
134
- * Returns true if back navigation was successful, false if history is empty
135
- */
136
- export const managerGoBack = (): boolean => {
137
- const currentState = bottomSheetStore.getState();
65
+ export const goBack = (): boolean => {
66
+ const { history } = bottomSheetStore.getState();
138
67
 
139
- // If there's history, pop and navigate to previous screen
140
- if (currentState.navigationHistory.length > 0) {
141
- const previous = currentState.navigationHistory[currentState.navigationHistory.length - 1];
142
- const newHistory = currentState.navigationHistory.slice(0, -1);
143
-
68
+ if (history.length > 0) {
69
+ const prev = history[history.length - 1];
144
70
  bottomSheetStore.setState({
145
- currentScreen: previous.screen,
146
- screenProps: previous.props,
147
- currentStep: previous.step,
148
- navigationHistory: newHistory,
149
- isOpen: true,
71
+ currentScreen: prev.screen,
72
+ screenProps: prev.props,
73
+ currentStep: prev.step,
74
+ history: history.slice(0, -1),
150
75
  });
151
-
152
76
  return true;
153
77
  }
154
-
155
- return false;
156
- };
157
-
158
- /**
159
- * Public API for showing bottom sheets
160
- */
161
- export const showBottomSheet = (
162
- screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> },
163
- ): void => {
164
- let screen: RouteName;
165
- let props: Record<string, unknown> = {};
166
-
167
- if (typeof screenOrConfig === 'string') {
168
- screen = screenOrConfig;
169
- } else {
170
- screen = screenOrConfig.screen;
171
- props = screenOrConfig.props || {};
172
- }
173
-
174
- if (!isValidRoute(screen)) {
175
- if (__DEV__) {
176
- console.warn(`[BottomSheetAPI] Invalid route: ${screen}`);
177
- }
178
- return;
179
- }
180
78
 
181
- managerShowBottomSheet(screen, props);
79
+ return false;
182
80
  };
183
81
 
184
- /**
185
- * Public API for closing bottom sheets
186
- */
187
- export const closeBottomSheet = (): void => {
188
- managerCloseBottomSheet();
82
+ export const updateState = (updates: Partial<BottomSheetState>) => {
83
+ bottomSheetStore.setState((state) => ({ ...state, ...updates }));
189
84
  };
190
-
191
-
@@ -29,7 +29,6 @@ import { useThemeStyles } from '../hooks/useThemeStyles';
29
29
  import { useColorScheme } from '../hooks/use-color-scheme';
30
30
  import { Colors } from '../constants/theme';
31
31
  import { normalizeColorScheme, normalizeTheme } from '../utils/themeUtils';
32
- import { useHapticPress } from '../hooks/use-haptic-press';
33
32
  import { EditDisplayNameModal } from '../components/profile/EditDisplayNameModal';
34
33
  import { EditUsernameModal } from '../components/profile/EditUsernameModal';
35
34
  import { EditEmailModal } from '../components/profile/EditEmailModal';
@@ -176,7 +175,6 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string;
176
175
  // Get theme colors using centralized hook
177
176
  const colorScheme = useColorScheme();
178
177
  const themeStyles = useThemeStyles(theme || 'light', colorScheme);
179
- const handlePressIn = useHapticPress();
180
178
 
181
179
  // Extract colors for convenience - ensure it's always defined
182
180
  // useThemeStyles always returns colors, but add safety check for edge cases
@@ -549,8 +549,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
549
549
  });
550
550
 
551
551
  // Attempt to refresh file list incrementally – fetch single file metadata if API allows
552
- if (result?.file || result?.files?.[0]) {
553
- const f = result.file || result.files[0];
552
+ const f = result?.file ?? result?.files?.[0];
553
+ if (f) {
554
554
  const merged: FileMetadata = {
555
555
  id: f.id,
556
556
  filename: f.originalName || f.sha256 || raw.name,
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Authentication helper utilities to reduce code duplication across hooks and utilities.
3
+ * These functions handle common token validation and authentication error patterns.
4
+ */
5
+
6
+ import type { OxyServices } from '../../core';
7
+
8
+ /**
9
+ * Error thrown when session sync is required
10
+ */
11
+ export class SessionSyncRequiredError extends Error {
12
+ constructor(message = 'Session needs to be synced. Please try again.') {
13
+ super(message);
14
+ this.name = 'SessionSyncRequiredError';
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Error thrown when authentication fails
20
+ */
21
+ export class AuthenticationFailedError extends Error {
22
+ constructor(message = 'Authentication failed. Please sign in again.') {
23
+ super(message);
24
+ this.name = 'AuthenticationFailedError';
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Ensures a valid token exists before making authenticated API calls.
30
+ * If no valid token exists and an active session ID is available,
31
+ * attempts to refresh the token using the session.
32
+ *
33
+ * @param oxyServices - The OxyServices instance
34
+ * @param activeSessionId - The active session ID (if available)
35
+ * @throws {SessionSyncRequiredError} If the session needs to be synced (offline session)
36
+ * @throws {Error} If token refresh fails for other reasons
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * // In a mutation or query function:
41
+ * await ensureValidToken(oxyServices, activeSessionId);
42
+ * return await oxyServices.updateProfile(updates);
43
+ * ```
44
+ */
45
+ export async function ensureValidToken(
46
+ oxyServices: OxyServices,
47
+ activeSessionId: string | null | undefined
48
+ ): Promise<void> {
49
+ if (oxyServices.hasValidToken() || !activeSessionId) {
50
+ return;
51
+ }
52
+
53
+ try {
54
+ await oxyServices.getTokenBySession(activeSessionId);
55
+ } catch (tokenError) {
56
+ const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
57
+
58
+ if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
59
+ throw new SessionSyncRequiredError();
60
+ }
61
+
62
+ throw tokenError;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Options for handling API authentication errors
68
+ */
69
+ export interface HandleApiErrorOptions {
70
+ /** Optional callback to attempt session sync and retry */
71
+ syncSession?: () => Promise<unknown>;
72
+ /** The active session ID for retry attempts */
73
+ activeSessionId?: string | null;
74
+ /** The OxyServices instance for retry attempts */
75
+ oxyServices?: OxyServices;
76
+ }
77
+
78
+ /**
79
+ * Checks if an error is an authentication error (401 or auth-related message)
80
+ *
81
+ * @param error - The error to check
82
+ * @returns True if the error is an authentication error
83
+ */
84
+ export function isAuthenticationError(error: unknown): boolean {
85
+ if (!error || typeof error !== 'object') {
86
+ return false;
87
+ }
88
+
89
+ const errorObj = error as { message?: string; status?: number; response?: { status?: number } };
90
+ const errorMessage = errorObj.message || '';
91
+ const status = errorObj.status || errorObj.response?.status;
92
+
93
+ return (
94
+ status === 401 ||
95
+ errorMessage.includes('Authentication required') ||
96
+ errorMessage.includes('Invalid or missing authorization header')
97
+ );
98
+ }
99
+
100
+ /**
101
+ * Wraps an API call with authentication error handling.
102
+ * If an authentication error occurs, it can optionally attempt to sync the session and retry.
103
+ *
104
+ * @param apiCall - The API call function to execute
105
+ * @param options - Optional error handling configuration
106
+ * @returns The result of the API call
107
+ * @throws {AuthenticationFailedError} If authentication fails and cannot be recovered
108
+ * @throws {Error} If the API call fails for non-auth reasons
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * // Simple usage:
113
+ * const result = await withAuthErrorHandling(
114
+ * () => oxyServices.updateProfile(updates)
115
+ * );
116
+ *
117
+ * // With retry on auth failure:
118
+ * const result = await withAuthErrorHandling(
119
+ * () => oxyServices.updateProfile(updates),
120
+ * { syncSession, activeSessionId, oxyServices }
121
+ * );
122
+ * ```
123
+ */
124
+ export async function withAuthErrorHandling<T>(
125
+ apiCall: () => Promise<T>,
126
+ options?: HandleApiErrorOptions
127
+ ): Promise<T> {
128
+ try {
129
+ return await apiCall();
130
+ } catch (error) {
131
+ if (!isAuthenticationError(error)) {
132
+ throw error;
133
+ }
134
+
135
+ // If we have sync capabilities, try to recover
136
+ if (options?.syncSession && options?.activeSessionId && options?.oxyServices) {
137
+ try {
138
+ await options.syncSession();
139
+ await options.oxyServices.getTokenBySession(options.activeSessionId);
140
+ // Retry the API call after refreshing token
141
+ return await apiCall();
142
+ } catch {
143
+ throw new AuthenticationFailedError();
144
+ }
145
+ }
146
+
147
+ throw new AuthenticationFailedError();
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Combines token validation and auth error handling for a complete authenticated API call.
153
+ * This is the recommended helper for most authenticated API operations.
154
+ *
155
+ * @param oxyServices - The OxyServices instance
156
+ * @param activeSessionId - The active session ID
157
+ * @param apiCall - The API call function to execute
158
+ * @param syncSession - Optional callback to sync session on auth failure
159
+ * @returns The result of the API call
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * return await authenticatedApiCall(
164
+ * oxyServices,
165
+ * activeSessionId,
166
+ * () => oxyServices.updateProfile(updates)
167
+ * );
168
+ * ```
169
+ */
170
+ export async function authenticatedApiCall<T>(
171
+ oxyServices: OxyServices,
172
+ activeSessionId: string | null | undefined,
173
+ apiCall: () => Promise<T>,
174
+ syncSession?: () => Promise<unknown>
175
+ ): Promise<T> {
176
+ await ensureValidToken(oxyServices, activeSessionId);
177
+
178
+ return withAuthErrorHandling(apiCall, {
179
+ syncSession,
180
+ activeSessionId,
181
+ oxyServices,
182
+ });
183
+ }