@umituz/react-native-design-system 2.3.1 → 2.3.2
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 +15 -3
- package/src/atoms/AtomicInput.tsx +0 -1
- package/src/atoms/AtomicPicker.tsx +0 -1
- package/src/atoms/picker/components/PickerChips.tsx +0 -1
- package/src/atoms/picker/components/PickerModal.tsx +1 -3
- package/src/atoms/picker/styles/pickerStyles.ts +1 -1
- package/src/device/domain/entities/Device.ts +207 -0
- package/src/device/domain/entities/DeviceMemoryUtils.ts +62 -0
- package/src/device/domain/entities/DeviceTypeUtils.ts +66 -0
- package/src/device/domain/entities/__tests__/DeviceMemoryUtils.test.ts +118 -0
- package/src/device/domain/entities/__tests__/DeviceTypeUtils.test.ts +104 -0
- package/src/device/domain/entities/__tests__/DeviceUtils.test.ts +167 -0
- package/src/device/index.ts +51 -0
- package/src/device/infrastructure/services/ApplicationInfoService.ts +86 -0
- package/src/device/infrastructure/services/DeviceCapabilityService.ts +60 -0
- package/src/device/infrastructure/services/DeviceIdService.ts +70 -0
- package/src/device/infrastructure/services/DeviceInfoService.ts +95 -0
- package/src/device/infrastructure/services/DeviceService.ts +104 -0
- package/src/device/infrastructure/services/PersistentDeviceIdService.ts +132 -0
- package/src/device/infrastructure/services/UserFriendlyIdService.ts +68 -0
- package/src/device/infrastructure/utils/__tests__/nativeModuleUtils.test.ts +158 -0
- package/src/device/infrastructure/utils/__tests__/stringUtils.test.ts +120 -0
- package/src/device/infrastructure/utils/nativeModuleUtils.ts +69 -0
- package/src/device/infrastructure/utils/stringUtils.ts +59 -0
- package/src/device/presentation/hooks/useAnonymousUser.ts +117 -0
- package/src/device/presentation/hooks/useDeviceInfo.ts +222 -0
- package/src/molecules/ConfirmationModalContent.tsx +4 -4
- package/src/molecules/ConfirmationModalMain.tsx +1 -1
- package/src/molecules/ScreenHeader.tsx +2 -2
- package/src/molecules/confirmation-modal/components.tsx +1 -1
- package/src/molecules/confirmation-modal/styles/confirmationModalStyles.ts +6 -7
- package/src/presentation/utils/variants/__tests__/core.test.ts +0 -1
- package/src/responsive/deviceDetection.ts +5 -5
- package/src/responsive/iPadBreakpoints.ts +55 -0
- package/src/responsive/iPadDetection.ts +48 -0
- package/src/responsive/iPadLayoutUtils.ts +95 -0
- package/src/responsive/iPadModalUtils.ts +98 -0
- package/src/responsive/index.ts +31 -0
- package/src/safe-area/__tests__/components/SafeAreaProvider.test.tsx +2 -2
- package/src/safe-area/__tests__/hooks/useContentSafeAreaPadding.test.tsx +2 -2
- package/src/safe-area/__tests__/hooks/useHeaderSafeAreaPadding.test.tsx +2 -2
- package/src/safe-area/__tests__/hooks/useSafeAreaInsets.test.tsx +2 -2
- package/src/safe-area/__tests__/hooks/useStatusBarSafeAreaPadding.test.tsx +2 -2
- package/src/safe-area/__tests__/integration/completeFlow.test.tsx +5 -4
- package/src/safe-area/__tests__/utils/testUtils.tsx +5 -4
- package/src/theme/infrastructure/stores/themeStore.ts +0 -2
- package/src/typography/presentation/utils/textColorUtils.ts +0 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anonymous User Hook
|
|
3
|
+
*
|
|
4
|
+
* Provides persistent device-based user ID for anonymous users.
|
|
5
|
+
* The ID is stable across app restarts and sessions.
|
|
6
|
+
* Compatible with subscription services and Firebase Anonymous Auth.
|
|
7
|
+
*
|
|
8
|
+
* @domain device
|
|
9
|
+
* @layer presentation/hooks
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
13
|
+
import { PersistentDeviceIdService } from '../../infrastructure/services/PersistentDeviceIdService';
|
|
14
|
+
import { DeviceService } from '../../infrastructure/services/DeviceService';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Anonymous user data
|
|
18
|
+
*/
|
|
19
|
+
export interface AnonymousUser {
|
|
20
|
+
/** Persistent device-based user ID (stable across sessions) */
|
|
21
|
+
userId: string;
|
|
22
|
+
/** User-friendly device name (e.g., "iPhone13-A8F2") */
|
|
23
|
+
deviceName: string;
|
|
24
|
+
/** Display name for the anonymous user */
|
|
25
|
+
displayName: string;
|
|
26
|
+
/** Always true for anonymous users */
|
|
27
|
+
isAnonymous: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* useAnonymousUser hook options
|
|
32
|
+
*/
|
|
33
|
+
export interface UseAnonymousUserOptions {
|
|
34
|
+
/** Custom display name for anonymous user */
|
|
35
|
+
anonymousDisplayName?: string;
|
|
36
|
+
/** Fallback user ID if device ID generation fails */
|
|
37
|
+
fallbackUserId?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* useAnonymousUser hook for persistent device-based user identification
|
|
42
|
+
*
|
|
43
|
+
* USAGE:
|
|
44
|
+
* ```typescript
|
|
45
|
+
* import { useAnonymousUser } from '@umituz/react-native-device';
|
|
46
|
+
*
|
|
47
|
+
* const { anonymousUser, isLoading } = useAnonymousUser();
|
|
48
|
+
*
|
|
49
|
+
* // Use for subscription services
|
|
50
|
+
* await subscriptionService.initialize(anonymousUser?.userId);
|
|
51
|
+
*
|
|
52
|
+
* // Use in SettingsScreen
|
|
53
|
+
* <UserProfileHeader
|
|
54
|
+
* userId={anonymousUser?.userId}
|
|
55
|
+
* displayName={anonymousUser?.displayName}
|
|
56
|
+
* isAnonymous={anonymousUser?.isAnonymous}
|
|
57
|
+
* />
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export const useAnonymousUser = (options?: UseAnonymousUserOptions) => {
|
|
61
|
+
const [anonymousUser, setAnonymousUser] = useState<AnonymousUser | null>(null);
|
|
62
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
63
|
+
const [error, setError] = useState<string | null>(null);
|
|
64
|
+
|
|
65
|
+
const {
|
|
66
|
+
anonymousDisplayName = 'Anonymous',
|
|
67
|
+
fallbackUserId = 'anonymous_fallback',
|
|
68
|
+
} = options || {};
|
|
69
|
+
|
|
70
|
+
const loadAnonymousUser = useCallback(async () => {
|
|
71
|
+
setIsLoading(true);
|
|
72
|
+
setError(null);
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const [userId, deviceName] = await Promise.all([
|
|
76
|
+
PersistentDeviceIdService.getDeviceId(),
|
|
77
|
+
DeviceService.getUserFriendlyId(),
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
setAnonymousUser({
|
|
81
|
+
userId: userId || fallbackUserId,
|
|
82
|
+
deviceName: deviceName || 'Unknown Device',
|
|
83
|
+
displayName: anonymousDisplayName,
|
|
84
|
+
isAnonymous: true,
|
|
85
|
+
});
|
|
86
|
+
} catch {
|
|
87
|
+
setAnonymousUser({
|
|
88
|
+
userId: fallbackUserId,
|
|
89
|
+
deviceName: 'Unknown Device',
|
|
90
|
+
displayName: anonymousDisplayName,
|
|
91
|
+
isAnonymous: true,
|
|
92
|
+
});
|
|
93
|
+
setError('Failed to generate device ID');
|
|
94
|
+
} finally {
|
|
95
|
+
setIsLoading(false);
|
|
96
|
+
}
|
|
97
|
+
}, [anonymousDisplayName, fallbackUserId]);
|
|
98
|
+
|
|
99
|
+
const refresh = useCallback(async () => {
|
|
100
|
+
await loadAnonymousUser();
|
|
101
|
+
}, [loadAnonymousUser]);
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
loadAnonymousUser();
|
|
105
|
+
}, [loadAnonymousUser]);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
/** Anonymous user data with persistent device-based ID */
|
|
109
|
+
anonymousUser,
|
|
110
|
+
/** Loading state */
|
|
111
|
+
isLoading,
|
|
112
|
+
/** Error message if ID generation failed */
|
|
113
|
+
error,
|
|
114
|
+
/** Refresh function to reload user data */
|
|
115
|
+
refresh,
|
|
116
|
+
};
|
|
117
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Domain - useDeviceInfo Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for device and application information.
|
|
5
|
+
* Provides device details with state management.
|
|
6
|
+
*
|
|
7
|
+
* @domain device
|
|
8
|
+
* @layer presentation/hooks
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
12
|
+
import { DeviceService } from '../../infrastructure/services/DeviceService';
|
|
13
|
+
import type { DeviceInfo, ApplicationInfo, SystemInfo } from '../../domain/entities/Device';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* useDeviceInfo hook for device and application information
|
|
17
|
+
*
|
|
18
|
+
* USAGE:
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const { deviceInfo, appInfo, systemInfo, isLoading, refresh } = useDeviceInfo();
|
|
21
|
+
*
|
|
22
|
+
* // Display device info
|
|
23
|
+
* <Text>Device: {deviceInfo?.modelName}</Text>
|
|
24
|
+
* <Text>OS: {deviceInfo?.osName} {deviceInfo?.osVersion}</Text>
|
|
25
|
+
*
|
|
26
|
+
* // Display app info
|
|
27
|
+
* <Text>App: {appInfo?.applicationName}</Text>
|
|
28
|
+
* <Text>Version: {appInfo?.nativeApplicationVersion}</Text>
|
|
29
|
+
*
|
|
30
|
+
* // Refresh info
|
|
31
|
+
* <AtomicButton onPress={refresh}>Refresh</AtomicButton>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export const useDeviceInfo = () => {
|
|
35
|
+
const [deviceInfo, setDeviceInfo] = useState<DeviceInfo | null>(null);
|
|
36
|
+
const [appInfo, setAppInfo] = useState<ApplicationInfo | null>(null);
|
|
37
|
+
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
|
|
38
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
39
|
+
const [error, setError] = useState<string | null>(null);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Load all device and app information
|
|
43
|
+
*/
|
|
44
|
+
const loadInfo = useCallback(async () => {
|
|
45
|
+
setIsLoading(true);
|
|
46
|
+
setError(null);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const system = await DeviceService.getSystemInfo();
|
|
50
|
+
setSystemInfo(system);
|
|
51
|
+
setDeviceInfo(system.device);
|
|
52
|
+
setAppInfo(system.application);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to load device info';
|
|
55
|
+
setError(errorMessage);
|
|
56
|
+
} finally {
|
|
57
|
+
setIsLoading(false);
|
|
58
|
+
}
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Load device info only
|
|
63
|
+
*/
|
|
64
|
+
const loadDeviceInfo = useCallback(async () => {
|
|
65
|
+
setIsLoading(true);
|
|
66
|
+
setError(null);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const info = await DeviceService.getDeviceInfo();
|
|
70
|
+
setDeviceInfo(info);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to load device info';
|
|
73
|
+
setError(errorMessage);
|
|
74
|
+
} finally {
|
|
75
|
+
setIsLoading(false);
|
|
76
|
+
}
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Load app info only
|
|
81
|
+
*/
|
|
82
|
+
const loadAppInfo = useCallback(async () => {
|
|
83
|
+
setIsLoading(true);
|
|
84
|
+
setError(null);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const info = await DeviceService.getApplicationInfo();
|
|
88
|
+
setAppInfo(info);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to load app info';
|
|
91
|
+
setError(errorMessage);
|
|
92
|
+
} finally {
|
|
93
|
+
setIsLoading(false);
|
|
94
|
+
}
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Refresh all info
|
|
99
|
+
*/
|
|
100
|
+
const refresh = useCallback(async () => {
|
|
101
|
+
await loadInfo();
|
|
102
|
+
}, [loadInfo]);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Load info on mount
|
|
106
|
+
*/
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
loadInfo();
|
|
109
|
+
}, [loadInfo]);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
// Data
|
|
113
|
+
deviceInfo,
|
|
114
|
+
appInfo,
|
|
115
|
+
systemInfo,
|
|
116
|
+
|
|
117
|
+
// State
|
|
118
|
+
isLoading,
|
|
119
|
+
error,
|
|
120
|
+
|
|
121
|
+
// Functions
|
|
122
|
+
refresh,
|
|
123
|
+
loadDeviceInfo,
|
|
124
|
+
loadAppInfo,
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* useDeviceCapabilities hook for device feature detection
|
|
130
|
+
*
|
|
131
|
+
* USAGE:
|
|
132
|
+
* ```typescript
|
|
133
|
+
* const { isDevice, isTablet, hasNotch, totalMemoryGB } = useDeviceCapabilities();
|
|
134
|
+
*
|
|
135
|
+
* // Conditional rendering
|
|
136
|
+
* {isTablet && <TabletLayout />}
|
|
137
|
+
* {hasNotch && <NotchSpacer />}
|
|
138
|
+
*
|
|
139
|
+
* // Performance optimization
|
|
140
|
+
* {totalMemoryGB && totalMemoryGB < 2 && <LowMemoryWarning />}
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export const useDeviceCapabilities = () => {
|
|
144
|
+
const [isDevice, setIsDevice] = useState(false);
|
|
145
|
+
const [isTablet, setIsTablet] = useState(false);
|
|
146
|
+
const [hasNotch, setHasNotch] = useState(false);
|
|
147
|
+
const [totalMemoryGB, setTotalMemoryGB] = useState<number | null>(null);
|
|
148
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
const loadCapabilities = async () => {
|
|
152
|
+
setIsLoading(true);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const capabilities = await DeviceService.getDeviceCapabilities();
|
|
156
|
+
setIsDevice(capabilities.isDevice);
|
|
157
|
+
setIsTablet(capabilities.isTablet);
|
|
158
|
+
setHasNotch(capabilities.hasNotch);
|
|
159
|
+
setTotalMemoryGB(capabilities.totalMemoryGB);
|
|
160
|
+
} catch {
|
|
161
|
+
setIsDevice(false);
|
|
162
|
+
setIsTablet(false);
|
|
163
|
+
setHasNotch(false);
|
|
164
|
+
setTotalMemoryGB(null);
|
|
165
|
+
} finally {
|
|
166
|
+
setIsLoading(false);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
loadCapabilities();
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
isDevice,
|
|
175
|
+
isTablet,
|
|
176
|
+
hasNotch,
|
|
177
|
+
totalMemoryGB,
|
|
178
|
+
isLoading,
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* useDeviceId hook for device unique identifier
|
|
184
|
+
*
|
|
185
|
+
* WARNING: Use with caution - user privacy considerations!
|
|
186
|
+
*
|
|
187
|
+
* USAGE:
|
|
188
|
+
* ```typescript
|
|
189
|
+
* const { deviceId, isLoading } = useDeviceId();
|
|
190
|
+
*
|
|
191
|
+
* // Analytics, crash reporting (with user consent)
|
|
192
|
+
* if (deviceId) {
|
|
193
|
+
* analytics.setUserId(deviceId);
|
|
194
|
+
* }
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export const useDeviceId = () => {
|
|
198
|
+
const [deviceId, setDeviceId] = useState<string | null>(null);
|
|
199
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
200
|
+
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
const loadDeviceId = async () => {
|
|
203
|
+
setIsLoading(true);
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const id = await DeviceService.getDeviceId();
|
|
207
|
+
setDeviceId(id);
|
|
208
|
+
} catch {
|
|
209
|
+
setDeviceId(null);
|
|
210
|
+
} finally {
|
|
211
|
+
setIsLoading(false);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
loadDeviceId();
|
|
216
|
+
}, []);
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
deviceId,
|
|
220
|
+
isLoading,
|
|
221
|
+
};
|
|
222
|
+
};
|
|
@@ -52,13 +52,13 @@ export const ConfirmationModalContent: React.FC<{
|
|
|
52
52
|
style?: StyleProp<ViewStyle>;
|
|
53
53
|
testID: string;
|
|
54
54
|
}> = ({ tokens, variant, title, message, confirmText, cancelText, icon, onConfirm, onCancel, style, testID }) => {
|
|
55
|
-
const variantConfig = getVariantConfig(variant as 'default' | 'destructive' | 'warning' | 'success'
|
|
55
|
+
const variantConfig = getVariantConfig(variant as 'default' | 'destructive' | 'warning' | 'success');
|
|
56
56
|
const finalIcon = icon || variantConfig.icon;
|
|
57
57
|
const getConfirmButtonStyle = useConfirmButtonStyle(variant, tokens);
|
|
58
58
|
|
|
59
59
|
return (
|
|
60
60
|
<View style={[getModalContainerStyle(tokens), style]}>
|
|
61
|
-
<View style={getIconContainerStyle(
|
|
61
|
+
<View style={getIconContainerStyle()}>
|
|
62
62
|
<ConfirmationModalIcon
|
|
63
63
|
icon={finalIcon}
|
|
64
64
|
iconColor={variantConfig.iconColor}
|
|
@@ -66,11 +66,11 @@ export const ConfirmationModalContent: React.FC<{
|
|
|
66
66
|
/>
|
|
67
67
|
</View>
|
|
68
68
|
|
|
69
|
-
<View style={getTitleContainerStyle(
|
|
69
|
+
<View style={getTitleContainerStyle()}>
|
|
70
70
|
<ConfirmationModalTitle title={title} tokens={tokens} testID={testID} />
|
|
71
71
|
</View>
|
|
72
72
|
|
|
73
|
-
<View style={getMessageContainerStyle(
|
|
73
|
+
<View style={getMessageContainerStyle()}>
|
|
74
74
|
<ConfirmationModalMessage message={message} tokens={tokens} testID={testID} />
|
|
75
75
|
</View>
|
|
76
76
|
|
|
@@ -65,7 +65,7 @@ export const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
|
|
|
65
65
|
statusBarTranslucent
|
|
66
66
|
testID={testID}
|
|
67
67
|
>
|
|
68
|
-
<View style={getModalOverlayStyle(
|
|
68
|
+
<View style={getModalOverlayStyle()}>
|
|
69
69
|
<ConfirmationModalBackdrop
|
|
70
70
|
showBackdrop={showBackdrop}
|
|
71
71
|
onBackdropPress={handleBackdropPress}
|
|
@@ -74,8 +74,8 @@ const ScreenHeaderBackButton: React.FC<{
|
|
|
74
74
|
const handleBackPress = React.useCallback(() => {
|
|
75
75
|
if (onBackPress) {
|
|
76
76
|
onBackPress();
|
|
77
|
-
} else {
|
|
78
|
-
|
|
77
|
+
} else if (__DEV__) {
|
|
78
|
+
console.warn('ScreenHeader: onBackPress is required when back button is visible');
|
|
79
79
|
}
|
|
80
80
|
}, [onBackPress]);
|
|
81
81
|
|
|
@@ -70,7 +70,7 @@ export const ConfirmationModalButtons: React.FC<{
|
|
|
70
70
|
confirmButtonStyle: StyleProp<ViewStyle>;
|
|
71
71
|
testID: string;
|
|
72
72
|
}> = ({ confirmText, cancelText, onConfirm, onCancel, confirmButtonStyle, testID }) => (
|
|
73
|
-
<View style={getButtonContainerStyle(
|
|
73
|
+
<View style={getButtonContainerStyle()}>
|
|
74
74
|
<AtomicButton
|
|
75
75
|
variant="outline"
|
|
76
76
|
size="md"
|
|
@@ -13,8 +13,7 @@ import type { DesignTokens } from '../../../theme';
|
|
|
13
13
|
* Note: Confirm text is handled in component with translations
|
|
14
14
|
*/
|
|
15
15
|
export const getVariantConfig = (
|
|
16
|
-
variant: ConfirmationModalVariant
|
|
17
|
-
_tokens: DesignTokens
|
|
16
|
+
variant: ConfirmationModalVariant
|
|
18
17
|
): Omit<ConfirmationModalVariantConfig, 'confirmText'> => {
|
|
19
18
|
switch (variant) {
|
|
20
19
|
case 'destructive':
|
|
@@ -44,7 +43,7 @@ export const getVariantConfig = (
|
|
|
44
43
|
/**
|
|
45
44
|
* Get modal overlay style
|
|
46
45
|
*/
|
|
47
|
-
export const getModalOverlayStyle = (
|
|
46
|
+
export const getModalOverlayStyle = (): ViewStyle => ({
|
|
48
47
|
flex: 1,
|
|
49
48
|
justifyContent: 'center',
|
|
50
49
|
alignItems: 'center',
|
|
@@ -79,28 +78,28 @@ export const getModalContainerStyle = (tokens: DesignTokens): ViewStyle => ({
|
|
|
79
78
|
/**
|
|
80
79
|
* Get icon container style
|
|
81
80
|
*/
|
|
82
|
-
export const getIconContainerStyle = (
|
|
81
|
+
export const getIconContainerStyle = (): ViewStyle => ({
|
|
83
82
|
marginBottom: 16,
|
|
84
83
|
});
|
|
85
84
|
|
|
86
85
|
/**
|
|
87
86
|
* Get title container style
|
|
88
87
|
*/
|
|
89
|
-
export const getTitleContainerStyle = (
|
|
88
|
+
export const getTitleContainerStyle = (): ViewStyle => ({
|
|
90
89
|
marginBottom: 8,
|
|
91
90
|
});
|
|
92
91
|
|
|
93
92
|
/**
|
|
94
93
|
* Get message container style
|
|
95
94
|
*/
|
|
96
|
-
export const getMessageContainerStyle = (
|
|
95
|
+
export const getMessageContainerStyle = (): ViewStyle => ({
|
|
97
96
|
marginBottom: 24,
|
|
98
97
|
});
|
|
99
98
|
|
|
100
99
|
/**
|
|
101
100
|
* Get button container style
|
|
102
101
|
*/
|
|
103
|
-
export const getButtonContainerStyle = (
|
|
102
|
+
export const getButtonContainerStyle = (): ViewStyle => ({
|
|
104
103
|
flexDirection: 'row',
|
|
105
104
|
gap: 12,
|
|
106
105
|
width: '100%',
|
|
@@ -17,13 +17,13 @@ import { validateScreenDimensions } from './validation';
|
|
|
17
17
|
* @returns Operation result or fallback
|
|
18
18
|
*/
|
|
19
19
|
const withDeviceDetectionFallback = <T>(
|
|
20
|
-
operation: () => T,
|
|
21
|
-
fallback: T,
|
|
20
|
+
operation: () => T,
|
|
21
|
+
fallback: T,
|
|
22
22
|
warningMessage: string
|
|
23
23
|
): T => {
|
|
24
24
|
try {
|
|
25
25
|
return operation();
|
|
26
|
-
} catch
|
|
26
|
+
} catch {
|
|
27
27
|
if (__DEV__) {
|
|
28
28
|
console.warn(`[DeviceDetection] ${warningMessage}`);
|
|
29
29
|
}
|
|
@@ -48,11 +48,11 @@ export enum DeviceType {
|
|
|
48
48
|
*/
|
|
49
49
|
export const getScreenDimensions = () => {
|
|
50
50
|
const { width, height } = Dimensions.get('window');
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
try {
|
|
53
53
|
validateScreenDimensions(width, height);
|
|
54
54
|
return { width, height };
|
|
55
|
-
} catch
|
|
55
|
+
} catch {
|
|
56
56
|
if (__DEV__) {
|
|
57
57
|
console.warn('[getScreenDimensions] Invalid screen dimensions detected, using fallback values');
|
|
58
58
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iPad Breakpoints and Constants
|
|
3
|
+
* Apple HIG compliant values for iPad responsive design
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* iPad specific breakpoints following Apple HIG
|
|
8
|
+
*/
|
|
9
|
+
export const IPAD_BREAKPOINTS = {
|
|
10
|
+
IPAD_MINI: 744,
|
|
11
|
+
IPAD_10_2: 810,
|
|
12
|
+
IPAD_AIR: 820,
|
|
13
|
+
IPAD_11_PRO: 834,
|
|
14
|
+
IPAD_12_9_PRO: 1024,
|
|
15
|
+
IPAD_LANDSCAPE_MINI: 1133,
|
|
16
|
+
IPAD_LANDSCAPE_AIR: 1180,
|
|
17
|
+
IPAD_LANDSCAPE_PRO: 1366,
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Apple HIG Touch Target Guidelines
|
|
22
|
+
*/
|
|
23
|
+
export const TOUCH_TARGETS = {
|
|
24
|
+
MINIMUM: 44,
|
|
25
|
+
RECOMMENDED: 48,
|
|
26
|
+
IPAD_RECOMMENDED: 50,
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Content width constraints for iPad
|
|
31
|
+
*/
|
|
32
|
+
export const CONTENT_WIDTH_CONSTRAINTS = {
|
|
33
|
+
READABLE_MAX: 672,
|
|
34
|
+
FORM_MAX: 580,
|
|
35
|
+
CARD_MAX: 400,
|
|
36
|
+
MODAL_MAX: 600,
|
|
37
|
+
PAYWALL_MAX: 540,
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* iPad Layout Configuration
|
|
42
|
+
*/
|
|
43
|
+
export const IPAD_LAYOUT_CONFIG = {
|
|
44
|
+
SCREEN_PADDING: 24,
|
|
45
|
+
SECTION_SPACING: 32,
|
|
46
|
+
CARD_SPACING: 20,
|
|
47
|
+
GRID_COLUMNS_PORTRAIT: 2,
|
|
48
|
+
GRID_COLUMNS_LANDSCAPE: 3,
|
|
49
|
+
GRID_GAP: 20,
|
|
50
|
+
MODAL_CORNER_RADIUS: 16,
|
|
51
|
+
SHEET_CORNER_RADIUS: 20,
|
|
52
|
+
MODAL_HORIZONTAL_MARGIN: 48,
|
|
53
|
+
FONT_SCALE: 1.1,
|
|
54
|
+
HEADING_SCALE: 1.15,
|
|
55
|
+
} as const;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iPad Device Detection Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Dimensions, Platform } from 'react-native';
|
|
6
|
+
import { DEVICE_BREAKPOINTS } from './config';
|
|
7
|
+
import { IPAD_BREAKPOINTS } from './iPadBreakpoints';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Detect if the current device is an iPad
|
|
11
|
+
*/
|
|
12
|
+
export function isIPad(): boolean {
|
|
13
|
+
if (Platform.OS !== 'ios') return false;
|
|
14
|
+
|
|
15
|
+
const { width, height } = Dimensions.get('window');
|
|
16
|
+
const minDimension = Math.min(width, height);
|
|
17
|
+
return minDimension >= DEVICE_BREAKPOINTS.SMALL_TABLET;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Detect if the current device is an iPad mini
|
|
22
|
+
*/
|
|
23
|
+
export function isIPadMini(): boolean {
|
|
24
|
+
if (!isIPad()) return false;
|
|
25
|
+
|
|
26
|
+
const { width, height } = Dimensions.get('window');
|
|
27
|
+
const minWidth = Math.min(width, height);
|
|
28
|
+
return minWidth < IPAD_BREAKPOINTS.IPAD_AIR;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Detect if the current device is an iPad Pro (12.9")
|
|
33
|
+
*/
|
|
34
|
+
export function isIPadPro(): boolean {
|
|
35
|
+
if (!isIPad()) return false;
|
|
36
|
+
|
|
37
|
+
const { width, height } = Dimensions.get('window');
|
|
38
|
+
const minWidth = Math.min(width, height);
|
|
39
|
+
return minWidth >= IPAD_BREAKPOINTS.IPAD_11_PRO;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if device is in landscape orientation
|
|
44
|
+
*/
|
|
45
|
+
export function isIPadLandscape(): boolean {
|
|
46
|
+
const { width, height } = Dimensions.get('window');
|
|
47
|
+
return width > height;
|
|
48
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iPad Layout Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Dimensions } from 'react-native';
|
|
6
|
+
import { DEVICE_BREAKPOINTS } from './config';
|
|
7
|
+
import {
|
|
8
|
+
IPAD_BREAKPOINTS,
|
|
9
|
+
TOUCH_TARGETS,
|
|
10
|
+
CONTENT_WIDTH_CONSTRAINTS,
|
|
11
|
+
IPAD_LAYOUT_CONFIG,
|
|
12
|
+
} from './iPadBreakpoints';
|
|
13
|
+
import { isIPad, isIPadPro } from './iPadDetection';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get optimal content max width based on screen size
|
|
17
|
+
*/
|
|
18
|
+
export function getContentMaxWidth(screenWidth: number): number {
|
|
19
|
+
if (screenWidth >= IPAD_BREAKPOINTS.IPAD_12_9_PRO) {
|
|
20
|
+
return CONTENT_WIDTH_CONSTRAINTS.READABLE_MAX;
|
|
21
|
+
}
|
|
22
|
+
if (screenWidth >= IPAD_BREAKPOINTS.IPAD_AIR) {
|
|
23
|
+
return Math.min(screenWidth * 0.85, CONTENT_WIDTH_CONSTRAINTS.READABLE_MAX);
|
|
24
|
+
}
|
|
25
|
+
if (screenWidth >= DEVICE_BREAKPOINTS.SMALL_TABLET) {
|
|
26
|
+
return Math.min(screenWidth * 0.90, CONTENT_WIDTH_CONSTRAINTS.FORM_MAX);
|
|
27
|
+
}
|
|
28
|
+
return screenWidth;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get grid columns based on screen width
|
|
33
|
+
*/
|
|
34
|
+
export function getIPadGridColumns(screenWidth: number): number {
|
|
35
|
+
if (screenWidth >= IPAD_BREAKPOINTS.IPAD_LANDSCAPE_AIR) return 4;
|
|
36
|
+
if (screenWidth >= IPAD_BREAKPOINTS.IPAD_12_9_PRO) return 3;
|
|
37
|
+
if (screenWidth >= DEVICE_BREAKPOINTS.SMALL_TABLET) return 2;
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get touch target size based on device
|
|
43
|
+
*/
|
|
44
|
+
export function getTouchTargetSize(): number {
|
|
45
|
+
return isIPad() ? TOUCH_TARGETS.IPAD_RECOMMENDED : TOUCH_TARGETS.RECOMMENDED;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get screen padding based on device type
|
|
50
|
+
*/
|
|
51
|
+
export function getIPadScreenPadding(): number {
|
|
52
|
+
if (isIPadPro()) return 32;
|
|
53
|
+
if (isIPad()) return IPAD_LAYOUT_CONFIG.SCREEN_PADDING;
|
|
54
|
+
return 16;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get font scale for iPad
|
|
59
|
+
*/
|
|
60
|
+
export function getIPadFontScale(): number {
|
|
61
|
+
if (isIPadPro()) return 1.15;
|
|
62
|
+
if (isIPad()) return IPAD_LAYOUT_CONFIG.FONT_SCALE;
|
|
63
|
+
return 1.0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface IPadLayoutInfo {
|
|
67
|
+
isIPad: boolean;
|
|
68
|
+
isLandscape: boolean;
|
|
69
|
+
screenWidth: number;
|
|
70
|
+
screenHeight: number;
|
|
71
|
+
contentMaxWidth: number;
|
|
72
|
+
gridColumns: number;
|
|
73
|
+
touchTargetSize: number;
|
|
74
|
+
screenPadding: number;
|
|
75
|
+
fontScale: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get complete iPad layout information
|
|
80
|
+
*/
|
|
81
|
+
export function getIPadLayoutInfo(): IPadLayoutInfo {
|
|
82
|
+
const { width, height } = Dimensions.get('window');
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
isIPad: isIPad(),
|
|
86
|
+
isLandscape: width > height,
|
|
87
|
+
screenWidth: width,
|
|
88
|
+
screenHeight: height,
|
|
89
|
+
contentMaxWidth: getContentMaxWidth(width),
|
|
90
|
+
gridColumns: getIPadGridColumns(width),
|
|
91
|
+
touchTargetSize: getTouchTargetSize(),
|
|
92
|
+
screenPadding: getIPadScreenPadding(),
|
|
93
|
+
fontScale: getIPadFontScale(),
|
|
94
|
+
};
|
|
95
|
+
}
|