@oxyhq/auth 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +56 -0
  2. package/dist/cjs/WebOxyProvider.js +287 -0
  3. package/dist/cjs/hooks/mutations/index.js +23 -0
  4. package/dist/cjs/hooks/mutations/mutationFactory.js +126 -0
  5. package/dist/cjs/hooks/mutations/useAccountMutations.js +275 -0
  6. package/dist/cjs/hooks/mutations/useServicesMutations.js +149 -0
  7. package/dist/cjs/hooks/queries/index.js +35 -0
  8. package/dist/cjs/hooks/queries/queryKeys.js +82 -0
  9. package/dist/cjs/hooks/queries/useAccountQueries.js +141 -0
  10. package/dist/cjs/hooks/queries/useSecurityQueries.js +45 -0
  11. package/dist/cjs/hooks/queries/useServicesQueries.js +113 -0
  12. package/dist/cjs/hooks/queryClient.js +110 -0
  13. package/dist/cjs/hooks/useAssets.js +225 -0
  14. package/dist/cjs/hooks/useFileDownloadUrl.js +91 -0
  15. package/dist/cjs/hooks/useFileFiltering.js +81 -0
  16. package/dist/cjs/hooks/useFollow.js +159 -0
  17. package/dist/cjs/hooks/useFollow.types.js +4 -0
  18. package/dist/cjs/hooks/useQueryClient.js +16 -0
  19. package/dist/cjs/hooks/useSessionSocket.js +215 -0
  20. package/dist/cjs/hooks/useWebSSO.js +146 -0
  21. package/dist/cjs/index.js +115 -0
  22. package/dist/cjs/stores/accountStore.js +226 -0
  23. package/dist/cjs/stores/assetStore.js +192 -0
  24. package/dist/cjs/stores/authStore.js +47 -0
  25. package/dist/cjs/stores/followStore.js +154 -0
  26. package/dist/cjs/utils/authHelpers.js +154 -0
  27. package/dist/cjs/utils/avatarUtils.js +77 -0
  28. package/dist/cjs/utils/errorHandlers.js +128 -0
  29. package/dist/cjs/utils/sessionHelpers.js +90 -0
  30. package/dist/cjs/utils/storageHelpers.js +147 -0
  31. package/dist/esm/WebOxyProvider.js +282 -0
  32. package/dist/esm/hooks/mutations/index.js +10 -0
  33. package/dist/esm/hooks/mutations/mutationFactory.js +122 -0
  34. package/dist/esm/hooks/mutations/useAccountMutations.js +267 -0
  35. package/dist/esm/hooks/mutations/useServicesMutations.js +141 -0
  36. package/dist/esm/hooks/queries/index.js +14 -0
  37. package/dist/esm/hooks/queries/queryKeys.js +76 -0
  38. package/dist/esm/hooks/queries/useAccountQueries.js +131 -0
  39. package/dist/esm/hooks/queries/useSecurityQueries.js +40 -0
  40. package/dist/esm/hooks/queries/useServicesQueries.js +105 -0
  41. package/dist/esm/hooks/queryClient.js +104 -0
  42. package/dist/esm/hooks/useAssets.js +220 -0
  43. package/dist/esm/hooks/useFileDownloadUrl.js +86 -0
  44. package/dist/esm/hooks/useFileFiltering.js +78 -0
  45. package/dist/esm/hooks/useFollow.js +154 -0
  46. package/dist/esm/hooks/useFollow.types.js +3 -0
  47. package/dist/esm/hooks/useQueryClient.js +12 -0
  48. package/dist/esm/hooks/useSessionSocket.js +209 -0
  49. package/dist/esm/hooks/useWebSSO.js +143 -0
  50. package/dist/esm/index.js +48 -0
  51. package/dist/esm/stores/accountStore.js +219 -0
  52. package/dist/esm/stores/assetStore.js +180 -0
  53. package/dist/esm/stores/authStore.js +44 -0
  54. package/dist/esm/stores/followStore.js +151 -0
  55. package/dist/esm/utils/authHelpers.js +145 -0
  56. package/dist/esm/utils/avatarUtils.js +72 -0
  57. package/dist/esm/utils/errorHandlers.js +121 -0
  58. package/dist/esm/utils/sessionHelpers.js +84 -0
  59. package/dist/esm/utils/storageHelpers.js +108 -0
  60. package/dist/types/WebOxyProvider.d.ts +97 -0
  61. package/dist/types/hooks/mutations/index.d.ts +8 -0
  62. package/dist/types/hooks/mutations/mutationFactory.d.ts +75 -0
  63. package/dist/types/hooks/mutations/useAccountMutations.d.ts +68 -0
  64. package/dist/types/hooks/mutations/useServicesMutations.d.ts +22 -0
  65. package/dist/types/hooks/queries/index.d.ts +10 -0
  66. package/dist/types/hooks/queries/queryKeys.d.ts +64 -0
  67. package/dist/types/hooks/queries/useAccountQueries.d.ts +42 -0
  68. package/dist/types/hooks/queries/useSecurityQueries.d.ts +14 -0
  69. package/dist/types/hooks/queries/useServicesQueries.d.ts +31 -0
  70. package/dist/types/hooks/queryClient.d.ts +18 -0
  71. package/dist/types/hooks/useAssets.d.ts +34 -0
  72. package/dist/types/hooks/useFileDownloadUrl.d.ts +18 -0
  73. package/dist/types/hooks/useFileFiltering.d.ts +28 -0
  74. package/dist/types/hooks/useFollow.d.ts +61 -0
  75. package/dist/types/hooks/useFollow.types.d.ts +32 -0
  76. package/dist/types/hooks/useQueryClient.d.ts +6 -0
  77. package/dist/types/hooks/useSessionSocket.d.ts +13 -0
  78. package/dist/types/hooks/useWebSSO.d.ts +57 -0
  79. package/dist/types/index.d.ts +46 -0
  80. package/dist/types/stores/accountStore.d.ts +33 -0
  81. package/dist/types/stores/assetStore.d.ts +53 -0
  82. package/dist/types/stores/authStore.d.ts +16 -0
  83. package/dist/types/stores/followStore.d.ts +24 -0
  84. package/dist/types/utils/authHelpers.d.ts +98 -0
  85. package/dist/types/utils/avatarUtils.d.ts +33 -0
  86. package/dist/types/utils/errorHandlers.d.ts +34 -0
  87. package/dist/types/utils/sessionHelpers.d.ts +63 -0
  88. package/dist/types/utils/storageHelpers.d.ts +27 -0
  89. package/package.json +71 -0
  90. package/src/WebOxyProvider.tsx +372 -0
  91. package/src/global.d.ts +1 -0
  92. package/src/hooks/mutations/index.ts +25 -0
  93. package/src/hooks/mutations/mutationFactory.ts +215 -0
  94. package/src/hooks/mutations/useAccountMutations.ts +344 -0
  95. package/src/hooks/mutations/useServicesMutations.ts +164 -0
  96. package/src/hooks/queries/index.ts +36 -0
  97. package/src/hooks/queries/queryKeys.ts +88 -0
  98. package/src/hooks/queries/useAccountQueries.ts +152 -0
  99. package/src/hooks/queries/useSecurityQueries.ts +64 -0
  100. package/src/hooks/queries/useServicesQueries.ts +126 -0
  101. package/src/hooks/queryClient.ts +112 -0
  102. package/src/hooks/useAssets.ts +291 -0
  103. package/src/hooks/useFileDownloadUrl.ts +118 -0
  104. package/src/hooks/useFileFiltering.ts +115 -0
  105. package/src/hooks/useFollow.ts +175 -0
  106. package/src/hooks/useFollow.types.ts +33 -0
  107. package/src/hooks/useQueryClient.ts +17 -0
  108. package/src/hooks/useSessionSocket.ts +233 -0
  109. package/src/hooks/useWebSSO.ts +187 -0
  110. package/src/index.ts +144 -0
  111. package/src/stores/accountStore.ts +296 -0
  112. package/src/stores/assetStore.ts +281 -0
  113. package/src/stores/authStore.ts +63 -0
  114. package/src/stores/followStore.ts +181 -0
  115. package/src/utils/authHelpers.ts +183 -0
  116. package/src/utils/avatarUtils.ts +103 -0
  117. package/src/utils/errorHandlers.ts +194 -0
  118. package/src/utils/sessionHelpers.ts +151 -0
  119. package/src/utils/storageHelpers.ts +130 -0
