@umituz/react-native-design-system 4.25.11 → 4.25.13

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-design-system",
3
- "version": "4.25.11",
3
+ "version": "4.25.13",
4
4
  "description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive, safe area, exception, infinite scroll, UUID, image, timezone, offline, onboarding, and loading utilities",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Gallery Download Service
3
+ * Single Responsibility: Download remote media to local storage
4
+ */
5
+
6
+ import { FileSystemService } from "../filesystem";
7
+ import { validateImageUri, getFileExtension } from "../image";
8
+ import { timezoneService } from "../timezone";
9
+ import type { DownloadMediaResult } from "./types";
10
+
11
+ const generateFilename = (uri: string, prefix: string): string => {
12
+ const extension = getFileExtension(uri) || "jpg";
13
+ const timestamp = timezoneService.formatDateToString(new Date());
14
+ const randomId = Math.random().toString(36).substring(2, 10);
15
+ return `${prefix}_${timestamp}_${randomId}.${extension}`;
16
+ };
17
+
18
+ class GalleryDownloadService {
19
+ async downloadMedia(
20
+ mediaUri: string,
21
+ prefix: string = "media",
22
+ ): Promise<DownloadMediaResult> {
23
+ try {
24
+ const validationResult = validateImageUri(mediaUri, "Media");
25
+ if (!validationResult.isValid) {
26
+ return {
27
+ success: false,
28
+ error: validationResult.error || "Invalid media file",
29
+ };
30
+ }
31
+
32
+ const filename = generateFilename(mediaUri, prefix);
33
+ const documentDir = FileSystemService.getDocumentDirectory();
34
+ const fileUri = `${documentDir}${filename}`;
35
+
36
+ const downloadResult = await FileSystemService.downloadFile(
37
+ mediaUri,
38
+ fileUri,
39
+ );
40
+
41
+ if (!downloadResult.success || !downloadResult.uri) {
42
+ return {
43
+ success: false,
44
+ error: downloadResult.error || "Download failed",
45
+ };
46
+ }
47
+
48
+ return {
49
+ success: true,
50
+ localUri: downloadResult.uri,
51
+ };
52
+ } catch (error) {
53
+ return {
54
+ success: false,
55
+ error: error instanceof Error ? error.message : "Download failed",
56
+ };
57
+ }
58
+ }
59
+
60
+ isRemoteUrl(uri: string): boolean {
61
+ return uri.startsWith("http://") || uri.startsWith("https://");
62
+ }
63
+
64
+ async cleanupFile(fileUri: string): Promise<void> {
65
+ await FileSystemService.deleteFile(fileUri);
66
+ }
67
+ }
68
+
69
+ export const galleryDownloadService = new GalleryDownloadService();
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Gallery Save Service
3
+ * Single Responsibility: Save media to device gallery
4
+ */
5
+
6
+ import * as MediaLibrary from "expo-media-library";
7
+ import { validateImageUri } from "../image";
8
+ import { galleryDownloadService } from "./gallery-download.service";
9
+ import type { SaveMediaResult } from "./types";
10
+
11
+ const requestMediaPermissions = async (): Promise<boolean> => {
12
+ try {
13
+ const { status } = await MediaLibrary.requestPermissionsAsync();
14
+ return status === "granted";
15
+ } catch {
16
+ return false;
17
+ }
18
+ };
19
+
20
+ class GallerySaveService {
21
+ async saveToGallery(
22
+ mediaUri: string,
23
+ prefix?: string,
24
+ ): Promise<SaveMediaResult> {
25
+ try {
26
+ const validationResult = validateImageUri(mediaUri, "Media");
27
+ if (!validationResult.isValid) {
28
+ return {
29
+ success: false,
30
+ error: validationResult.error || "Invalid media file",
31
+ };
32
+ }
33
+
34
+ const hasPermission = await requestMediaPermissions();
35
+ if (!hasPermission) {
36
+ return {
37
+ success: false,
38
+ error: "Media library permission denied",
39
+ };
40
+ }
41
+
42
+ let localUri = mediaUri;
43
+ const isRemote = galleryDownloadService.isRemoteUrl(mediaUri);
44
+
45
+ if (isRemote) {
46
+ const downloadResult = await galleryDownloadService.downloadMedia(
47
+ mediaUri,
48
+ prefix,
49
+ );
50
+
51
+ if (!downloadResult.success || !downloadResult.localUri) {
52
+ return {
53
+ success: false,
54
+ error: downloadResult.error || "Download failed",
55
+ };
56
+ }
57
+
58
+ localUri = downloadResult.localUri;
59
+ }
60
+
61
+ const asset = await MediaLibrary.createAssetAsync(localUri);
62
+
63
+ if (isRemote && localUri) {
64
+ await galleryDownloadService.cleanupFile(localUri);
65
+ }
66
+
67
+ return {
68
+ success: true,
69
+ fileUri: asset.uri,
70
+ };
71
+ } catch (error) {
72
+ return {
73
+ success: false,
74
+ error: error instanceof Error ? error.message : "Save failed",
75
+ };
76
+ }
77
+ }
78
+ }
79
+
80
+ export const gallerySaveService = new GallerySaveService();
@@ -0,0 +1,3 @@
1
+ export { gallerySaveService } from "./gallery-save.service";
2
+ export { galleryDownloadService } from "./gallery-download.service";
3
+ export type { SaveMediaResult, DownloadMediaResult } from "./types";
@@ -0,0 +1,11 @@
1
+ export interface SaveMediaResult {
2
+ success: boolean;
3
+ fileUri?: string;
4
+ error?: string;
5
+ }
6
+
7
+ export interface DownloadMediaResult {
8
+ success: boolean;
9
+ localUri?: string;
10
+ error?: string;
11
+ }
package/src/index.ts CHANGED
@@ -2,16 +2,8 @@
2
2
  * @umituz/react-native-design-system
