@umituz/react-native-design-system 2.3.0 → 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.
Files changed (50) hide show
  1. package/package.json +16 -4
  2. package/src/atoms/AtomicBadge.tsx +121 -0
  3. package/src/atoms/AtomicInput.tsx +0 -1
  4. package/src/atoms/AtomicPicker.tsx +0 -1
  5. package/src/atoms/index.ts +8 -0
  6. package/src/atoms/picker/components/PickerChips.tsx +0 -1
  7. package/src/atoms/picker/components/PickerModal.tsx +1 -3
  8. package/src/atoms/picker/styles/pickerStyles.ts +1 -1
  9. package/src/device/domain/entities/Device.ts +207 -0
  10. package/src/device/domain/entities/DeviceMemoryUtils.ts +62 -0
  11. package/src/device/domain/entities/DeviceTypeUtils.ts +66 -0
  12. package/src/device/domain/entities/__tests__/DeviceMemoryUtils.test.ts +118 -0
  13. package/src/device/domain/entities/__tests__/DeviceTypeUtils.test.ts +104 -0
  14. package/src/device/domain/entities/__tests__/DeviceUtils.test.ts +167 -0
  15. package/src/device/index.ts +51 -0
  16. package/src/device/infrastructure/services/ApplicationInfoService.ts +86 -0
  17. package/src/device/infrastructure/services/DeviceCapabilityService.ts +60 -0
  18. package/src/device/infrastructure/services/DeviceIdService.ts +70 -0
  19. package/src/device/infrastructure/services/DeviceInfoService.ts +95 -0
  20. package/src/device/infrastructure/services/DeviceService.ts +104 -0
  21. package/src/device/infrastructure/services/PersistentDeviceIdService.ts +132 -0
  22. package/src/device/infrastructure/services/UserFriendlyIdService.ts +68 -0
  23. package/src/device/infrastructure/utils/__tests__/nativeModuleUtils.test.ts +158 -0
  24. package/src/device/infrastructure/utils/__tests__/stringUtils.test.ts +120 -0
  25. package/src/device/infrastructure/utils/nativeModuleUtils.ts +69 -0
  26. package/src/device/infrastructure/utils/stringUtils.ts +59 -0
  27. package/src/device/presentation/hooks/useAnonymousUser.ts +117 -0
  28. package/src/device/presentation/hooks/useDeviceInfo.ts +222 -0
  29. package/src/index.ts +4 -0
  30. package/src/molecules/ConfirmationModalContent.tsx +4 -4
  31. package/src/molecules/ConfirmationModalMain.tsx +1 -1
  32. package/src/molecules/ScreenHeader.tsx +2 -2
  33. package/src/molecules/confirmation-modal/components.tsx +1 -1
  34. package/src/molecules/confirmation-modal/styles/confirmationModalStyles.ts +6 -7
  35. package/src/presentation/utils/variants/__tests__/core.test.ts +0 -1
  36. package/src/responsive/deviceDetection.ts +5 -5
  37. package/src/responsive/iPadBreakpoints.ts +55 -0
  38. package/src/responsive/iPadDetection.ts +48 -0
  39. package/src/responsive/iPadLayoutUtils.ts +95 -0
  40. package/src/responsive/iPadModalUtils.ts +98 -0
  41. package/src/responsive/index.ts +31 -0
  42. package/src/safe-area/__tests__/components/SafeAreaProvider.test.tsx +2 -2
  43. package/src/safe-area/__tests__/hooks/useContentSafeAreaPadding.test.tsx +2 -2
  44. package/src/safe-area/__tests__/hooks/useHeaderSafeAreaPadding.test.tsx +2 -2
  45. package/src/safe-area/__tests__/hooks/useSafeAreaInsets.test.tsx +2 -2
  46. package/src/safe-area/__tests__/hooks/useStatusBarSafeAreaPadding.test.tsx +2 -2
  47. package/src/safe-area/__tests__/integration/completeFlow.test.tsx +5 -4
  48. package/src/safe-area/__tests__/utils/testUtils.tsx +5 -4
  49. package/src/theme/infrastructure/stores/themeStore.ts +0 -2
  50. package/src/typography/presentation/utils/textColorUtils.ts +0 -1
@@ -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
+ }
@@ -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
+ });