@umituz/react-native-design-system 2.3.1 → 2.3.3
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/{responsive → device/detection}/deviceDetection.ts +7 -7
- package/src/device/detection/iPadBreakpoints.ts +55 -0
- package/src/device/detection/iPadDetection.ts +48 -0
- package/src/device/detection/iPadLayoutUtils.ts +95 -0
- package/src/device/detection/iPadModalUtils.ts +98 -0
- package/src/device/detection/index.ts +54 -0
- 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 +104 -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/gridUtils.ts +1 -1
- package/src/responsive/index.ts +36 -20
- package/src/responsive/responsive.ts +2 -2
- package/src/responsive/responsiveLayout.ts +1 -1
- package/src/responsive/responsiveModal.ts +1 -1
- package/src/responsive/responsiveSizing.ts +1 -1
- package/src/responsive/useResponsive.ts +1 -1
- 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,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Utils Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { DeviceUtils, DEVICE_CONSTANTS } from '../Device';
|
|
6
|
+
|
|
7
|
+
describe('DeviceUtils', () => {
|
|
8
|
+
const mockDeviceInfo = {
|
|
9
|
+
brand: 'Apple',
|
|
10
|
+
manufacturer: 'Apple',
|
|
11
|
+
modelName: 'iPhone 14',
|
|
12
|
+
modelId: 'iPhone14,1',
|
|
13
|
+
deviceName: 'Test iPhone',
|
|
14
|
+
deviceYearClass: 2022,
|
|
15
|
+
deviceType: DEVICE_CONSTANTS.DEVICE_TYPE.PHONE,
|
|
16
|
+
isDevice: true,
|
|
17
|
+
osName: 'iOS',
|
|
18
|
+
osVersion: '16.0',
|
|
19
|
+
osBuildId: '20A362',
|
|
20
|
+
platformApiLevel: null,
|
|
21
|
+
totalMemory: 6442450944, // 6GB
|
|
22
|
+
platform: 'ios' as const,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const mockAppInfo = {
|
|
26
|
+
applicationName: 'Test App',
|
|
27
|
+
applicationId: 'com.test.app',
|
|
28
|
+
nativeApplicationVersion: '1.0.0',
|
|
29
|
+
nativeBuildVersion: '100',
|
|
30
|
+
installTime: new Date('2023-01-01'),
|
|
31
|
+
lastUpdateTime: new Date('2023-01-15'),
|
|
32
|
+
androidId: null,
|
|
33
|
+
iosIdForVendor: 'test-ios-id',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe('isPhysicalDevice', () => {
|
|
37
|
+
it('should return true for physical device', () => {
|
|
38
|
+
expect(DeviceUtils.isPhysicalDevice(true)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should return false for simulator', () => {
|
|
42
|
+
expect(DeviceUtils.isPhysicalDevice(false)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('getDeviceDisplayName', () => {
|
|
47
|
+
it('should return device name when available', () => {
|
|
48
|
+
const result = DeviceUtils.getDeviceDisplayName(mockDeviceInfo);
|
|
49
|
+
expect(result).toBe('Test iPhone');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return model name when device name is null', () => {
|
|
53
|
+
const info = { ...mockDeviceInfo, deviceName: null };
|
|
54
|
+
const result = DeviceUtils.getDeviceDisplayName(info);
|
|
55
|
+
expect(result).toBe('iPhone 14');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return brand and manufacturer when model name is null', () => {
|
|
59
|
+
const info = { ...mockDeviceInfo, deviceName: null, modelName: null };
|
|
60
|
+
const result = DeviceUtils.getDeviceDisplayName(info);
|
|
61
|
+
expect(result).toBe('Apple Apple');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should return Unknown Device when all info is null', () => {
|
|
65
|
+
const info = { ...mockDeviceInfo, deviceName: null, modelName: null, brand: null, manufacturer: null };
|
|
66
|
+
const result = DeviceUtils.getDeviceDisplayName(info);
|
|
67
|
+
expect(result).toBe('Unknown Device');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('getOSDisplayString', () => {
|
|
72
|
+
it('should return OS name and version', () => {
|
|
73
|
+
const result = DeviceUtils.getOSDisplayString(mockDeviceInfo);
|
|
74
|
+
expect(result).toBe('iOS 16.0');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should return OS name only when version is null', () => {
|
|
78
|
+
const info = { ...mockDeviceInfo, osVersion: null };
|
|
79
|
+
const result = DeviceUtils.getOSDisplayString(info);
|
|
80
|
+
expect(result).toBe('iOS');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should return Unknown OS when OS name is null', () => {
|
|
84
|
+
const info = { ...mockDeviceInfo, osName: null };
|
|
85
|
+
const result = DeviceUtils.getOSDisplayString(info);
|
|
86
|
+
expect(result).toBe('Unknown OS');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('getAppVersionString', () => {
|
|
91
|
+
it('should return version and build number', () => {
|
|
92
|
+
const result = DeviceUtils.getAppVersionString(mockAppInfo);
|
|
93
|
+
expect(result).toBe('1.0.0 (100)');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should return version only when build is null', () => {
|
|
97
|
+
const info = { ...mockAppInfo, nativeBuildVersion: null };
|
|
98
|
+
const result = DeviceUtils.getAppVersionString(info);
|
|
99
|
+
expect(result).toBe('1.0.0');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should return Unknown Version when version is null', () => {
|
|
103
|
+
const info = { ...mockAppInfo, nativeApplicationVersion: null };
|
|
104
|
+
const result = DeviceUtils.getAppVersionString(info);
|
|
105
|
+
expect(result).toBe('Unknown Version');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('meetsMinimumRequirements', () => {
|
|
110
|
+
it('should pass for device meeting all requirements', () => {
|
|
111
|
+
const result = DeviceUtils.meetsMinimumRequirements(mockDeviceInfo, 4);
|
|
112
|
+
expect(result.meets).toBe(true);
|
|
113
|
+
expect(result.reasons).toHaveLength(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should fail for simulator', () => {
|
|
117
|
+
const info = { ...mockDeviceInfo, isDevice: false };
|
|
118
|
+
const result = DeviceUtils.meetsMinimumRequirements(info, 1);
|
|
119
|
+
expect(result.meets).toBe(false);
|
|
120
|
+
expect(result.reasons).toContain('Running on simulator/emulator');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should fail for insufficient memory', () => {
|
|
124
|
+
const result = DeviceUtils.meetsMinimumRequirements(mockDeviceInfo, 8);
|
|
125
|
+
expect(result.meets).toBe(false);
|
|
126
|
+
expect(result.reasons[0]).toContain('Insufficient memory');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should fail for old device', () => {
|
|
130
|
+
const info = { ...mockDeviceInfo, deviceYearClass: 2016 };
|
|
131
|
+
const result = DeviceUtils.meetsMinimumRequirements(info, 1);
|
|
132
|
+
expect(result.meets).toBe(false);
|
|
133
|
+
expect(result.reasons[0]).toContain('Device too old: 2016');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('getDeviceTier', () => {
|
|
138
|
+
it('should return high for new device', () => {
|
|
139
|
+
const result = DeviceUtils.getDeviceTier(mockDeviceInfo);
|
|
140
|
+
expect(result).toBe('high');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should return mid for 2020 device', () => {
|
|
144
|
+
const info = { ...mockDeviceInfo, deviceYearClass: 2020 };
|
|
145
|
+
const result = DeviceUtils.getDeviceTier(info);
|
|
146
|
+
expect(result).toBe('mid');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should return low for old device', () => {
|
|
150
|
+
const info = { ...mockDeviceInfo, deviceYearClass: 2017 };
|
|
151
|
+
const result = DeviceUtils.getDeviceTier(info);
|
|
152
|
+
expect(result).toBe('low');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should classify by memory when year is null', () => {
|
|
156
|
+
const info = { ...mockDeviceInfo, deviceYearClass: null, totalMemory: 8589934592 }; // 8GB
|
|
157
|
+
const result = DeviceUtils.getDeviceTier(info);
|
|
158
|
+
expect(result).toBe('high');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should return mid when no info available', () => {
|
|
162
|
+
const info = { ...mockDeviceInfo, deviceYearClass: null, totalMemory: null };
|
|
163
|
+
const result = DeviceUtils.getDeviceTier(info);
|
|
164
|
+
expect(result).toBe('mid');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Module - Public API
|
|
3
|
+
*
|
|
4
|
+
* Complete device utilities including:
|
|
5
|
+
* - Device detection (phone, tablet, iPad variants)
|
|
6
|
+
* - Device info and capabilities
|
|
7
|
+
* - Anonymous user management
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// DETECTION - Device type and screen detection
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
// Breakpoints
|
|
16
|
+
IPAD_BREAKPOINTS,
|
|
17
|
+
TOUCH_TARGETS,
|
|
18
|
+
CONTENT_WIDTH_CONSTRAINTS,
|
|
19
|
+
IPAD_LAYOUT_CONFIG,
|
|
20
|
+
// Device type detection
|
|
21
|
+
DeviceType,
|
|
22
|
+
getScreenDimensions,
|
|
23
|
+
isSmallPhone,
|
|
24
|
+
isTablet,
|
|
25
|
+
isLandscape,
|
|
26
|
+
getDeviceType,
|
|
27
|
+
getSpacingMultiplier,
|
|
28
|
+
// iPad-specific detection
|
|
29
|
+
isIPad,
|
|
30
|
+
isIPadMini,
|
|
31
|
+
isIPadPro,
|
|
32
|
+
isIPadLandscape,
|
|
33
|
+
// iPad layout utilities
|
|
34
|
+
getContentMaxWidth,
|
|
35
|
+
getIPadGridColumns,
|
|
36
|
+
getTouchTargetSize,
|
|
37
|
+
getIPadScreenPadding,
|
|
38
|
+
getIPadFontScale,
|
|
39
|
+
getIPadLayoutInfo,
|
|
40
|
+
type IPadLayoutInfo,
|
|
41
|
+
// iPad modal utilities
|
|
42
|
+
getIPadModalDimensions,
|
|
43
|
+
getPaywallDimensions,
|
|
44
|
+
type ModalDimensions,
|
|
45
|
+
type PaywallDimensions,
|
|
46
|
+
} from './detection';
|
|
47
|
+
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// DOMAIN - Device entities and utilities
|
|
50
|
+
// ============================================================================
|
|
51
|
+
|
|
52
|
+
export type {
|
|
53
|
+
DeviceInfo,
|
|
54
|
+
ApplicationInfo,
|
|
55
|
+
SystemInfo,
|
|
56
|
+
DeviceType as DeviceInfoType,
|
|
57
|
+
} from './domain/entities/Device';
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
DEVICE_CONSTANTS,
|
|
61
|
+
DeviceUtils,
|
|
62
|
+
} from './domain/entities/Device';
|
|
63
|
+
|
|
64
|
+
export { DeviceTypeUtils } from './domain/entities/DeviceTypeUtils';
|
|
65
|
+
export { DeviceMemoryUtils } from './domain/entities/DeviceMemoryUtils';
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// INFRASTRUCTURE - Device services
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
export { DeviceService } from './infrastructure/services/DeviceService';
|
|
72
|
+
export { UserFriendlyIdService } from './infrastructure/services/UserFriendlyIdService';
|
|
73
|
+
import { PersistentDeviceIdService } from './infrastructure/services/PersistentDeviceIdService';
|
|
74
|
+
export { PersistentDeviceIdService };
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// PRESENTATION - Device hooks
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
useDeviceInfo,
|
|
82
|
+
useDeviceCapabilities,
|
|
83
|
+
useDeviceId,
|
|
84
|
+
} from './presentation/hooks/useDeviceInfo';
|
|
85
|
+
|
|
86
|
+
export {
|
|
87
|
+
useAnonymousUser,
|
|
88
|
+
} from './presentation/hooks/useAnonymousUser';
|
|
89
|
+
|
|
90
|
+
export type {
|
|
91
|
+
AnonymousUser,
|
|
92
|
+
UseAnonymousUserOptions,
|
|
93
|
+
} from './presentation/hooks/useAnonymousUser';
|
|
94
|
+
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// UTILITIES
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get anonymous user ID for services
|
|
101
|
+
*/
|
|
102
|
+
export async function getAnonymousUserId(): Promise<string> {
|
|
103
|
+
return PersistentDeviceIdService.getDeviceId();
|
|
104
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Info Service
|
|
3
|
+
*
|
|
4
|
+
* Single Responsibility: Get application information from native modules
|
|
5
|
+
* Follows SOLID principles - only handles application info retrieval
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as Application from 'expo-application';
|
|
9
|
+
import { Platform } from 'react-native';
|
|
10
|
+
import type { ApplicationInfo } from '../../domain/entities/Device';
|
|
11
|
+
import { safeAccess, withTimeout } from '../utils/nativeModuleUtils';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Service for retrieving application information
|
|
15
|
+
*/
|
|
16
|
+
export class ApplicationInfoService {
|
|
17
|
+
/**
|
|
18
|
+
* Get application information
|
|
19
|
+
* SAFE: Returns minimal info if native modules are not ready
|
|
20
|
+
*/
|
|
21
|
+
static async getApplicationInfo(): Promise<ApplicationInfo> {
|
|
22
|
+
try {
|
|
23
|
+
const [installTime, lastUpdateTime] = await Promise.all([
|
|
24
|
+
withTimeout(() => Application.getInstallationTimeAsync(), 1000),
|
|
25
|
+
withTimeout(() => Application.getLastUpdateTimeAsync(), 1000),
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const applicationName = safeAccess(() => Application.applicationName, 'Unknown');
|
|
29
|
+
const applicationId = safeAccess(() => Application.applicationId, 'Unknown');
|
|
30
|
+
const nativeApplicationVersion = safeAccess(
|
|
31
|
+
() => Application.nativeApplicationVersion,
|
|
32
|
+
null,
|
|
33
|
+
);
|
|
34
|
+
const nativeBuildVersion = safeAccess(
|
|
35
|
+
() => Application.nativeBuildVersion,
|
|
36
|
+
null,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
let androidId: string | null = null;
|
|
40
|
+
let iosIdForVendor: string | null = null;
|
|
41
|
+
|
|
42
|
+
if (Platform.OS === 'android') {
|
|
43
|
+
const result = await withTimeout(async () => Application.getAndroidId(), 1000);
|
|
44
|
+
androidId = result || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (Platform.OS === 'ios') {
|
|
48
|
+
const result = await withTimeout(
|
|
49
|
+
async () => Application.getIosIdForVendorAsync(),
|
|
50
|
+
1000,
|
|
51
|
+
);
|
|
52
|
+
iosIdForVendor = result || null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
applicationName: applicationName || 'Unknown',
|
|
57
|
+
applicationId: applicationId || 'Unknown',
|
|
58
|
+
nativeApplicationVersion,
|
|
59
|
+
nativeBuildVersion,
|
|
60
|
+
installTime,
|
|
61
|
+
lastUpdateTime,
|
|
62
|
+
androidId,
|
|
63
|
+
iosIdForVendor,
|
|
64
|
+
};
|
|
65
|
+
} catch {
|
|
66
|
+
return this.getMinimalApplicationInfo();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get minimal application info (fallback)
|
|
72
|
+
*/
|
|
73
|
+
private static getMinimalApplicationInfo(): ApplicationInfo {
|
|
74
|
+
return {
|
|
75
|
+
applicationName: 'Unknown',
|
|
76
|
+
applicationId: 'Unknown',
|
|
77
|
+
nativeApplicationVersion: null,
|
|
78
|
+
nativeBuildVersion: null,
|
|
79
|
+
installTime: null,
|
|
80
|
+
lastUpdateTime: null,
|
|
81
|
+
androidId: null,
|
|
82
|
+
iosIdForVendor: null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Capability Service
|
|
3
|
+
*
|
|
4
|
+
* Single Responsibility: Check device capabilities and features
|
|
5
|
+
* Follows SOLID principles - only handles capability checks
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as Device from 'expo-device';
|
|
9
|
+
import { Platform } from 'react-native';
|
|
10
|
+
import { DeviceInfoService } from './DeviceInfoService';
|
|
11
|
+
import { safeAccess } from '../utils/nativeModuleUtils';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Service for checking device capabilities
|
|
15
|
+
*/
|
|
16
|
+
export class DeviceCapabilityService {
|
|
17
|
+
/**
|
|
18
|
+
* Check if device supports specific features
|
|
19
|
+
*/
|
|
20
|
+
static async getDeviceCapabilities(): Promise<{
|
|
21
|
+
isDevice: boolean;
|
|
22
|
+
isTablet: boolean;
|
|
23
|
+
hasNotch: boolean;
|
|
24
|
+
totalMemoryGB: number | null;
|
|
25
|
+
}> {
|
|
26
|
+
const info = await DeviceInfoService.getDeviceInfo();
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
isDevice: info.isDevice,
|
|
30
|
+
isTablet: info.deviceType === Device.DeviceType.TABLET,
|
|
31
|
+
hasNotch: await this.hasNotch(),
|
|
32
|
+
totalMemoryGB: info.totalMemory
|
|
33
|
+
? info.totalMemory / (1024 * 1024 * 1024)
|
|
34
|
+
: null,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if device has notch/dynamic island
|
|
40
|
+
*/
|
|
41
|
+
static async hasNotch(): Promise<boolean> {
|
|
42
|
+
try {
|
|
43
|
+
if (Platform.OS !== 'ios') {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const modelName = safeAccess(() => Device.modelName?.toLowerCase() || '', '');
|
|
48
|
+
|
|
49
|
+
// iPhone X and newer (with notch or dynamic island)
|
|
50
|
+
return (
|
|
51
|
+
modelName.includes('iphone x') ||
|
|
52
|
+
modelName.includes('iphone 1') || // 11, 12, 13, 14, 15
|
|
53
|
+
modelName.includes('pro')
|
|
54
|
+
);
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device ID Service
|
|
3
|
+
*
|
|
4
|
+
* Single Responsibility: Get device unique identifiers
|
|
5
|
+
* Follows SOLID principles - only handles device ID retrieval
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as Application from 'expo-application';
|
|
9
|
+
import { Platform } from 'react-native';
|
|
10
|
+
import { withTimeout } from '../utils/nativeModuleUtils';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Service for retrieving device unique identifiers
|
|
14
|
+
*/
|
|
15
|
+
export class DeviceIdService {
|
|
16
|
+
/**
|
|
17
|
+
* Get device unique identifier (platform-specific)
|
|
18
|
+
*
|
|
19
|
+
* WARNING: Use with caution - user privacy considerations!
|
|
20
|
+
* Android: androidId (can be reset)
|
|
21
|
+
* iOS: iosIdForVendor (changes on reinstall)
|
|
22
|
+
* Web: null (not supported)
|
|
23
|
+
*
|
|
24
|
+
* SAFE: Returns null if native modules are not ready
|
|
25
|
+
*/
|
|
26
|
+
static async getDeviceId(): Promise<string | null> {
|
|
27
|
+
try {
|
|
28
|
+
let deviceId: string | null = null;
|
|
29
|
+
|
|
30
|
+
if (Platform.OS === 'android') {
|
|
31
|
+
const result = await withTimeout(async () => Application.getAndroidId(), 1000);
|
|
32
|
+
deviceId = result || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (Platform.OS === 'ios') {
|
|
36
|
+
const result = await withTimeout(
|
|
37
|
+
async () => Application.getIosIdForVendorAsync(),
|
|
38
|
+
1000,
|
|
39
|
+
);
|
|
40
|
+
deviceId = result || null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return deviceId;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get offline user ID with fallback (iOS/Android only)
|
|
51
|
+
*
|
|
52
|
+
* For offline apps that need a persistent user ID:
|
|
53
|
+
* 1. Try to get platform-specific device ID (iOS/Android)
|
|
54
|
+
* 2. Fallback to a default offline user ID if device ID not available
|
|
55
|
+
*
|
|
56
|
+
* NOTE: Returns null for web platform (not supported)
|
|
57
|
+
*/
|
|
58
|
+
static async getOfflineUserId(fallbackId: string = 'offline_user'): Promise<string | null> {
|
|
59
|
+
if (Platform.OS === 'web') {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const deviceId = await this.getDeviceId();
|
|
64
|
+
if (deviceId) {
|
|
65
|
+
return `offline_${deviceId}`;
|
|
66
|
+
}
|
|
67
|
+
return fallbackId;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Info Service
|
|
3
|
+
*
|
|
4
|
+
* Single Responsibility: Get device information from native modules
|
|
5
|
+
* Follows SOLID principles - only handles device info retrieval
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as Device from 'expo-device';
|
|
9
|
+
import { Platform } from 'react-native';
|
|
10
|
+
import * as Localization from 'expo-localization';
|
|
11
|
+
import type { DeviceInfo } from '../../domain/entities/Device';
|
|
12
|
+
import { safeAccess, withTimeout } from '../utils/nativeModuleUtils';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Service for retrieving device information
|
|
16
|
+
*/
|
|
17
|
+
export class DeviceInfoService {
|
|
18
|
+
/**
|
|
19
|
+
* Get device information
|
|
20
|
+
* SAFE: Returns minimal info if native modules are not ready
|
|
21
|
+
*/
|
|
22
|
+
static async getDeviceInfo(): Promise<DeviceInfo> {
|
|
23
|
+
try {
|
|
24
|
+
const totalMemory = await withTimeout(
|
|
25
|
+
() => Device.getMaxMemoryAsync(),
|
|
26
|
+
1000,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const brand = safeAccess(() => Device.brand, null);
|
|
30
|
+
const manufacturer = safeAccess(() => Device.manufacturer, null);
|
|
31
|
+
const modelName = safeAccess(() => Device.modelName, null);
|
|
32
|
+
const modelId = safeAccess(() => Device.modelId, null);
|
|
33
|
+
const deviceName = safeAccess(() => Device.deviceName, null);
|
|
34
|
+
const deviceYearClass = safeAccess(() => Device.deviceYearClass, null);
|
|
35
|
+
const deviceType = safeAccess(() => Device.deviceType, null);
|
|
36
|
+
const isDevice = safeAccess(() => Device.isDevice, false);
|
|
37
|
+
const osName = safeAccess(() => Device.osName, null);
|
|
38
|
+
const osVersion = safeAccess(() => Device.osVersion, null);
|
|
39
|
+
const osBuildId = safeAccess(() => Device.osBuildId, null);
|
|
40
|
+
const platformApiLevel = safeAccess(() => Device.platformApiLevel, null);
|
|
41
|
+
|
|
42
|
+
// Localization
|
|
43
|
+
const calendars = Localization.getCalendars();
|
|
44
|
+
const locales = Localization.getLocales();
|
|
45
|
+
const timezone = calendars?.[0]?.timeZone ?? null;
|
|
46
|
+
const region = locales?.[0]?.regionCode ?? null;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
brand,
|
|
50
|
+
manufacturer,
|
|
51
|
+
modelName,
|
|
52
|
+
modelId,
|
|
53
|
+
deviceName,
|
|
54
|
+
deviceYearClass,
|
|
55
|
+
deviceType,
|
|
56
|
+
isDevice,
|
|
57
|
+
osName,
|
|
58
|
+
osVersion,
|
|
59
|
+
osBuildId,
|
|
60
|
+
platformApiLevel,
|
|
61
|
+
totalMemory,
|
|
62
|
+
platform: Platform.OS as 'ios' | 'android' | 'web',
|
|
63
|
+
timezone,
|
|
64
|
+
region,
|
|
65
|
+
};
|
|
66
|
+
} catch {
|
|
67
|
+
return this.getMinimalDeviceInfo();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get minimal device info (fallback)
|
|
73
|
+
*/
|
|
74
|
+
private static getMinimalDeviceInfo(): DeviceInfo {
|
|
75
|
+
return {
|
|
76
|
+
brand: null,
|
|
77
|
+
manufacturer: null,
|
|
78
|
+
modelName: null,
|
|
79
|
+
modelId: null,
|
|
80
|
+
deviceName: null,
|
|
81
|
+
deviceYearClass: null,
|
|
82
|
+
deviceType: null,
|
|
83
|
+
isDevice: false,
|
|
84
|
+
osName: null,
|
|
85
|
+
osVersion: null,
|
|
86
|
+
osBuildId: null,
|
|
87
|
+
platformApiLevel: null,
|
|
88
|
+
totalMemory: null,
|
|
89
|
+
platform: Platform.OS as 'ios' | 'android' | 'web',
|
|
90
|
+
timezone: null,
|
|
91
|
+
region: null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Service - Facade/Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Main entry point for device operations.
|
|
5
|
+
* Delegates to specialized services following Single Responsibility Principle.
|
|
6
|
+
*
|
|
7
|
+
* @domain device
|
|
8
|
+
* @layer infrastructure/services
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { DeviceInfo, ApplicationInfo, SystemInfo } from '../../domain/entities/Device';
|
|
12
|
+
import { DeviceInfoService } from './DeviceInfoService';
|
|
13
|
+
import { ApplicationInfoService } from './ApplicationInfoService';
|
|
14
|
+
import { DeviceIdService } from './DeviceIdService';
|
|
15
|
+
import { DeviceCapabilityService } from './DeviceCapabilityService';
|
|
16
|
+
import { UserFriendlyIdService } from './UserFriendlyIdService';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Device Service - Facade for all device operations
|
|
20
|
+
*
|
|
21
|
+
* This is a facade that delegates to specialized services.
|
|
22
|
+
* Each service has a single responsibility (SOLID principles).
|
|
23
|
+
*/
|
|
24
|
+
export class DeviceService {
|
|
25
|
+
/**
|
|
26
|
+
* Get device information
|
|
27
|
+
* Delegates to DeviceInfoService
|
|
28
|
+
*/
|
|
29
|
+
static async getDeviceInfo(): Promise<DeviceInfo> {
|
|
30
|
+
return DeviceInfoService.getDeviceInfo();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get application information
|
|
35
|
+
* Delegates to ApplicationInfoService
|
|
36
|
+
*/
|
|
37
|
+
static async getApplicationInfo(): Promise<ApplicationInfo> {
|
|
38
|
+
return ApplicationInfoService.getApplicationInfo();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get complete system information (device + app)
|
|
43
|
+
* @param options - Optional configuration
|
|
44
|
+
* @param options.userId - Optional user ID to include in system info
|
|
45
|
+
*/
|
|
46
|
+
static async getSystemInfo(options?: { userId?: string }): Promise<SystemInfo> {
|
|
47
|
+
const [device, application] = await Promise.all([
|
|
48
|
+
DeviceInfoService.getDeviceInfo(),
|
|
49
|
+
ApplicationInfoService.getApplicationInfo(),
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
device,
|
|
54
|
+
application,
|
|
55
|
+
timestamp: Date.now(),
|
|
56
|
+
...(options?.userId && { userId: options.userId }),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get device unique identifier (platform-specific)
|
|
62
|
+
* Delegates to DeviceIdService
|
|
63
|
+
*/
|
|
64
|
+
static async getDeviceId(): Promise<string | null> {
|
|
65
|
+
return DeviceIdService.getDeviceId();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get offline user ID with fallback
|
|
70
|
+
* Delegates to DeviceIdService
|
|
71
|
+
*/
|
|
72
|
+
static async getOfflineUserId(fallbackId: string = 'offline_user'): Promise<string | null> {
|
|
73
|
+
return DeviceIdService.getOfflineUserId(fallbackId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if device supports specific features
|
|
78
|
+
* Delegates to DeviceCapabilityService
|
|
79
|
+
*/
|
|
80
|
+
static async getDeviceCapabilities(): Promise<{
|
|
81
|
+
isDevice: boolean;
|
|
82
|
+
isTablet: boolean;
|
|
83
|
+
hasNotch: boolean;
|
|
84
|
+
totalMemoryGB: number | null;
|
|
85
|
+
}> {
|
|
86
|
+
return DeviceCapabilityService.getDeviceCapabilities();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if device has notch/dynamic island
|
|
91
|
+
* Delegates to DeviceCapabilityService
|
|
92
|
+
*/
|
|
93
|
+
static async hasNotch(): Promise<boolean> {
|
|
94
|
+
return DeviceCapabilityService.hasNotch();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get user friendly device ID
|
|
99
|
+
* Delegates to UserFriendlyIdService
|
|
100
|
+
*/
|
|
101
|
+
static async getUserFriendlyId(): Promise<string> {
|
|
102
|
+
return UserFriendlyIdService.getUserFriendlyId();
|
|
103
|
+
}
|
|
104
|
+
}
|