3
3
  * Universal design system for React Native apps
4
4
  *
5
- * This main entry point exports only modules with NO optional native dependencies.
6
- * Modules with native dependencies are available via sub-path imports:
7
- * - @umituz/react-native-design-system/gallery (expo-media-library)
8
- * - @umituz/react-native-design-system/media (expo-file-system, expo-image-picker, expo-media-library)
9
- * - @umituz/react-native-design-system/filesystem (expo-file-system)
10
- * - @umituz/react-native-design-system/image (expo-file-system, expo-image-manipulator)
11
- * - @umituz/react-native-design-system/device (expo-device, expo-secure-store)
12
- * - @umituz/react-native-design-system/offline (expo-network)
13
- * - @umituz/react-native-design-system/onboarding (expo-video)
14
- * - @umituz/react-native-design-system/storage (expo-secure-store)
5
+ * Consolidated package including all modules.
6
+ * Sub-path imports also available (e.g. @umituz/react-native-design-system/media)
15
7
  */
16
8
 
17
9
  // =============================================================================
@@ -29,6 +21,11 @@ export * from "./typography";
29
21
  // =============================================================================
30
22
  export * from "./responsive";
31
23
 
24
+ // =============================================================================
25
+ // DEVICE EXPORTS
26
+ // =============================================================================
27
+ export * from "./device";
28
+
32
29
  // =============================================================================
33
30
  // ATOMS EXPORTS
34
31
  // =============================================================================
@@ -64,27 +61,67 @@ export * from "./exception";
64
61
  // =============================================================================
65
62
  export * from "./infinite-scroll";
66
63
 
64
+ // =============================================================================
65
+ // UUID EXPORTS
66
+ // =============================================================================
67
+ export * from "./uuid";
68
+
67
69
  // =============================================================================
68
70
  // TIMEZONE EXPORTS
69
71
  // =============================================================================
70
72
  export * from "./timezone";
71
73
 
74
+ // =============================================================================
75
+ // OFFLINE EXPORTS
76
+ // =============================================================================
77
+ export * from "./offline";
78
+
79
+ // =============================================================================
80
+ // IMAGE EXPORTS
81
+ // =============================================================================
82
+ export * from "./image";
83
+
72
84
  // =============================================================================