@@ -0,0 +1,105 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { queryKeys } from './queryKeys';
3
+ import { useWebOxy } from '../../WebOxyProvider';
4
+ import { fetchSessionsWithFallback, mapSessionsToClient } from '../../utils/sessionHelpers';
5
+ import { authenticatedApiCall } from '../../utils/authHelpers';
6
+ /**
7
+ * Get all active sessions for the current user
8
+ */
9
+ export const useSessions = (userId, options) => {
10
+ const { oxyServices, activeSessionId } = useWebOxy();
11
+ return useQuery({
12
+ queryKey: queryKeys.sessions.list(userId),
13
+ queryFn: async () => {
14
+ if (!activeSessionId) {
15
+ throw new Error('No active session');
16
+ }
17
+ const sessions = await fetchSessionsWithFallback(oxyServices, activeSessionId, {
18
+ fallbackDeviceId: undefined,
19
+ fallbackUserId: userId,
20
+ });
21
+ return mapSessionsToClient(sessions, activeSessionId);
22
+ },
23
+ enabled: (options?.enabled !== false) && !!activeSessionId,
24
+ staleTime: 2 * 60 * 1000, // 2 minutes (sessions change frequently)
25
+ gcTime: 10 * 60 * 1000, // 10 minutes
26
+ });
27
+ };
28
+ /**
29
+ * Get specific session by ID
30
+ */
31
+ export const useSession = (sessionId, options) => {
32
+ const { oxyServices } = useWebOxy();
33
+ return useQuery({
34
+ queryKey: queryKeys.sessions.detail(sessionId || ''),
35
+ queryFn: async () => {
36
+ if (!sessionId) {
37
+ throw new Error('Session ID is required');
38
+ }
39
+ const validation = await oxyServices.validateSession(sessionId, { useHeaderValidation: true });
40
+ if (!validation?.valid || !validation.user) {
41
+ throw new Error('Session not found or invalid');
42
+ }
43
+ const now = new Date();
44
+ return {
45
+ sessionId,
46
+ deviceId: '', // Device ID not available from validation response
47
+ expiresAt: validation.expiresAt || new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
48
+ lastActive: validation.lastActivity || now.toISOString(),
49
+ userId: validation.user.id?.toString() ?? '',
50
+ isCurrent: false,
51
+ };
52
+ },
53
+ enabled: (options?.enabled !== false) && !!sessionId,
54
+ staleTime: 2 * 60 * 1000,
55
+ gcTime: 10 * 60 * 1000,
56
+ });
57
+ };
58
+ /**
59
+ * Get device sessions for the current active session
60
+ */
61
+ export const useDeviceSessions = (options) => {
62
+ const { oxyServices, activeSessionId } = useWebOxy();
63
+ return useQuery({
64
+ queryKey: queryKeys.sessions.active(),
65
+ queryFn: async () => {
66
+ if (!activeSessionId) {
67
+ throw new Error('No active session');
68
+ }
69
+ return await oxyServices.getDeviceSessions(activeSessionId);
70
+ },
71
+ enabled: (options?.enabled !== false) && !!activeSessionId,
72
+ staleTime: 2 * 60 * 1000,
73
+ gcTime: 10 * 60 * 1000,
74
+ });
75
+ };
76
+ /**
77
+ * Get user devices
78
+ */
79
+ export const useUserDevices = (options) => {
80
+ const { oxyServices, isAuthenticated, activeSessionId } = useWebOxy();
81
+ return useQuery({
82
+ queryKey: queryKeys.devices.list(),
83
+ queryFn: async () => {
84
+ return authenticatedApiCall(oxyServices, activeSessionId, () => oxyServices.getUserDevices());
85
+ },
86
+ enabled: (options?.enabled !== false) && isAuthenticated,
87
+ staleTime: 5 * 60 * 1000,
88
+ gcTime: 30 * 60 * 1000,
89
+ });
90
+ };
91
+ /**
92
+ * Get security information
93
+ */
94
+ export const useSecurityInfo = (options) => {
95
+ const { oxyServices, isAuthenticated } = useWebOxy();
96
+ return useQuery({
97
+ queryKey: [...queryKeys.devices.all, 'security'],
98
+ queryFn: async () => {
99
+ return await oxyServices.getSecurityInfo();
100
+ },
101
+ enabled: (options?.enabled !== false) && isAuthenticated,
102
+ staleTime: 5 * 60 * 1000,
103
+ gcTime: 30 * 60 * 1000,
104
+ });
105
+ };
@@ -0,0 +1,104 @@
1
+ import { QueryClient } from '@tanstack/react-query';
2
+ const QUERY_CACHE_KEY = 'oxy_query_cache';
3
+ const QUERY_CACHE_VERSION = '1';
4
+ /**
5
+ * Custom persistence adapter for TanStack Query using our StorageInterface
6
+ */
7
+ export const createPersistenceAdapter = (storage) => {
8
+ return {
9
+ persistClient: async (client) => {
10
+ try {
11
+ const serialized = JSON.stringify({
12
+ clientState: client,
13
+ timestamp: Date.now(),
14
+ version: QUERY_CACHE_VERSION,
15
+ });
16
+ await storage.setItem(QUERY_CACHE_KEY, serialized);
17
+ }
18
+ catch (error) {
19
+ if (__DEV__) {
20
+ console.warn('[QueryClient] Failed to persist cache:', error);
21
+ }
22
+ }
23
+ },
24
+ restoreClient: async () => {
25
+ try {
26
+ const cached = await storage.getItem(QUERY_CACHE_KEY);
27
+ if (!cached)
28
+ return undefined;
29
+ const parsed = JSON.parse(cached);
30
+ // Check version compatibility
31
+ if (parsed.version !== QUERY_CACHE_VERSION) {
32
+ // Clear old cache on version mismatch
33
+ await storage.removeItem(QUERY_CACHE_KEY);
34
+ return undefined;
35
+ }
36
+ // Check if cache is too old (30 days)
37
+ const maxAge = 30 * 24 * 60 * 60 * 1000;
38
+ if (parsed.timestamp && Date.now() - parsed.timestamp > maxAge) {
39
+ await storage.removeItem(QUERY_CACHE_KEY);
40
+ return undefined;
41
+ }
42
+ return parsed.clientState;
43
+ }
44
+ catch (error) {
45
+ if (__DEV__) {
46
+ console.warn('[QueryClient] Failed to restore cache:', error);
47
+ }
48
+ return undefined;
49
+ }
50
+ },
51
+ removeClient: async () => {
52
+ try {
53
+ await storage.removeItem(QUERY_CACHE_KEY);
54
+ }
55
+ catch (error) {
56
+ if (__DEV__) {
57
+ console.warn('[QueryClient] Failed to remove cache:', error);
58
+ }
59
+ }
60
+ },
61
+ };
62
+ };
63
+ /**
64
+ * Create a QueryClient with offline-first configuration
65
+ */
66
+ export const createQueryClient = (storage) => {
67
+ const client = new QueryClient({
68
+ defaultOptions: {
69
+ queries: {
70
+ // Data is fresh for 5 minutes
71
+ staleTime: 5 * 60 * 1000,
72
+ // Keep unused data in cache for 30 minutes
73
+ gcTime: 30 * 60 * 1000,
74
+ // Retry 3 times with exponential backoff
75
+ retry: 3,
76
+ retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
77
+ // Refetch on reconnect
78
+ refetchOnReconnect: true,
79
+ // Don't refetch on window focus (better for mobile)
80
+ refetchOnWindowFocus: false,
81
+ // Offline-first: use cache when offline
82
+ networkMode: 'offlineFirst',
83
+ },
84
+ mutations: {
85
+ // Retry once for mutations
86
+ retry: 1,
87
+ // Offline-first: queue mutations when offline
88
+ networkMode: 'offlineFirst',
89
+ },
90
+ },
91
+ });
92
+ // Note: Persistence is handled by TanStack Query's built-in persistence
93
+ // For now, we rely on the query client's default behavior with networkMode: 'offlineFirst'
94
+ // The cache will be available in memory and queries will use cached data when offline
95
+ // Full persistence to AsyncStorage can be added later with @tanstack/react-query-persist-client if needed
96
+ return client;
97
+ };
98
+ /**
99
+ * Clear persisted query cache
100
+ */
101
+ export const clearQueryCache = async (storage) => {
102
+ const adapter = createPersistenceAdapter(storage);
103
+ await adapter.removeClient();
104
+ };
@@ -0,0 +1,220 @@
1
+ import { useCallback } from 'react';
2
+ import { useAssetStore } from '../stores/assetStore';
3
+ // Create a singleton instance for the hook
4
+ let oxyInstance = null;
5
+ export const setOxyAssetInstance = (instance) => {
6
+ oxyInstance = instance;
7
+ };
8
+ /**
9
+ * Hook for managing assets with Zustand store integration
10
+ */
11
+ export const useAssets = () => {
12
+ const { assets, uploadProgress, loading, errors, setAsset, setAssets, removeAsset, setUploadProgress, removeUploadProgress, addLink, removeLink, setUploading, setLinking, setDeleting, setUploadError, setLinkError, setDeleteError, clearErrors, getAssetsByApp, getAssetsByEntity, getAssetUsageCount, isAssetLinked, reset } = useAssetStore();
13
+ // Upload asset with progress tracking
14
+ const upload = useCallback(async (file, metadata) => {
15
+ if (!oxyInstance) {
16
+ throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
17
+ }
18
+ try {
19
+ clearErrors();
20
+ setUploading(true);
21
+ // Upload file (progress tracking simplified for now)
22
+ const result = await oxyInstance.assetUpload(file, undefined, metadata);
23
+ // Update progress with final status
24
+ if (result?.file) {
25
+ const fileId = result.file.id;
26
+ setUploadProgress(fileId, {
27
+ fileId,
28
+ uploaded: file.size,
29
+ total: file.size,
30
+ percentage: 100,
31
+ status: 'complete'
32
+ });
33
+ // Remove progress after a short delay
34
+ setTimeout(() => {
35
+ removeUploadProgress(fileId);
36
+ }, 2000);
37
+ }
38
+ // Add asset to store
39
+ if (result.file) {
40
+ setAsset(result.file);
41
+ return result.file;
42
+ }
43
+ return null;
44
+ }
45
+ catch (error) {
46
+ setUploadError(error.message || 'Upload failed');
47
+ throw error;
48
+ }
49
+ finally {
50
+ setUploading(false);
51
+ }
52
+ }, [
53
+ clearErrors,
54
+ setUploading,
55
+ setUploadProgress,
56
+ removeUploadProgress,
57
+ setAsset,
58
+ setUploadError
59
+ ]);
60
+ // Link asset to entity
61
+ const link = useCallback(async (assetId, app, entityType, entityId) => {
62
+ if (!oxyInstance) {
63
+ throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
64
+ }
65
+ try {
66
+ clearErrors();
67
+ setLinking(true);
68
+ // Auto-detect visibility for avatars and profile banners
69
+ const visibility = (entityType === 'avatar' || entityType === 'profile-banner')
70
+ ? 'public'
71
+ : undefined;
72
+ const result = await oxyInstance.assetLink(assetId, app, entityType, entityId, visibility);
73
+ if (result.file) {
74
+ setAsset(result.file);
75
+ }
76
+ else {
77
+ // If API doesn't return full file, update store optimistically
78
+ addLink(assetId, {
79
+ app,
80
+ entityType,
81
+ entityId,
82
+ createdBy: '', // Will be filled by server
83
+ createdAt: new Date().toISOString()
84
+ });
85
+ }
86
+ }
87
+ catch (error) {
88
+ setLinkError(error.message || 'Link failed');
89
+ throw error;
90
+ }
91
+ finally {
92
+ setLinking(false);
93
+ }
94
+ }, [clearErrors, setLinking, setAsset, addLink, setLinkError]);
95
+ // Unlink asset from entity
96
+ const unlink = useCallback(async (assetId, app, entityType, entityId) => {
97
+ if (!oxyInstance) {
98
+ throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
99
+ }
100
+ try {
101
+ clearErrors();
102
+ setLinking(true);
103
+ const result = await oxyInstance.assetUnlink(assetId, app, entityType, entityId);
104
+ if (result.file) {
105
+ setAsset(result.file);
106
+ }
107
+ else {
108
+ // Update store optimistically
109
+ removeLink(assetId, app, entityType, entityId);
110
+ }
111
+ }
112
+ catch (error) {
113
+ setLinkError(error.message || 'Unlink failed');
114
+ throw error;
115
+ }
116
+ finally {
117
+ setLinking(false);
118
+ }
119
+ }, [clearErrors, setLinking, setAsset, removeLink, setLinkError]);
120
+ // Get asset URL
121
+ const getUrl = useCallback(async (assetId, variant, expiresIn) => {
122
+ if (!oxyInstance) {
123
+ throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
124
+ }
125
+ try {
126
+ const result = await oxyInstance.assetGetUrl(assetId, variant, expiresIn);
127
+ return result.url;
128
+ }
129
+ catch (error) {
130
+ throw error;
131
+ }
132
+ }, []);
133
+ // Get asset metadata
134
+ const getAsset = useCallback(async (assetId) => {
135
+ if (!oxyInstance) {
136
+ throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
137
+ }
138
+ try {
139
+ const result = await oxyInstance.assetGet(assetId);
140
+ if (result.file) {
141
+ setAsset(result.file);
142
+ return result.file;
143
+ }
144
+ throw new Error('Asset not found');
145
+ }
146
+ catch (error) {
147
+ throw error;
148
+ }
149
+ }, [setAsset]);
150
+ // Delete asset
151
+ const deleteAsset = useCallback(async (assetId, force = false) => {
152
+ if (!oxyInstance) {
153
+ throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
154
+ }
155
+ try {
156
+ clearErrors();
157
+ setDeleting(true);
158
+ await oxyInstance.assetDelete(assetId, force);
159
+ removeAsset(assetId);
160
+ }
161
+ catch (error) {
162
+ setDeleteError(error.message || 'Delete failed');
163
+ throw error;
164
+ }
165
+ finally {
166
+ setDeleting(false);
167
+ }
168
+ }, [clearErrors, setDeleting, removeAsset, setDeleteError]);
169
+ // Restore asset from trash
170
+ const restore = useCallback(async (assetId) => {
171
+ if (!oxyInstance) {
172
+ throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
173
+ }
174
+ try {
175
+ const result = await oxyInstance.assetRestore(assetId);
176
+ if (result.file) {
177
+ setAsset(result.file);
178
+ }
179
+ }
180
+ catch (error) {
181
+ throw error;
182
+ }
183
+ }, [setAsset]);
184
+ // Get variants
185
+ const getVariants = useCallback(async (assetId) => {
186
+ if (!oxyInstance) {
187
+ throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
188
+ }
189
+ try {
190
+ return await oxyInstance.assetGetVariants(assetId);
191
+ }
192
+ catch (error) {
193
+ throw error;
194
+ }
195
+ }, []);
196
+ return {
197
+ // State
198
+ assets: Object.values(assets),
199
+ uploadProgress,
200
+ loading,
201
+ errors,
202
+ // Actions
203
+ upload,
204
+ link,
205
+ unlink,
206
+ getUrl,
207
+ getAsset,
208
+ deleteAsset,
209
+ restore,
210
+ getVariants,
211
+ // Utility methods
212
+ getAssetsByApp,
213
+ getAssetsByEntity,
214
+ getAssetUsageCount,
215
+ isAssetLinked,
216
+ // Store management
217
+ clearErrors,
218
+ reset
219
+ };
220
+ };
@@ -0,0 +1,86 @@
1
+ import { useEffect, useState } from 'react';
2
+ let oxyInstance = null;
3
+ export const setOxyFileUrlInstance = (instance) => {
4
+ oxyInstance = instance;
5
+ };
6
+ /**
7
+ * Hook to resolve a file's download URL asynchronously.
8
+ *
9
+ * Prefers `getFileDownloadUrlAsync` and falls back to the synchronous
10
+ * `getFileDownloadUrl` helper if the async call fails.
11
+ */
12
+ export const useFileDownloadUrl = (fileId, options) => {
13
+ const [url, setUrl] = useState(null);
14
+ const [loading, setLoading] = useState(false);
15
+ const [error, setError] = useState(null);
16
+ useEffect(() => {
17
+ if (!fileId) {
18
+ setUrl(null);
19
+ setLoading(false);
20
+ setError(null);
21
+ return;
22
+ }
23
+ if (!oxyInstance) {
24
+ // Fail silently but don't crash the UI – caller can decide what to do with null URL.
25
+ setUrl(null);
26
+ setLoading(false);
27
+ setError(new Error('OxyServices instance not configured for useFileDownloadUrl'));
28
+ return;
29
+ }
30
+ let cancelled = false;
31
+ const load = async () => {
32
+ setLoading(true);
33
+ setError(null);
34
+ // Store instance in local variable for TypeScript null checking
35
+ const instance = oxyInstance;
36
+ if (!instance) {
37
+ setLoading(false);
38
+ setError(new Error('OxyServices instance not configured for useFileDownloadUrl'));
39
+ return;
40
+ }
41
+ try {
42
+ const { variant, expiresIn } = options || {};
43
+ let resolvedUrl = null;
44
+ if (typeof instance.getFileDownloadUrlAsync === 'function') {
45
+ resolvedUrl = await instance.getFileDownloadUrlAsync(fileId, variant, expiresIn);
46
+ }
47
+ if (!resolvedUrl && typeof instance.getFileDownloadUrl === 'function') {
48
+ resolvedUrl = instance.getFileDownloadUrl(fileId, variant, expiresIn);
49
+ }
50
+ if (!cancelled) {
51
+ setUrl(resolvedUrl || null);
52
+ }
53
+ }
54
+ catch (err) {
55
+ // Fallback to sync URL on error where possible
56
+ try {
57
+ if (typeof instance.getFileDownloadUrl === 'function') {
58
+ const { variant, expiresIn } = options || {};
59
+ const fallbackUrl = instance.getFileDownloadUrl(fileId, variant, expiresIn);
60
+ if (!cancelled) {
61
+ setUrl(fallbackUrl || null);
62
+ setError(err instanceof Error ? err : new Error(String(err)));
63
+ }
64
+ return;
65
+ }
66
+ }
67
+ catch {
68
+ // ignore secondary failure, we'll surface the original error below
69
+ }
70
+ if (!cancelled) {
71
+ setError(err instanceof Error ? err : new Error(String(err)));
72
+ }
73
+ }
74
+ finally {
75
+ if (!cancelled) {
76
+ setLoading(false);
77
+ }
78
+ }
79
+ };
80
+ load();
81
+ return () => {
82
+ cancelled = true;
83
+ };
84
+ }, [fileId, options?.variant, options?.expiresIn]);
85
+ return { url, loading, error };
86
+ };
@@ -0,0 +1,78 @@
1
+ import { useMemo, useState, useCallback } from 'react';
2
+ /**
3
+ * Hook for file filtering, sorting, and search functionality
4
+ * Extracts common file management logic for reuse across components
5
+ */
6
+ export function useFileFiltering({ files, initialViewMode = 'all', initialSortBy = 'date', initialSortOrder = 'desc', }) {
7
+ const [viewMode, setViewMode] = useState(initialViewMode);
8
+ const [searchQuery, setSearchQuery] = useState('');
9
+ const [sortBy, setSortBy] = useState(initialSortBy);
10
+ const [sortOrder, setSortOrder] = useState(initialSortOrder);
11
+ const toggleSortOrder = useCallback(() => {
12
+ setSortOrder((prev) => (prev === 'asc' ? 'desc' : 'asc'));
13
+ }, []);
14
+ const filteredFiles = useMemo(() => {
15
+ // Filter by view mode
16
+ let filteredByMode = files;
17
+ if (viewMode === 'photos') {
18
+ filteredByMode = files.filter((file) => file.contentType.startsWith('image/'));
19
+ }
20
+ else if (viewMode === 'videos') {
21
+ filteredByMode = files.filter((file) => file.contentType.startsWith('video/'));
22
+ }
23
+ else if (viewMode === 'documents') {
24
+ filteredByMode = files.filter((file) => file.contentType.includes('pdf') ||
25
+ file.contentType.includes('document') ||
26
+ file.contentType.includes('text') ||
27
+ file.contentType.includes('msword') ||
28
+ file.contentType.includes('excel') ||
29
+ file.contentType.includes('spreadsheet') ||
30
+ file.contentType.includes('presentation') ||
31
+ file.contentType.includes('powerpoint'));
32
+ }
33
+ else if (viewMode === 'audio') {
34
+ filteredByMode = files.filter((file) => file.contentType.startsWith('audio/'));
35
+ }
36
+ // Filter by search query
37
+ let filtered = filteredByMode;
38
+ if (searchQuery.trim()) {
39
+ const query = searchQuery.toLowerCase();
40
+ filtered = filteredByMode.filter((file) => file.filename.toLowerCase().includes(query) ||
41
+ file.contentType.toLowerCase().includes(query) ||
42
+ (file.metadata?.description &&
43
+ file.metadata.description.toLowerCase().includes(query)));
44
+ }
45
+ // Sort files
46
+ const sorted = [...filtered].sort((a, b) => {
47
+ let comparison = 0;
48
+ if (sortBy === 'date') {
49
+ const dateA = new Date(a.uploadDate || 0).getTime();
50
+ const dateB = new Date(b.uploadDate || 0).getTime();
51
+ comparison = dateA - dateB;
52
+ }
53
+ else if (sortBy === 'size') {
54
+ comparison = (a.length || 0) - (b.length || 0);
55
+ }
56
+ else if (sortBy === 'name') {
57
+ comparison = (a.filename || '').localeCompare(b.filename || '');
58
+ }
59
+ else if (sortBy === 'type') {
60
+ comparison = (a.contentType || '').localeCompare(b.contentType || '');
61
+ }
62
+ return sortOrder === 'asc' ? comparison : -comparison;
63
+ });
64
+ return sorted;
65
+ }, [files, searchQuery, viewMode, sortBy, sortOrder]);
66
+ return {
67
+ filteredFiles,
68
+ viewMode,
69
+ setViewMode,
70
+ searchQuery,
71
+ setSearchQuery,
72
+ sortBy,
73
+ setSortBy,
74
+ sortOrder,
75
+ setSortOrder,
76
+ toggleSortOrder,
77
+ };
78
+ }