@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,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent Device ID Service
|
|
3
|
+
*
|
|
4
|
+
* Provides a stable, persistent device identifier that survives app restarts.
|
|
5
|
+
* Uses native device ID when available, falls back to generated UUID.
|
|
6
|
+
* Stores the ID in AsyncStorage for persistence.
|
|
7
|
+
*
|
|
8
|
+
* @domain device
|
|
9
|
+
* @layer infrastructure/services
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
13
|
+
import { DeviceIdService } from './DeviceIdService';
|
|
14
|
+
|
|
15
|
+
const STORAGE_KEY = '@device/persistent_id';
|
|
16
|
+
|
|
17
|
+
/** Cached ID to avoid repeated AsyncStorage reads */
|
|
18
|
+
let cachedDeviceId: string | null = null;
|
|
19
|
+
|
|
20
|
+
/** Promise to prevent race conditions during initialization */
|
|
21
|
+
let initializationPromise: Promise<string> | null = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate a UUID v4 without external dependencies
|
|
25
|
+
*/
|
|
26
|
+
function generateUUID(): string {
|
|
27
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
28
|
+
const r = (Math.random() * 16) | 0;
|
|
29
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
30
|
+
return v.toString(16);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Service for managing persistent device identifiers
|
|
36
|
+
*/
|
|
37
|
+
export class PersistentDeviceIdService {
|
|
38
|
+
/**
|
|
39
|
+
* Get or create a persistent device ID
|
|
40
|
+
*
|
|
41
|
+
* This method:
|
|
42
|
+
* 1. Returns cached ID if available (fastest)
|
|
43
|
+
* 2. Checks AsyncStorage for previously stored ID
|
|
44
|
+
* 3. If not found, gets native device ID or generates UUID
|
|
45
|
+
* 4. Stores and caches the result for future calls
|
|
46
|
+
*
|
|
47
|
+
* Thread-safe: Multiple concurrent calls return the same promise
|
|
48
|
+
*/
|
|
49
|
+
static async getDeviceId(): Promise<string> {
|
|
50
|
+
if (cachedDeviceId) {
|
|
51
|
+
return cachedDeviceId;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (initializationPromise) {
|
|
55
|
+
return initializationPromise;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
initializationPromise = this.initializeDeviceId();
|
|
59
|
+
return initializationPromise;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Initialize and persist device ID
|
|
64
|
+
*/
|
|
65
|
+
private static async initializeDeviceId(): Promise<string> {
|
|
66
|
+
try {
|
|
67
|
+
const storedId = await AsyncStorage.getItem(STORAGE_KEY);
|
|
68
|
+
|
|
69
|
+
if (storedId) {
|
|
70
|
+
cachedDeviceId = storedId;
|
|
71
|
+
return storedId;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const newId = await this.createNewDeviceId();
|
|
75
|
+
await AsyncStorage.setItem(STORAGE_KEY, newId);
|
|
76
|
+
cachedDeviceId = newId;
|
|
77
|
+
|
|
78
|
+
return newId;
|
|
79
|
+
} catch {
|
|
80
|
+
const fallbackId = generateUUID();
|
|
81
|
+
cachedDeviceId = fallbackId;
|
|
82
|
+
return fallbackId;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a new device ID from native source or generate one
|
|
88
|
+
*/
|
|
89
|
+
private static async createNewDeviceId(): Promise<string> {
|
|
90
|
+
const nativeId = await DeviceIdService.getDeviceId();
|
|
91
|
+
|
|
92
|
+
if (nativeId) {
|
|
93
|
+
return `device_${nativeId}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return `generated_${generateUUID()}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if device ID exists in storage
|
|
101
|
+
*/
|
|
102
|
+
static async hasStoredId(): Promise<boolean> {
|
|
103
|
+
try {
|
|
104
|
+
const storedId = await AsyncStorage.getItem(STORAGE_KEY);
|
|
105
|
+
return storedId !== null;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Clear stored device ID (use with caution)
|
|
113
|
+
* This will generate a new ID on next getDeviceId() call
|
|
114
|
+
*/
|
|
115
|
+
static async clearStoredId(): Promise<void> {
|
|
116
|
+
try {
|
|
117
|
+
await AsyncStorage.removeItem(STORAGE_KEY);
|
|
118
|
+
cachedDeviceId = null;
|
|
119
|
+
initializationPromise = null;
|
|
120
|
+
} catch {
|
|
121
|
+
// Silent fail - non-critical operation
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get the cached device ID without async operation
|
|
127
|
+
* Returns null if not yet initialized
|
|
128
|
+
*/
|
|
129
|
+
static getCachedId(): string | null {
|
|
130
|
+
return cachedDeviceId;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Friendly ID Service
|
|
3
|
+
*
|
|
4
|
+
* Single Responsibility: Generate user-friendly device identifiers
|
|
5
|
+
* Follows SOLID principles - only handles ID generation logic
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Platform } from 'react-native';
|
|
9
|
+
import type { DeviceInfo } from '../../domain/entities/Device';
|
|
10
|
+
import { DeviceInfoService } from './DeviceInfoService';
|
|
11
|
+
import { DeviceIdService } from './DeviceIdService';
|
|
12
|
+
import {
|
|
13
|
+
cleanModelName,
|
|
14
|
+
extractIdPart,
|
|
15
|
+
generateRandomId,
|
|
16
|
+
getPlatformPrefix,
|
|
17
|
+
} from '../utils/stringUtils';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Service for generating user-friendly device IDs
|
|
21
|
+
*/
|
|
22
|
+
export class UserFriendlyIdService {
|
|
23
|
+
/**
|
|
24
|
+
* Get user friendly device ID (e.g. "iPhone13-A8F2")
|
|
25
|
+
*
|
|
26
|
+
* Useful for displaying a readable user identifier in profiles.
|
|
27
|
+
* Combines cleaned model name with short device hash.
|
|
28
|
+
*
|
|
29
|
+
* SAFE: This method has multiple fallback layers to prevent native module crashes.
|
|
30
|
+
* If native modules are not ready, it will return a safe fallback ID.
|
|
31
|
+
*/
|
|
32
|
+
static async getUserFriendlyId(): Promise<string> {
|
|
33
|
+
// Web platform - no native modules needed
|
|
34
|
+
if (Platform.OS === 'web') {
|
|
35
|
+
return `WebUser-${generateRandomId()}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Try to get device info and ID
|
|
40
|
+
const deviceInfo: DeviceInfo = await DeviceInfoService.getDeviceInfo();
|
|
41
|
+
const deviceId: string | null = await DeviceIdService.getDeviceId();
|
|
42
|
+
|
|
43
|
+
// If we got device info, use it
|
|
44
|
+
if (deviceInfo && (deviceInfo.modelName || deviceInfo.deviceName)) {
|
|
45
|
+
const model = deviceInfo.modelName || deviceInfo.deviceName || 'Device';
|
|
46
|
+
const cleanModel = cleanModelName(model);
|
|
47
|
+
const idPart = extractIdPart(deviceId, 6);
|
|
48
|
+
|
|
49
|
+
return `${cleanModel}-${idPart}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fallback: Use platform + random ID
|
|
53
|
+
return this.generateFallbackId();
|
|
54
|
+
} catch {
|
|
55
|
+
// Final fallback: Generate safe random ID
|
|
56
|
+
return this.generateFallbackId();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generate fallback ID when native modules are not available
|
|
62
|
+
*/
|
|
63
|
+
private static generateFallbackId(): string {
|
|
64
|
+
const platformPrefix = getPlatformPrefix(Platform.OS);
|
|
65
|
+
return `${platformPrefix}-${generateRandomId()}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Module Utils Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { withTimeout, safeAccess, withTimeoutAll } from '../nativeModuleUtils';
|
|
6
|
+
|
|
7
|
+
describe('Native Module Utils', () => {
|
|
8
|
+
describe('withTimeout', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.useFakeTimers();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
jest.useRealTimers();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should resolve with operation result when successful', async () => {
|
|
18
|
+
const operation = jest.fn().mockResolvedValue('success');
|
|
19
|
+
const promise = withTimeout(operation, 1000);
|
|
20
|
+
|
|
21
|
+
jest.advanceTimersByTime(0);
|
|
22
|
+
await expect(promise).resolves.toBe('success');
|
|
23
|
+
expect(operation).toHaveBeenCalledTimes(1);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should return null when operation times out', async () => {
|
|
27
|
+
const operation = jest.fn().mockImplementation(() => new Promise(() => {}));
|
|
28
|
+
const promise = withTimeout(operation, 1000);
|
|
29
|
+
|
|
30
|
+
jest.advanceTimersByTime(1000);
|
|
31
|
+
await expect(promise).resolves.toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return null when operation throws', async () => {
|
|
35
|
+
const operation = jest.fn().mockRejectedValue(new Error('Test error'));
|
|
36
|
+
const promise = withTimeout(operation, 1000);
|
|
37
|
+
|
|
38
|
+
jest.advanceTimersByTime(0);
|
|
39
|
+
await expect(promise).resolves.toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should use default timeout when not specified', async () => {
|
|
43
|
+
const operation = jest.fn().mockImplementation(() => new Promise(() => {}));
|
|
44
|
+
const promise = withTimeout(operation);
|
|
45
|
+
|
|
46
|
+
jest.advanceTimersByTime(1000);
|
|
47
|
+
await expect(promise).resolves.toBeNull();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('safeAccess', () => {
|
|
52
|
+
it('should return value when accessor succeeds', () => {
|
|
53
|
+
const accessor = () => 'test value';
|
|
54
|
+
const result = safeAccess(accessor, 'fallback');
|
|
55
|
+
expect(result).toBe('test value');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return fallback when accessor throws', () => {
|
|
59
|
+
const accessor = () => {
|
|
60
|
+
throw new Error('Test error');
|
|
61
|
+
};
|
|
62
|
+
const result = safeAccess(accessor, 'fallback');
|
|
63
|
+
expect(result).toBe('fallback');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should return fallback when accessor returns null/undefined', () => {
|
|
67
|
+
const accessor1 = () => null;
|
|
68
|
+
const accessor2 = () => undefined;
|
|
69
|
+
|
|
70
|
+
expect(safeAccess(accessor1, 'fallback')).toBe('fallback');
|
|
71
|
+
expect(safeAccess(accessor2, 'fallback')).toBe('fallback');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should return value when accessor returns falsy but not null/undefined', () => {
|
|
75
|
+
const accessor1 = () => '';
|
|
76
|
+
const accessor2 = () => 0;
|
|
77
|
+
const accessor3 = () => false;
|
|
78
|
+
|
|
79
|
+
expect(safeAccess(accessor1, 'fallback')).toBe('');
|
|
80
|
+
expect(safeAccess(accessor2, 'fallback')).toBe(0);
|
|
81
|
+
expect(safeAccess(accessor3, 'fallback')).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('withTimeoutAll', () => {
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
jest.useFakeTimers();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
jest.useRealTimers();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should resolve all operations when successful', async () => {
|
|
95
|
+
const operations = [
|
|
96
|
+
jest.fn().mockResolvedValue('result1'),
|
|
97
|
+
jest.fn().mockResolvedValue('result2'),
|
|
98
|
+
jest.fn().mockResolvedValue('result3'),
|
|
99
|
+
];
|
|
100
|
+
const promise = withTimeoutAll(operations, 1000);
|
|
101
|
+
|
|
102
|
+
jest.advanceTimersByTime(0);
|
|
103
|
+
const results = await promise;
|
|
104
|
+
|
|
105
|
+
expect(results).toEqual(['result1', 'result2', 'result3']);
|
|
106
|
+
operations.forEach(op => expect(op).toHaveBeenCalledTimes(1));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle individual operation failures', async () => {
|
|
110
|
+
const operations = [
|
|
111
|
+
jest.fn().mockResolvedValue('result1'),
|
|
112
|
+
jest.fn().mockRejectedValue(new Error('Test error')),
|
|
113
|
+
jest.fn().mockResolvedValue('result3'),
|
|
114
|
+
];
|
|
115
|
+
const promise = withTimeoutAll(operations, 1000);
|
|
116
|
+
|
|
117
|
+
jest.advanceTimersByTime(0);
|
|
118
|
+
const results = await promise;
|
|
119
|
+
|
|
120
|
+
expect(results).toEqual(['result1', null, 'result3']);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should return null for all operations when timeout occurs', async () => {
|
|
124
|
+
const operations = [
|
|
125
|
+
jest.fn().mockImplementation(() => new Promise(() => {})),
|
|
126
|
+
jest.fn().mockResolvedValue('result2'),
|
|
127
|
+
];
|
|
128
|
+
const promise = withTimeoutAll(operations, 1000);
|
|
129
|
+
|
|
130
|
+
jest.advanceTimersByTime(1000);
|
|
131
|
+
const results = await promise;
|
|
132
|
+
|
|
133
|
+
expect(results).toEqual([null, null]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should use default timeout when not specified', async () => {
|
|
137
|
+
const operations = [
|
|
138
|
+
jest.fn().mockImplementation(() => new Promise(() => {})),
|
|
139
|
+
];
|
|
140
|
+
const promise = withTimeoutAll(operations);
|
|
141
|
+
|
|
142
|
+
jest.advanceTimersByTime(2000);
|
|
143
|
+
const results = await promise;
|
|
144
|
+
|
|
145
|
+
expect(results).toEqual([null]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should handle empty operations array', async () => {
|
|
149
|
+
const operations: Array<() => Promise<string>> = [];
|
|
150
|
+
const promise = withTimeoutAll(operations, 1000);
|
|
151
|
+
|
|
152
|
+
jest.advanceTimersByTime(0);
|
|
153
|
+
const results = await promise;
|
|
154
|
+
|
|
155
|
+
expect(results).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* String Utils Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { cleanModelName, extractIdPart, generateRandomId, getPlatformPrefix } from '../stringUtils';
|
|
6
|
+
|
|
7
|
+
describe('String Utils', () => {
|
|
8
|
+
describe('cleanModelName', () => {
|
|
9
|
+
it('should remove special characters from model name', () => {
|
|
10
|
+
const result = cleanModelName('iPhone 14 Pro Max');
|
|
11
|
+
expect(result).toBe('iPhone14ProMax');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should handle null/undefined input', () => {
|
|
15
|
+
expect(cleanModelName(null)).toBe('Device');
|
|
16
|
+
expect(cleanModelName(undefined)).toBe('Device');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should handle empty string', () => {
|
|
20
|
+
const result = cleanModelName('');
|
|
21
|
+
expect(result).toBe('Device');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should handle model with only special characters', () => {
|
|
25
|
+
const result = cleanModelName('!@#$%^&*()');
|
|
26
|
+
expect(result).toBe('Device');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should preserve alphanumeric characters', () => {
|
|
30
|
+
const result = cleanModelName('SM-G998B');
|
|
31
|
+
expect(result).toBe('SMG998B');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('extractIdPart', () => {
|
|
36
|
+
it('should extract last N characters from device ID', () => {
|
|
37
|
+
const deviceId = '12345678-1234-1234-1234-123456789012';
|
|
38
|
+
const result = extractIdPart(deviceId, 6);
|
|
39
|
+
expect(result).toBe('789012');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should use default length of 6 when not specified', () => {
|
|
43
|
+
const deviceId = '12345678-1234-1234-1234-123456789012';
|
|
44
|
+
const result = extractIdPart(deviceId);
|
|
45
|
+
expect(result).toBe('789012');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should handle null device ID by generating random ID', () => {
|
|
49
|
+
const result = extractIdPart(null, 6);
|
|
50
|
+
expect(result).toMatch(/^[A-Z0-9]{6}$/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should handle empty device ID by generating random ID', () => {
|
|
54
|
+
const result = extractIdPart('', 6);
|
|
55
|
+
expect(result).toMatch(/^[A-Z0-9]{6}$/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should handle device ID shorter than requested length', () => {
|
|
59
|
+
const deviceId = 'ABC';
|
|
60
|
+
const result = extractIdPart(deviceId, 6);
|
|
61
|
+
expect(result).toBe('ABC');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should convert to uppercase', () => {
|
|
65
|
+
const deviceId = 'abcdef';
|
|
66
|
+
const result = extractIdPart(deviceId, 3);
|
|
67
|
+
expect(result).toBe('DEF');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('generateRandomId', () => {
|
|
72
|
+
it('should generate random ID with default length', () => {
|
|
73
|
+
const result = generateRandomId();
|
|
74
|
+
expect(result).toMatch(/^[A-Z0-9]{6}$/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should generate random ID with specified length', () => {
|
|
78
|
+
const result = generateRandomId(10);
|
|
79
|
+
expect(result).toMatch(/^[A-Z0-9]{10}$/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should generate different IDs on multiple calls', () => {
|
|
83
|
+
const id1 = generateRandomId(6);
|
|
84
|
+
const id2 = generateRandomId(6);
|
|
85
|
+
expect(id1).not.toBe(id2);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should handle length of 1', () => {
|
|
89
|
+
const result = generateRandomId(1);
|
|
90
|
+
expect(result).toMatch(/^[A-Z0-9]{1}$/);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('getPlatformPrefix', () => {
|
|
95
|
+
it('should return iOS for ios platform', () => {
|
|
96
|
+
const result = getPlatformPrefix('ios');
|
|
97
|
+
expect(result).toBe('iOS');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should return Android for android platform', () => {
|
|
101
|
+
const result = getPlatformPrefix('android');
|
|
102
|
+
expect(result).toBe('Android');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should return Device for unknown platform', () => {
|
|
106
|
+
const result = getPlatformPrefix('windows');
|
|
107
|
+
expect(result).toBe('Device');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should return Device for empty string', () => {
|
|
111
|
+
const result = getPlatformPrefix('');
|
|
112
|
+
expect(result).toBe('Device');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should be case sensitive', () => {
|
|
116
|
+
expect(getPlatformPrefix('IOS')).toBe('Device');
|
|
117
|
+
expect(getPlatformPrefix('ANDROID')).toBe('Device');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Module Utilities
|
|
3
|
+
*
|
|
4
|
+
* Safe wrappers for native module access with timeout and error handling
|
|
5
|
+
* Prevents crashes when native modules are not ready
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Execute a native module call with timeout
|
|
10
|
+
* @param operation - Async operation to execute
|
|
11
|
+
* @param timeoutMs - Timeout in milliseconds (default: 1000)
|
|
12
|
+
* @returns Result of operation or null if timeout/error
|
|
13
|
+
*/
|
|
14
|
+
export async function withTimeout<T>(
|
|
15
|
+
operation: () => Promise<T>,
|
|
16
|
+
timeoutMs: number = 1000,
|
|
17
|
+
): Promise<T | null> {
|
|
18
|
+
try {
|
|
19
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
20
|
+
setTimeout(() => reject(new Error('Operation timeout')), timeoutMs);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return await Promise.race([operation(), timeoutPromise]);
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Safely access a native module property
|
|
31
|
+
* @param accessor - Function that accesses the property
|
|
32
|
+
* @param fallback - Fallback value if access fails
|
|
33
|
+
* @returns Property value or fallback
|
|
34
|
+
*/
|
|
35
|
+
export function safeAccess<T>(accessor: () => T, fallback: T): T {
|
|
36
|
+
try {
|
|
37
|
+
const value = accessor();
|
|
38
|
+
return value ?? fallback;
|
|
39
|
+
} catch {
|
|
40
|
+
return fallback;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Execute multiple native module operations with timeout
|
|
46
|
+
* @param operations - Array of async operations
|
|
47
|
+
* @param timeoutMs - Timeout in milliseconds (default: 2000)
|
|
48
|
+
* @returns Array of results (null for failed operations)
|
|
49
|
+
*/
|
|
50
|
+
export async function withTimeoutAll<T>(
|
|
51
|
+
operations: Array<() => Promise<T>>,
|
|
52
|
+
timeoutMs: number = 2000,
|
|
53
|
+
): Promise<Array<T | null>> {
|
|
54
|
+
try {
|
|
55
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
56
|
+
setTimeout(() => reject(new Error('Operations timeout')), timeoutMs);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const results = await Promise.race([
|
|
60
|
+
Promise.all(operations.map((op) => op().catch(() => null))),
|
|
61
|
+
timeoutPromise,
|
|
62
|
+
]).catch(() => operations.map(() => null));
|
|
63
|
+
|
|
64
|
+
return results as Array<T | null>;
|
|
65
|
+
} catch {
|
|
66
|
+
return operations.map(() => null);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* String Utilities
|
|
3
|
+
*
|
|
4
|
+
* Pure utility functions for string manipulation
|
|
5
|
+
* No dependencies, no side effects
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Clean model name by removing special characters
|
|
10
|
+
* @param model - Device model name
|
|
11
|
+
* @returns Cleaned model name with only alphanumeric characters
|
|
12
|
+
*/
|
|
13
|
+
export function cleanModelName(model: string | null | undefined): string {
|
|
14
|
+
if (!model) {
|
|
15
|
+
return 'Device';
|
|
16
|
+
}
|
|
17
|
+
const cleaned = model.replace(/[^a-zA-Z0-9]/g, '');
|
|
18
|
+
return cleaned || 'Device';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extract ID part from device ID
|
|
23
|
+
* @param deviceId - Full device ID
|
|
24
|
+
* @param length - Length of ID part to extract (default: 6)
|
|
25
|
+
* @returns Last N characters of device ID in uppercase
|
|
26
|
+
*/
|
|
27
|
+
export function extractIdPart(deviceId: string | null, length: number = 6): string {
|
|
28
|
+
if (!deviceId) {
|
|
29
|
+
return generateRandomId(length);
|
|
30
|
+
}
|
|
31
|
+
const start = Math.max(0, deviceId.length - length);
|
|
32
|
+
return deviceId.substring(start).toUpperCase();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate random alphanumeric ID
|
|
37
|
+
* @param length - Length of ID to generate (default: 6)
|
|
38
|
+
* @returns Random ID in uppercase
|
|
39
|
+
*/
|
|
40
|
+
export function generateRandomId(length: number = 6): string {
|
|
41
|
+
return Math.random().toString(36).substring(2, 2 + length).toUpperCase();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get platform prefix for device ID
|
|
46
|
+
* @param platform - Platform OS
|
|
47
|
+
* @returns Platform prefix string
|
|
48
|
+
*/
|
|
49
|
+
export function getPlatformPrefix(platform: string): string {
|
|
50
|
+
switch (platform) {
|
|
51
|
+
case 'ios':
|
|
52
|
+
return 'iOS';
|
|
53
|
+
case 'android':
|
|
54
|
+
return 'Android';
|
|
55
|
+
default:
|
|
56
|
+
return 'Device';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|