73
85
  // HAPTICS EXPORTS
74
86
  // =============================================================================
75
87
  export * from "./haptics";
76
88
 
89
+ // =============================================================================
90
+ // MEDIA EXPORTS
91
+ // =============================================================================
92
+ export * from "./media";
93
+
77
94
  // =============================================================================
78
95
  // VARIANT UTILITIES
79
96
  // =============================================================================
80
97
  export * from "./presentation/utils/variants";
81
98
 
99
+ // =============================================================================
100
+ // UTILITIES
101
+ // =============================================================================
102
+ export * from "./utilities";
103
+
82
104
  // =============================================================================
83
105
  // UTILS EXPORTS (Logger, formatters, validators)
84
106
  // =============================================================================
85
107
  export { logger, Logger } from "./utils/logger";
86
108
  export type { LoggerConfig } from "./utils/logger";
87
109
 
110
+ // =============================================================================
111
+ // STORAGE EXPORTS
112
+ // =============================================================================
113
+ export * from "./storage";
114
+
115
+ // =============================================================================
116
+ // ONBOARDING EXPORTS
117
+ // =============================================================================
118
+ export * from "./onboarding";
119
+
120
+ // =============================================================================
121
+ // FILESYSTEM EXPORTS
122
+ // =============================================================================
123
+ export * from "./filesystem";
124
+
88
125
  // =============================================================================
89
126
  // TANSTACK EXPORTS
90
127
  // =============================================================================
@@ -101,21 +138,11 @@ export * from "./loading";
101
138
  export * from "./init";
102
139
 
103
140
  // =============================================================================
104
- // STORAGE EXPORTS
141
+ // GALLERY EXPORTS
105
142
  // =============================================================================
106
- export * from "./storage";
143
+ export * from "./gallery";
107
144
 
108
145
  // =============================================================================
109
146
  // CAROUSEL EXPORTS
110
147
  // =============================================================================
111
148
  export * from "./carousel";
112
-
113
- // =============================================================================
114
- // OFFLINE EXPORTS
115
- // =============================================================================
116
- export * from "./offline";
117
-
118
- // =============================================================================
119
- // ONBOARDING EXPORTS
120
- // =============================================================================
121
- export * from "./onboarding";
@@ -1,17 +1,33 @@
1
1
  /**
2
2
  * useOffline Hook
3
3
  * Primary hook for accessing offline state in components
4
- * Automatically subscribes to network changes via expo-network
4
+ * Automatically subscribes to network changes via expo-network (lazy loaded)
5
5
  */
6
6
 
7
7
  import { useEffect, useCallback, useRef, useMemo } from 'react';
8
- import * as Network from 'expo-network';
9
8
  import type { NetworkState as ExpoNetworkState } from 'expo-network';
10
9
  import type { NetworkState, OfflineConfig } from '../../types';
11
10
  import { useOfflineStore } from '../../infrastructure/storage/OfflineStore';
12
11
  import { networkEvents } from '../../infrastructure/events/NetworkEvents';
13
12
  import { useOfflineConfigStore } from '../../infrastructure/storage/OfflineConfigStore';
14
13
 
14
+ /**
15
+ * Lazy-load expo-network to avoid crash when native module is not available
16
+ * (e.g. running in Expo Go without a custom dev build)
17
+ */
18
+ let _networkModule: typeof import('expo-network') | null = null;
19
+
20
+ const getNetworkModule = (): typeof import('expo-network') | null => {
21
+ if (_networkModule !== null) return _networkModule;
22
+ try {
23
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
24
+ _networkModule = require('expo-network') as typeof import('expo-network');
25
+ return _networkModule;
26
+ } catch {
27
+ return null;
28
+ }
29
+ };
30
+
15
31
  /**
16
32
  * Convert expo-network state to our internal format
17
33
  */
@@ -67,6 +83,15 @@ export const useOffline = (config?: OfflineConfig) => {
67
83
 
68
84
  isMountedRef.current = true;
69
85
 
86
+ const Network = getNetworkModule();
87
+
88
+ if (!Network) {
89
+ if (__DEV__ || mergedConfig.debug) {
90
+ console.warn('[DesignSystem] useOffline: expo-network native module not available. Network state tracking disabled.');
91
+ }
92
+ return;
93
+ }
94
+
70
95
  Network.getNetworkStateAsync()
71
96
  .then((state: ExpoNetworkState) => {
72
97
  if (isMountedRef.current) {
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Clipboard Utilities
3
+ *
4
+ * Simple wrapper around expo-clipboard for copy/paste functionality
5
+ *
6
+ * Usage:
7
+ * ```typescript
8
+ * import { ClipboardUtils } from '@umituz/react-native-design-system';
9
+ *
10
+ * // Copy text
11
+ * await ClipboardUtils.copyToClipboard('Hello World');
12
+ *
13
+ * // Paste text
14
+ * const text = await ClipboardUtils.getFromClipboard();
15
+ *
16
+ * // Check if clipboard has content
17
+ * const hasContent = await ClipboardUtils.hasContent();
18
+ * ```
19
+ */
20
+
21
+ import * as Clipboard from 'expo-clipboard';
22
+
23
+ export class ClipboardUtils {
24
+ /**
25
+ * Copy text to clipboard
26
+ */
27
+ static async copyToClipboard(text: string): Promise<void> {
28
+ try {
29
+ await Clipboard.setStringAsync(text);
30
+ } catch (error) {
31
+ throw error;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Get text from clipboard
37
+ */
38
+ static async getFromClipboard(): Promise<string> {
39
+ try {
40
+ return await Clipboard.getStringAsync();
41
+ } catch (error) {
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Check if clipboard has content
48
+ */
49
+ static async hasContent(): Promise<boolean> {
50
+ try {
51
+ return await Clipboard.hasStringAsync();
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Clear clipboard
59
+ */
60
+ static async clear(): Promise<void> {
61
+ try {
62
+ await Clipboard.setStringAsync('');
63
+ } catch (error) {
64
+ throw error;
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Clipboard - Public API
3
+ */
4
+
5
+ export { ClipboardUtils } from './ClipboardUtils';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Utilities - Public API
3
+ */
4
+
5
+ export * from './clipboard';
6
+ export * from './sharing';
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Sharing Domain - Core Entities
3
+ *
4
+ * This file defines core types and interfaces for sharing functionality.
5
+ * Handles system share sheet using expo-sharing.
6
+ *
7
+ * @domain sharing
8
+ * @layer domain/entities
9
+ */
10
+
11
+ /**
12
+ * Share options for sharing content
13
+ */
14
+ export interface ShareOptions {
15
+ /**
16
+ * Dialog title (Android only)
17
+ */
18
+ dialogTitle?: string;
19
+
20
+ /**
21
+ * MIME type of the file being shared
22
+ */
23
+ mimeType?: string;
24
+
25
+ /**
26
+ * UTI (Uniform Type Identifier) for the file (iOS only)
27
+ */
28
+ UTI?: string;
29
+ }
30
+
31
+ /**
32
+ * Share result
33
+ */
34
+ export interface ShareResult {
35
+ success: boolean;
36
+ error?: string;
37
+ }
38
+
39
+ /**
40
+ * Common MIME types for sharing
41
+ */
42
+ export const MIME_TYPES = {
43
+ // Images
44
+ IMAGE_JPEG: 'image/jpeg',
45
+ IMAGE_PNG: 'image/png',
46
+ IMAGE_GIF: 'image/gif',
47
+ IMAGE_WEBP: 'image/webp',
48
+
49
+ // Videos
50
+ VIDEO_MP4: 'video/mp4',
51
+ VIDEO_QUICKTIME: 'video/quicktime',
52
+ VIDEO_AVI: 'video/avi',
53
+
54
+ // Audio
55
+ AUDIO_MP3: 'audio/mpeg',
56
+ AUDIO_WAV: 'audio/wav',
57
+ AUDIO_AAC: 'audio/aac',
58
+
59
+ // Documents
60
+ PDF: 'application/pdf',
61
+ TEXT: 'text/plain',
62
+ JSON: 'application/json',
63
+ ZIP: 'application/zip',
64
+
65
+ // Generic
66
+ OCTET_STREAM: 'application/octet-stream',
67
+ } as const;
68
+
69
+ /**
70
+ * iOS UTI (Uniform Type Identifiers)
71
+ */
72
+ export const UTI_TYPES = {
73
+ // Images
74
+ IMAGE: 'public.image',
75
+ JPEG: 'public.jpeg',
76
+ PNG: 'public.png',
77
+
78
+ // Videos
79
+ VIDEO: 'public.video',
80
+ MOVIE: 'public.movie',
81
+
82
+ // Audio
83
+ AUDIO: 'public.audio',
84
+ MP3: 'public.mp3',
85
+
86
+ // Documents
87
+ PDF: 'com.adobe.pdf',
88
+ TEXT: 'public.text',
89
+ JSON: 'public.json',
90
+
91
+ // Generic
92
+ DATA: 'public.data',
93
+ CONTENT: 'public.content',
94
+ } as const;
95
+
96
+ /**
97
+ * Sharing constants
98
+ */
99
+ export const SHARING_CONSTANTS = {
100
+ DEFAULT_DIALOG_TITLE: 'Share',
101
+ DEFAULT_MIME_TYPE: MIME_TYPES.OCTET_STREAM,
102
+ } as const;
103
+
104
+
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Sharing Utilities
3
+ * Helper functions for sharing functionality
4
+ */
5
+
6
+ import { MIME_TYPES, UTI_TYPES, SHARING_CONSTANTS, ShareOptions } from './Share';
7
+
8
+ export class SharingUtils {
9
+ /**
10
+ * Get MIME type from file extension
11
+ */
12
+ static getMimeTypeFromExtension(filename: string): string {
13
+ const extension = filename.split('.').pop()?.toLowerCase();
14
+
15
+ switch (extension) {
16
+ // Images
17
+ case 'jpg':
18
+ case 'jpeg':
19
+ return MIME_TYPES.IMAGE_JPEG;
20
+ case 'png':
21
+ return MIME_TYPES.IMAGE_PNG;
22
+ case 'gif':
23
+ return MIME_TYPES.IMAGE_GIF;
24
+ case 'webp':
25
+ return MIME_TYPES.IMAGE_WEBP;
26
+
27
+ // Videos
28
+ case 'mp4':
29
+ return MIME_TYPES.VIDEO_MP4;
30
+ case 'mov':
31
+ return MIME_TYPES.VIDEO_QUICKTIME;
32
+ case 'avi':
33
+ return MIME_TYPES.VIDEO_AVI;
34
+
35
+ // Audio
36
+ case 'mp3':
37
+ return MIME_TYPES.AUDIO_MP3;
38
+ case 'wav':
39
+ return MIME_TYPES.AUDIO_WAV;
40
+ case 'aac':
41
+ return MIME_TYPES.AUDIO_AAC;
42
+
43
+ // Documents
44
+ case 'pdf':
45
+ return MIME_TYPES.PDF;
46
+ case 'txt':
47
+ return MIME_TYPES.TEXT;
48
+ case 'json':
49
+ return MIME_TYPES.JSON;
50
+ case 'zip':
51
+ return MIME_TYPES.ZIP;
52
+
53
+ default:
54
+ return MIME_TYPES.OCTET_STREAM;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Get UTI from file extension (iOS)
60
+ */
61
+ static getUTIFromExtension(filename: string): string {
62
+ const extension = filename.split('.').pop()?.toLowerCase();
63
+
64
+ switch (extension) {
65
+ // Images
66
+ case 'jpg':
67
+ case 'jpeg':
68
+ return UTI_TYPES.JPEG;
69
+ case 'png':
70
+ return UTI_TYPES.PNG;
71
+ case 'gif':
72
+ case 'webp':
73
+ return UTI_TYPES.IMAGE;
74
+
75
+ // Videos
76
+ case 'mp4':
77
+ case 'mov':
78
+ case 'avi':
79
+ return UTI_TYPES.VIDEO;
80
+
81
+ // Audio
82
+ case 'mp3':
83
+ return UTI_TYPES.MP3;
84
+ case 'wav':
85
+ case 'aac':
86
+ return UTI_TYPES.AUDIO;
87
+
88
+ // Documents
89
+ case 'pdf':
90
+ return UTI_TYPES.PDF;
91
+ case 'txt':
92
+ return UTI_TYPES.TEXT;
93
+ case 'json':
94
+ return UTI_TYPES.JSON;
95
+
96
+ default:
97
+ return UTI_TYPES.DATA;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Prepare share options from filename
103
+ */
104
+ static prepareShareOptions(filename: string, dialogTitle?: string): ShareOptions {
105
+ return {
106
+ dialogTitle: dialogTitle || SHARING_CONSTANTS.DEFAULT_DIALOG_TITLE,
107
+ mimeType: SharingUtils.getMimeTypeFromExtension(filename),
108
+ UTI: SharingUtils.getUTIFromExtension(filename),
109
+ };
110
+ }
111
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Sharing Domain - Barrel Export
3
+ * Provides system share sheet functionality using expo-sharing.
4
+ */
5
+
6
+ // ============================================================================
7
+ // DOMAIN LAYER - ENTITIES
8
+ // ============================================================================
9
+
10
+ export type {
11
+ ShareOptions,
12
+ ShareResult,
13
+ } from './domain/entities/Share';
14
+
15
+ export {
16
+ MIME_TYPES,
17
+ UTI_TYPES,
18
+ SHARING_CONSTANTS,
19
+ } from './domain/entities/Share';
20
+
21
+ export { SharingUtils } from './domain/entities/SharingUtils';
22
+
23
+ // ============================================================================
24
+ // INFRASTRUCTURE LAYER - SERVICES
25
+ // ============================================================================
26
+
27
+ export { SharingService } from './infrastructure/services/SharingService';
28
+
29
+ // ============================================================================
30
+ // PRESENTATION LAYER - HOOKS
31
+ // ============================================================================
32
+
33
+ export { useSharing } from './presentation/hooks/useSharing';
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Sharing Domain - Sharing Service
3
+ *
4
+ * Service for sharing files using expo-sharing.
5
+ * Provides abstraction layer for system share sheet.
6
+ *
7
+ * @domain sharing
8
+ * @layer infrastructure/services
9
+ */
10
+
11
+ import * as Sharing from 'expo-sharing';
12
+ import { FileSystemService } from '../../../../filesystem';
13
+ import type { ShareOptions, ShareResult } from '../../domain/entities/Share';
14
+ import { SharingUtils } from '../../domain/entities/SharingUtils';
15
+
16
+ /**
17
+ * Sharing service for sharing files via system share sheet
18
+ */
19
+ export class SharingService {
20
+ /**
21
+ * Check if sharing is available on device
22
+ */
23
+ static async isAvailable(): Promise<boolean> {
24
+ try {
25
+ return await Sharing.isAvailableAsync();
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Share a file via system share sheet
33
+ *
34
+ * @param uri - File URI to share (can be local or remote http/https)
35
+ * @param options - Share options (dialog title, MIME type, UTI)
36
+ * @returns ShareResult with success status
37
+ *
38
+ * USAGE:
39
+ * ```typescript
40
+ * // Basic share (local)
41
+ * await SharingService.shareFile('file:///path/to/image.jpg');
42
+ *
43
+ * // Remote file (will be downloaded)
44
+ * await SharingService.shareFile('https://example.com/image.jpg');
45
+ * ```
46
+ */
47
+ static async shareFile(uri: string, options?: ShareOptions): Promise<ShareResult> {
48
+ try {
49
+ // Check availability
50
+ const available = await SharingService.isAvailable();
51
+ if (!available) {
52
+ return {
53
+ success: false,
54
+ error: 'Sharing is not available on this device',
55
+ };
56
+ }
57
+
58
+ let shareUri = uri;
59
+
60
+ // Handle remote URLs
61
+ if (uri.startsWith('http://') || uri.startsWith('https://')) {
62
+ try {
63
+ const filename = uri.split('/').pop()?.split('?')[0] || `share-${Date.now()}`;
64
+ // Ensure we have an extension if possible, or default to bin/jpg?
65
+ // Better to rely on what we have, or let the caller specify mimeType to infer extension?
66
+ // For now, nice and simple:
67
+ const localUri = FileSystemService.generateFilePath(filename);
68
+ const result = await FileSystemService.downloadFile(uri, localUri);
69
+
70
+ if (!result.success) {
71
+ return { success: false, error: result.error || 'Failed to download file' };
72
+ }
73
+ shareUri = result.uri!;
74
+ } catch (downloadError) {
75
+ return {
76
+ success: false,
77
+ error: downloadError instanceof Error ? downloadError.message : 'Failed to download file'
78
+ };
79
+ }
80
+ }
81
+
82
+ // Share file
83
+ await Sharing.shareAsync(shareUri, options);
84
+
85
+ return { success: true };
86
+ } catch (error) {
87
+ return {
88
+ success: false,
89
+ error: error instanceof Error ? error.message : 'Failed to share file',
90
+ };
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Share a file with automatic MIME type detection
96
+ *
97
+ * @param uri - File URI to share
98
+ * @param filename - Filename (used for MIME type detection)
99
+ * @param dialogTitle - Optional dialog title (Android)
100
+ * @returns ShareResult with success status
101
+ *
102
+ * USAGE:
103
+ * ```typescript
104
+ * // Auto-detect MIME type from filename
105
+ * await SharingService.shareWithAutoType(
106
+ * 'file:///path/to/file.jpg',
107
+ * 'photo.jpg',
108
+ * 'Share Photo'
109
+ * );
110
+ * ```
111
+ */
112
+ static async shareWithAutoType(
113
+ uri: string,
114
+ filename: string,
115
+ dialogTitle?: string
116
+ ): Promise<ShareResult> {
117
+ const options = SharingUtils.prepareShareOptions(filename, dialogTitle);
118
+ return await SharingService.shareFile(uri, options);
119
+ }
120
+
121
+ /**
122
+ * Share multiple files (if supported)
123
+ *
124
+ * NOTE: expo-sharing currently only supports sharing one file at a time.
125
+ * This method shares files sequentially.
126
+ *
127
+ * @param uris - Array of file URIs to share
128
+ * @param options - Share options
129
+ * @returns ShareResult with success status
130
+ */
131
+ static async shareMultipleFiles(
132
+ uris: string[],
133
+ options?: ShareOptions
134
+ ): Promise<ShareResult> {
135
+ try {
136
+ if (uris.length === 0) {
137
+ return {
138
+ success: false,
139
+ error: 'No files to share',
140
+ };
141
+ }
142
+
143
+ // Check availability
144
+ const available = await SharingService.isAvailable();
145
+ if (!available) {
146
+ return {
147
+ success: false,
148
+ error: 'Sharing is not available on this device',
149
+ };
150
+ }
151
+
152
+ // Share files sequentially
153
+ for (const uri of uris) {
154
+ await Sharing.shareAsync(uri, options);
155
+ }
156
+
157
+ return { success: true };
158
+ } catch (error) {
159
+ return {
160
+ success: false,
161
+ error: error instanceof Error ? error.message : 'Failed to share files',
162
+ };
163
+ }
164
+ }
165
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Sharing Domain - useSharing Hook
3
+ *
4
+ * React hook for sharing files.
5
+ * Provides system share sheet functionality with state management.
6
+ *
7
+ * @domain sharing
8
+ * @layer presentation/hooks
9
+ */
10
+
11
+ import { useCallback, useMemo } from 'react';
12
+ import { SharingService } from '../../infrastructure/services/SharingService';
13
+ import type { ShareOptions } from '../../domain/entities/Share';
14
+ import { useAsyncOperation } from '../../../../utils/hooks';
15
+
16
+ /**
17
+ * useSharing hook for sharing files via system share sheet
18
+ */
19
+ export const useSharing = () => {
20
+ // Check availability on mount
21
+ const availabilityOp = useAsyncOperation<boolean, string>(
22
+ () => SharingService.isAvailable(),
23
+ {
24
+ immediate: true,
25
+ initialData: false,
26
+ errorHandler: () => 'Failed to check sharing availability',
27
+ }
28
+ );
29
+
30
+ // Share operations
31
+ const shareOp = useAsyncOperation<boolean, string>(
32
+ async (uri: string, options?: ShareOptions) => {
33
+ const result = await SharingService.shareFile(uri, options);
34
+ if (!result.success) {
35
+ throw new Error(result.error || 'Failed to share file');
36
+ }
37
+ return true;
38
+ },
39
+ {
40
+ immediate: false,
41
+ errorHandler: (err) => err instanceof Error ? err.message : 'Failed to share file',
42
+ }
43
+ );
44
+
45
+ const shareWithAutoTypeOp = useAsyncOperation<boolean, string>(
46
+ async (uri: string, filename: string, dialogTitle?: string) => {
47
+ const result = await SharingService.shareWithAutoType(uri, filename, dialogTitle);
48
+ if (!result.success) {
49
+ throw new Error(result.error || 'Failed to share file');
50
+ }
51
+ return true;
52
+ },
53
+ {
54
+ immediate: false,
55
+ errorHandler: (err) => err instanceof Error ? err.message : 'Failed to share file',
56
+ }
57
+ );
58
+
59
+ const shareMultipleOp = useAsyncOperation<boolean, string>(
60
+ async (uris: string[], options?: ShareOptions) => {
61
+ const result = await SharingService.shareMultipleFiles(uris, options);
62
+ if (!result.success) {
63
+ throw new Error(result.error || 'Failed to share files');
64
+ }
65
+ return true;
66
+ },
67
+ {
68
+ immediate: false,
69
+ errorHandler: (err) => err instanceof Error ? err.message : 'Failed to share files',
70
+ }
71
+ );
72
+
73
+ const share = useCallback(
74
+ async (uri: string, options?: ShareOptions): Promise<boolean> => {
75
+ const result = await shareOp.execute(uri, options);
76
+ return result ?? false;
77
+ },
78
+ [shareOp]
79
+ );
80
+
81
+ const shareWithAutoType = useCallback(
82
+ async (uri: string, filename: string, dialogTitle?: string): Promise<boolean> => {
83
+ const result = await shareWithAutoTypeOp.execute(uri, filename, dialogTitle);
84
+ return result ?? false;
85
+ },
86
+ [shareWithAutoTypeOp]
87
+ );
88
+
89
+ const shareMultiple = useCallback(
90
+ async (uris: string[], options?: ShareOptions): Promise<boolean> => {
91
+ const result = await shareMultipleOp.execute(uris, options);
92
+ return result ?? false;
93
+ },
94
+ [shareMultipleOp]
95
+ );
96
+
97
+ return useMemo(() => ({
98
+ share,
99
+ shareWithAutoType,
100
+ shareMultiple,
101
+ isAvailable: availabilityOp.data ?? false,
102
+ isSharing: shareOp.isLoading || shareWithAutoTypeOp.isLoading || shareMultipleOp.isLoading,
103
+ error: shareOp.error || shareWithAutoTypeOp.error || shareMultipleOp.error,
104
+ }), [
105
+ share,
106
+ shareWithAutoType,
107
+ shareMultiple,
108
+ availabilityOp.data,
109
+ shareOp.isLoading,
110
+ shareWithAutoTypeOp.isLoading,
111
+ shareMultipleOp.isLoading,
112
+ shareOp.error,
113
+ shareWithAutoTypeOp.error,
114
+ shareMultipleOp.error,
115
+ ]);
116
+ };