@umituz/react-native-design-system 4.28.11 → 4.28.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +31 -8
- package/src/atoms/AtomicAvatar.tsx +69 -40
- package/src/atoms/AtomicDatePicker.tsx +6 -6
- package/src/atoms/AtomicSpinner.tsx +24 -22
- package/src/atoms/AtomicText.tsx +32 -27
- package/src/atoms/AtomicTextArea.tsx +17 -15
- package/src/atoms/EmptyState.tsx +44 -41
- package/src/atoms/button/AtomicButton.tsx +8 -9
- package/src/atoms/card/AtomicCard.tsx +26 -8
- package/src/atoms/datepicker/components/DatePickerButton.tsx +8 -8
- package/src/atoms/datepicker/components/DatePickerModal.tsx +7 -7
- package/src/atoms/fab/styles/fabStyles.ts +0 -21
- package/src/atoms/icon/index.ts +6 -20
- package/src/atoms/picker/components/PickerModal.tsx +24 -4
- package/src/atoms/skeleton/AtomicSkeleton.tsx +9 -11
- package/src/carousel/Carousel.tsx +43 -20
- package/src/carousel/carouselCalculations.ts +12 -9
- package/src/carousel/index.ts +0 -1
- package/src/device/detection/iPadDetection.ts +5 -14
- package/src/device/infrastructure/services/DeviceFeatureService.ts +89 -9
- package/src/device/infrastructure/services/DeviceInfoService.ts +33 -0
- package/src/device/infrastructure/services/UserFriendlyIdService.ts +8 -6
- package/src/device/infrastructure/utils/__tests__/stringUtils.test.ts +56 -20
- package/src/device/infrastructure/utils/nativeModuleUtils.ts +16 -2
- package/src/device/infrastructure/utils/stringUtils.ts +51 -5
- package/src/filesystem/domain/utils/FileUtils.ts +5 -1
- package/src/image/domain/utils/ImageUtils.ts +6 -0
- package/src/layouts/AppHeader/AppHeader.tsx +13 -3
- package/src/layouts/Container/Container.tsx +19 -1
- package/src/layouts/FormLayout/FormLayout.tsx +20 -1
- package/src/layouts/Grid/Grid.tsx +34 -4
- package/src/layouts/ScreenHeader/ScreenHeader.tsx +4 -0
- package/src/layouts/ScreenLayout/ScreenLayout.tsx +42 -3
- package/src/molecules/SearchBar/SearchBar.tsx +27 -23
- package/src/molecules/action-footer/ActionFooter.tsx +32 -31
- package/src/molecules/alerts/AlertService.ts +60 -15
- package/src/molecules/avatar/Avatar.tsx +3 -3
- package/src/molecules/avatar/AvatarGroup.tsx +7 -7
- package/src/molecules/bottom-sheet/components/BottomSheet.tsx +3 -3
- package/src/molecules/calendar/infrastructure/utils/DateUtilities.ts +12 -1
- package/src/molecules/calendar/presentation/components/CalendarDayCell.tsx +48 -32
- package/src/molecules/info-grid/InfoGrid.tsx +5 -3
- package/src/organisms/FormContainer.tsx +11 -1
- package/src/tanstack/domain/utils/ErrorHelpers.ts +2 -2
- package/src/tanstack/domain/utils/MetricsCalculator.ts +6 -1
- package/src/theme/core/colors/ColorUtils.ts +7 -4
- package/src/utils/formatters/stringFormatter.ts +18 -3
- package/src/utils/index.ts +6 -4
- package/src/utils/math/CalculationUtils.ts +10 -1
|
@@ -21,8 +21,70 @@ import { ErrorHandler } from '../../../utils/errors/ErrorHandler';
|
|
|
21
21
|
export class DeviceFeatureService {
|
|
22
22
|
private static config: DeviceFeatureConfig = { features: {} };
|
|
23
23
|
|
|
24
|
+
// In-memory usage tracking for debouncing
|
|
25
|
+
private static inMemoryUsage = new Map<string, number>();
|
|
26
|
+
private static dirtyFeatures = new Set<string>();
|
|
27
|
+
private static flushInterval: ReturnType<typeof setInterval> | null = null;
|
|
28
|
+
private static FLUSH_DELAY = 5000; // 5 seconds
|
|
29
|
+
|
|
24
30
|
static setConfig(config: DeviceFeatureConfig): void {
|
|
25
31
|
this.config = config;
|
|
32
|
+
this.startPeriodicFlush();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Start periodic flush of in-memory usage to storage
|
|
37
|
+
*/
|
|
38
|
+
private static startPeriodicFlush(): void {
|
|
39
|
+
if (this.flushInterval) return;
|
|
40
|
+
|
|
41
|
+
this.flushInterval = setInterval(() => {
|
|
42
|
+
this.flushDirtyFeatures();
|
|
43
|
+
}, this.FLUSH_DELAY);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Flush dirty features to storage
|
|
48
|
+
*/
|
|
49
|
+
private static async flushDirtyFeatures(): Promise<void> {
|
|
50
|
+
if (this.dirtyFeatures.size === 0) return;
|
|
51
|
+
|
|
52
|
+
const featuresToFlush = Array.from(this.dirtyFeatures);
|
|
53
|
+
this.dirtyFeatures.clear();
|
|
54
|
+
|
|
55
|
+
for (const featureKey of featuresToFlush) {
|
|
56
|
+
const [deviceId, featureName] = featureKey.split(':');
|
|
57
|
+
const increment = this.inMemoryUsage.get(featureKey) || 0;
|
|
58
|
+
|
|
59
|
+
if (increment > 0) {
|
|
60
|
+
try {
|
|
61
|
+
const usage = await this.getFeatureUsage(deviceId, featureName);
|
|
62
|
+
const updatedUsage: DeviceFeatureUsage = {
|
|
63
|
+
...usage,
|
|
64
|
+
usageCount: usage.usageCount + increment,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
await this.setFeatureUsage(deviceId, featureName, updatedUsage);
|
|
68
|
+
this.inMemoryUsage.delete(featureKey);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
ErrorHandler.log(error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Stop periodic flush (call on app cleanup)
|
|
78
|
+
*/
|
|
79
|
+
static async destroy(): Promise<void> {
|
|
80
|
+
if (this.flushInterval) {
|
|
81
|
+
clearInterval(this.flushInterval);
|
|
82
|
+
this.flushInterval = null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Flush any remaining dirty features
|
|
86
|
+
await this.flushDirtyFeatures();
|
|
87
|
+
this.inMemoryUsage.clear();
|
|
26
88
|
}
|
|
27
89
|
|
|
28
90
|
static async checkFeatureAccess(
|
|
@@ -42,10 +104,17 @@ export class DeviceFeatureService {
|
|
|
42
104
|
}
|
|
43
105
|
|
|
44
106
|
const usage = await this.getFeatureUsage(deviceId, featureName);
|
|
107
|
+
const featureKey = `${deviceId}:${featureName}`;
|
|
108
|
+
const inMemoryIncrement = this.inMemoryUsage.get(featureKey) || 0;
|
|
109
|
+
const totalUsageCount = usage.usageCount + inMemoryIncrement;
|
|
110
|
+
|
|
45
111
|
const shouldReset = this.shouldResetUsage(usage, featureConfig.resetPeriod);
|
|
46
112
|
|
|
47
113
|
if (shouldReset) {
|
|
48
114
|
await this.resetFeatureUsage(deviceId, featureName);
|
|
115
|
+
// Clear in-memory counter on reset
|
|
116
|
+
this.inMemoryUsage.delete(featureKey);
|
|
117
|
+
this.dirtyFeatures.delete(featureKey);
|
|
49
118
|
return {
|
|
50
119
|
isAllowed: true,
|
|
51
120
|
remainingUses: featureConfig.maxUses - 1,
|
|
@@ -55,16 +124,16 @@ export class DeviceFeatureService {
|
|
|
55
124
|
};
|
|
56
125
|
}
|
|
57
126
|
|
|
58
|
-
const isAllowed =
|
|
127
|
+
const isAllowed = totalUsageCount < featureConfig.maxUses;
|
|
59
128
|
const remainingUses = Math.max(
|
|
60
129
|
0,
|
|
61
|
-
featureConfig.maxUses -
|
|
130
|
+
featureConfig.maxUses - totalUsageCount
|
|
62
131
|
);
|
|
63
132
|
|
|
64
133
|
return {
|
|
65
134
|
isAllowed,
|
|
66
135
|
remainingUses,
|
|
67
|
-
usageCount:
|
|
136
|
+
usageCount: totalUsageCount,
|
|
68
137
|
resetAt: this.calculateNextReset(featureConfig.resetPeriod),
|
|
69
138
|
maxUses: featureConfig.maxUses,
|
|
70
139
|
};
|
|
@@ -72,14 +141,25 @@ export class DeviceFeatureService {
|
|
|
72
141
|
|
|
73
142
|
static async incrementFeatureUsage(featureName: string): Promise<void> {
|
|
74
143
|
const deviceId = await PersistentDeviceIdService.getDeviceId();
|
|
75
|
-
const
|
|
144
|
+
const featureKey = `${deviceId}:${featureName}`;
|
|
76
145
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
146
|
+
// Increment in-memory counter
|
|
147
|
+
const currentCount = this.inMemoryUsage.get(featureKey) || 0;
|
|
148
|
+
this.inMemoryUsage.set(featureKey, currentCount + 1);
|
|
149
|
+
|
|
150
|
+
// Mark as dirty for periodic flush
|
|
151
|
+
this.dirtyFeatures.add(featureKey);
|
|
81
152
|
|
|
82
|
-
|
|
153
|
+
// If this is the first increment, fetch current usage and set baseline
|
|
154
|
+
if (currentCount === 0) {
|
|
155
|
+
try {
|
|
156
|
+
const usage = await this.getFeatureUsage(deviceId, featureName);
|
|
157
|
+
// Store baseline to avoid double-counting
|
|
158
|
+
this.inMemoryUsage.set(featureKey, 0);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
ErrorHandler.log(error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
83
163
|
}
|
|
84
164
|
|
|
85
165
|
private static async getFeatureUsage(
|
|
@@ -25,7 +25,34 @@ const getDeviceModule = (): typeof import('expo-device') | null => {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
export class DeviceInfoService {
|
|
28
|
+
// Static cache for device info (app-lifetime)
|
|
29
|
+
private static cachedDeviceInfo: DeviceInfo | null = null;
|
|
30
|
+
private static cachePromise: Promise<DeviceInfo> | null = null;
|
|
31
|
+
|
|
28
32
|
static async getDeviceInfo(): Promise<DeviceInfo> {
|
|
33
|
+
// Return cached data if available
|
|
34
|
+
if (this.cachedDeviceInfo) {
|
|
35
|
+
return this.cachedDeviceInfo;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Return existing promise if cache is being populated
|
|
39
|
+
if (this.cachePromise) {
|
|
40
|
+
return this.cachePromise;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Populate cache
|
|
44
|
+
this.cachePromise = this.fetchDeviceInfo();
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const deviceInfo = await this.cachePromise;
|
|
48
|
+
this.cachedDeviceInfo = deviceInfo;
|
|
49
|
+
return deviceInfo;
|
|
50
|
+
} finally {
|
|
51
|
+
this.cachePromise = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private static async fetchDeviceInfo(): Promise<DeviceInfo> {
|
|
29
56
|
try {
|
|
30
57
|
const Device = getDeviceModule();
|
|
31
58
|
|
|
@@ -74,6 +101,12 @@ export class DeviceInfoService {
|
|
|
74
101
|
}
|
|
75
102
|
}
|
|
76
103
|
|
|
104
|
+
// Clear cache (useful for testing)
|
|
105
|
+
static clearCache(): void {
|
|
106
|
+
this.cachedDeviceInfo = null;
|
|
107
|
+
this.cachePromise = null;
|
|
108
|
+
}
|
|
109
|
+
|
|
77
110
|
private static getMinimalDeviceInfo(): DeviceInfo {
|
|
78
111
|
return {
|
|
79
112
|
brand: null,
|
|
@@ -32,7 +32,8 @@ export class UserFriendlyIdService {
|
|
|
32
32
|
static async getUserFriendlyId(): Promise<string> {
|
|
33
33
|
// Web platform - no native modules needed
|
|
34
34
|
if (Platform.OS === 'web') {
|
|
35
|
-
|
|
35
|
+
const randomId = await generateRandomId();
|
|
36
|
+
return `WebUser-${randomId}`;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
try {
|
|
@@ -44,25 +45,26 @@ export class UserFriendlyIdService {
|
|
|
44
45
|
if (deviceInfo && (deviceInfo.modelName || deviceInfo.deviceName)) {
|
|
45
46
|
const model = deviceInfo.modelName || deviceInfo.deviceName || 'Device';
|
|
46
47
|
const cleanModel = cleanModelName(model);
|
|
47
|
-
const idPart = extractIdPart(deviceId, 6);
|
|
48
|
+
const idPart = await extractIdPart(deviceId, 6);
|
|
48
49
|
|
|
49
50
|
return `${cleanModel}-${idPart}`;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
// Fallback: Use platform + random ID
|
|
53
|
-
return this.generateFallbackId();
|
|
54
|
+
return await this.generateFallbackId();
|
|
54
55
|
} catch {
|
|
55
56
|
// Final fallback: Generate safe random ID
|
|
56
|
-
return this.generateFallbackId();
|
|
57
|
+
return await this.generateFallbackId();
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
62
|
* Generate fallback ID when native modules are not available
|
|
62
63
|
*/
|
|
63
|
-
private static generateFallbackId(): string {
|
|
64
|
+
private static async generateFallbackId(): Promise<string> {
|
|
64
65
|
const platformPrefix = getPlatformPrefix(Platform.OS);
|
|
65
|
-
|
|
66
|
+
const randomId = await generateRandomId();
|
|
67
|
+
return `${platformPrefix}-${randomId}`;
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* String Utils Tests
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { cleanModelName, extractIdPart, generateRandomId, getPlatformPrefix } from '../stringUtils';
|
|
5
|
+
import { cleanModelName, extractIdPart, generateRandomId, generateRandomIdSync, extractIdPartSync, getPlatformPrefix } from '../stringUtils';
|
|
6
6
|
|
|
7
7
|
describe('String Utils', () => {
|
|
8
8
|
describe('cleanModelName', () => {
|
|
@@ -33,60 +33,96 @@ describe('String Utils', () => {
|
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
describe('extractIdPart', () => {
|
|
36
|
-
it('should extract last N characters from device ID', () => {
|
|
36
|
+
it('should extract last N characters from device ID', async () => {
|
|
37
37
|
const deviceId = '12345678-1234-1234-1234-123456789012';
|
|
38
|
-
const result = extractIdPart(deviceId, 6);
|
|
38
|
+
const result = await extractIdPart(deviceId, 6);
|
|
39
39
|
expect(result).toBe('789012');
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
it('should use default length of 6 when not specified', () => {
|
|
42
|
+
it('should use default length of 6 when not specified', async () => {
|
|
43
43
|
const deviceId = '12345678-1234-1234-1234-123456789012';
|
|
44
|
-
const result = extractIdPart(deviceId);
|
|
44
|
+
const result = await extractIdPart(deviceId);
|
|
45
45
|
expect(result).toBe('789012');
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
it('should handle null device ID by generating random ID', () => {
|
|
49
|
-
const result = extractIdPart(null, 6);
|
|
48
|
+
it('should handle null device ID by generating random ID', async () => {
|
|
49
|
+
const result = await extractIdPart(null, 6);
|
|
50
50
|
expect(result).toMatch(/^[A-Z0-9]{6}$/);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
it('should handle empty device ID by generating random ID', () => {
|
|
54
|
-
const result = extractIdPart('', 6);
|
|
53
|
+
it('should handle empty device ID by generating random ID', async () => {
|
|
54
|
+
const result = await extractIdPart('', 6);
|
|
55
55
|
expect(result).toMatch(/^[A-Z0-9]{6}$/);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
it('should handle device ID shorter than requested length', () => {
|
|
58
|
+
it('should handle device ID shorter than requested length', async () => {
|
|
59
59
|
const deviceId = 'ABC';
|
|
60
|
-
const result = extractIdPart(deviceId, 6);
|
|
60
|
+
const result = await extractIdPart(deviceId, 6);
|
|
61
61
|
expect(result).toBe('ABC');
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
it('should convert to uppercase', async () => {
|
|
65
|
+
const deviceId = 'abcdef';
|
|
66
|
+
const result = await extractIdPart(deviceId, 3);
|
|
67
|
+
expect(result).toBe('DEF');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('extractIdPartSync', () => {
|
|
72
|
+
it('should extract last N characters from device ID', () => {
|
|
73
|
+
const deviceId = '12345678-1234-1234-1234-123456789012';
|
|
74
|
+
const result = extractIdPartSync(deviceId, 6);
|
|
75
|
+
expect(result).toBe('789012');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should handle null device ID by generating random ID', () => {
|
|
79
|
+
const result = extractIdPartSync(null, 6);
|
|
80
|
+
expect(result).toMatch(/^[A-Z0-9]{6}$/);
|
|
81
|
+
});
|
|
82
|
+
|
|
64
83
|
it('should convert to uppercase', () => {
|
|
65
84
|
const deviceId = 'abcdef';
|
|
66
|
-
const result =
|
|
85
|
+
const result = extractIdPartSync(deviceId, 3);
|
|
67
86
|
expect(result).toBe('DEF');
|
|
68
87
|
});
|
|
69
88
|
});
|
|
70
89
|
|
|
71
90
|
describe('generateRandomId', () => {
|
|
72
|
-
it('should generate random ID with default length', () => {
|
|
73
|
-
const result = generateRandomId();
|
|
91
|
+
it('should generate random ID with default length', async () => {
|
|
92
|
+
const result = await generateRandomId();
|
|
74
93
|
expect(result).toMatch(/^[A-Z0-9]{6}$/);
|
|
75
94
|
});
|
|
76
95
|
|
|
77
|
-
it('should generate random ID with specified length', () => {
|
|
78
|
-
const result = generateRandomId(10);
|
|
96
|
+
it('should generate random ID with specified length', async () => {
|
|
97
|
+
const result = await generateRandomId(10);
|
|
79
98
|
expect(result).toMatch(/^[A-Z0-9]{10}$/);
|
|
80
99
|
});
|
|
81
100
|
|
|
82
|
-
it('should generate different IDs on multiple calls', () => {
|
|
83
|
-
const id1 = generateRandomId(6);
|
|
84
|
-
const id2 = generateRandomId(6);
|
|
101
|
+
it('should generate different IDs on multiple calls', async () => {
|
|
102
|
+
const id1 = await generateRandomId(6);
|
|
103
|
+
const id2 = await generateRandomId(6);
|
|
85
104
|
expect(id1).not.toBe(id2);
|
|
86
105
|
});
|
|
87
106
|
|
|
107
|
+
it('should handle length of 1', async () => {
|
|
108
|
+
const result = await generateRandomId(1);
|
|
109
|
+
expect(result).toMatch(/^[A-Z0-9]{1}$/);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('generateRandomIdSync', () => {
|
|
114
|
+
it('should generate random ID with default length', () => {
|
|
115
|
+
const result = generateRandomIdSync();
|
|
116
|
+
expect(result).toMatch(/^[A-Z0-9]{6}$/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should generate random ID with specified length', () => {
|
|
120
|
+
const result = generateRandomIdSync(10);
|
|
121
|
+
expect(result).toMatch(/^[A-Z0-9]{10}$/);
|
|
122
|
+
});
|
|
123
|
+
|
|
88
124
|
it('should handle length of 1', () => {
|
|
89
|
-
const result =
|
|
125
|
+
const result = generateRandomIdSync(1);
|
|
90
126
|
expect(result).toMatch(/^[A-Z0-9]{1}$/);
|
|
91
127
|
});
|
|
92
128
|
});
|
|
@@ -15,14 +15,21 @@ export async function withTimeout<T>(
|
|
|
15
15
|
operation: () => Promise<T>,
|
|
16
16
|
timeoutMs: number = 1000,
|
|
17
17
|
): Promise<T | null> {
|
|
18
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
19
|
+
|
|
18
20
|
try {
|
|
19
21
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
20
|
-
setTimeout(() => reject(new Error('Operation timeout')), timeoutMs);
|
|
22
|
+
timeoutId = setTimeout(() => reject(new Error('Operation timeout')), timeoutMs);
|
|
21
23
|
});
|
|
22
24
|
|
|
23
25
|
return await Promise.race([operation(), timeoutPromise]);
|
|
24
26
|
} catch {
|
|
25
27
|
return null;
|
|
28
|
+
} finally {
|
|
29
|
+
// Always clear timeout to prevent memory leak
|
|
30
|
+
if (timeoutId !== undefined) {
|
|
31
|
+
clearTimeout(timeoutId);
|
|
32
|
+
}
|
|
26
33
|
}
|
|
27
34
|
}
|
|
28
35
|
|
|
@@ -51,9 +58,11 @@ export async function withTimeoutAll<T>(
|
|
|
51
58
|
operations: Array<() => Promise<T>>,
|
|
52
59
|
timeoutMs: number = 2000,
|
|
53
60
|
): Promise<Array<T | null>> {
|
|
61
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
62
|
+
|
|
54
63
|
try {
|
|
55
64
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
56
|
-
setTimeout(() => reject(new Error('Operations timeout')), timeoutMs);
|
|
65
|
+
timeoutId = setTimeout(() => reject(new Error('Operations timeout')), timeoutMs);
|
|
57
66
|
});
|
|
58
67
|
|
|
59
68
|
const results = await Promise.race([
|
|
@@ -64,6 +73,11 @@ export async function withTimeoutAll<T>(
|
|
|
64
73
|
return results as Array<T | null>;
|
|
65
74
|
} catch {
|
|
66
75
|
return operations.map(() => null);
|
|
76
|
+
} finally {
|
|
77
|
+
// Always clear timeout to prevent memory leak
|
|
78
|
+
if (timeoutId !== undefined) {
|
|
79
|
+
clearTimeout(timeoutId);
|
|
80
|
+
}
|
|
67
81
|
}
|
|
68
82
|
}
|
|
69
83
|
|
|
@@ -24,21 +24,67 @@ export function cleanModelName(model: string | null | undefined): string {
|
|
|
24
24
|
* @param length - Length of ID part to extract (default: 6)
|
|
25
25
|
* @returns Last N characters of device ID in uppercase
|
|
26
26
|
*/
|
|
27
|
-
export function extractIdPart(deviceId: string | null, length: number = 6): string {
|
|
27
|
+
export async function extractIdPart(deviceId: string | null, length: number = 6): Promise<string> {
|
|
28
28
|
if (!deviceId) {
|
|
29
|
-
return generateRandomId(length);
|
|
29
|
+
return await generateRandomId(length);
|
|
30
30
|
}
|
|
31
31
|
const start = Math.max(0, deviceId.length - length);
|
|
32
32
|
return deviceId.substring(start).toUpperCase();
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
36
|
+
* Synchronous version of extractIdPart (uses fallback for null deviceId)
|
|
37
|
+
* @param deviceId - Full device ID
|
|
38
|
+
* @param length - Length of ID part to extract (default: 6)
|
|
39
|
+
* @returns Last N characters of device ID in uppercase
|
|
40
|
+
*/
|
|
41
|
+
export function extractIdPartSync(deviceId: string | null, length: number = 6): string {
|
|
42
|
+
if (!deviceId) {
|
|
43
|
+
return generateRandomIdSync(length);
|
|
44
|
+
}
|
|
45
|
+
const start = Math.max(0, deviceId.length - length);
|
|
46
|
+
return deviceId.substring(start).toUpperCase();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generate random alphanumeric ID using cryptographically secure random bytes
|
|
51
|
+
* @param length - Length of ID to generate (default: 6)
|
|
52
|
+
* @returns Random ID in uppercase
|
|
53
|
+
*/
|
|
54
|
+
export async function generateRandomId(length: number = 6): Promise<string> {
|
|
55
|
+
try {
|
|
56
|
+
// Use expo-crypto for cryptographically secure random bytes
|
|
57
|
+
const { getRandomBytesAsync } = require('expo-crypto');
|
|
58
|
+
const bytes: Uint8Array = await getRandomBytesAsync(length);
|
|
59
|
+
|
|
60
|
+
return Array.from(bytes)
|
|
61
|
+
.map(byte => byte.toString(36))
|
|
62
|
+
.join('')
|
|
63
|
+
.substring(0, length)
|
|
64
|
+
.toUpperCase();
|
|
65
|
+
} catch {
|
|
66
|
+
// Fallback with __DEV__ warning for environments without expo-crypto
|
|
67
|
+
if (__DEV__) {
|
|
68
|
+
console.warn('[stringUtils] expo-crypto not available, using insecure fallback');
|
|
69
|
+
}
|
|
70
|
+
return Array.from({ length }, () =>
|
|
71
|
+
Math.floor(Math.random() * 36).toString(36)
|
|
72
|
+
).join('').toUpperCase();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Synchronous version of generateRandomId (uses fallback)
|
|
37
78
|
* @param length - Length of ID to generate (default: 6)
|
|
38
79
|
* @returns Random ID in uppercase
|
|
39
80
|
*/
|
|
40
|
-
export function
|
|
41
|
-
|
|
81
|
+
export function generateRandomIdSync(length: number = 6): string {
|
|
82
|
+
if (__DEV__) {
|
|
83
|
+
console.warn('[stringUtils] Using insecure fallback for random ID generation');
|
|
84
|
+
}
|
|
85
|
+
return Array.from({ length }, () =>
|
|
86
|
+
Math.floor(Math.random() * 36).toString(36)
|
|
87
|
+
).join('').toUpperCase();
|
|
42
88
|
}
|
|
43
89
|
|
|
44
90
|
/**
|
|
@@ -59,7 +59,11 @@ export class FileUtils {
|
|
|
59
59
|
*/
|
|
60
60
|
static getFileExtension(filename: string): string {
|
|
61
61
|
const lastDot = filename.lastIndexOf('.');
|
|
62
|
-
|
|
62
|
+
// Check dot is not at position 0 (dotfile like .gitignore) and not at end
|
|
63
|
+
if (lastDot > 0 && lastDot < filename.length - 1) {
|
|
64
|
+
return filename.substring(lastDot);
|
|
65
|
+
}
|
|
66
|
+
return '';
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
/**
|
|
@@ -12,6 +12,12 @@ export class ImageUtils {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
static getAspectRatio(width: number, height: number): number {
|
|
15
|
+
if (height === 0) {
|
|
16
|
+
if (__DEV__) {
|
|
17
|
+
console.warn('[ImageUtils] Cannot calculate aspect ratio when height is zero');
|
|
18
|
+
}
|
|
19
|
+
return 1; // Default to square aspect ratio
|
|
20
|
+
}
|
|
15
21
|
return width / height;
|
|
16
22
|
}
|
|
17
23
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Composition: AtomicIcon + AtomicText + AtomicButton
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import React from 'react';
|
|
11
|
+
import React, { useMemo } from 'react';
|
|
12
12
|
import { View, type ViewStyle } from 'react-native';
|
|
13
13
|
import { SafeAreaView } from '../../safe-area';
|
|
14
14
|
import { useAppDesignTokens } from '../../theme';
|
|
@@ -51,13 +51,23 @@ export const AppHeader: React.FC<AppHeaderProps> = ({
|
|
|
51
51
|
onRightPress,
|
|
52
52
|
backgroundColor,
|
|
53
53
|
style,
|
|
54
|
+
accessibilityLabel,
|
|
55
|
+
accessibilityHint,
|
|
56
|
+
accessible,
|
|
54
57
|
}) => {
|
|
55
58
|
const tokens = useAppDesignTokens();
|
|
56
59
|
const bgColor = backgroundColor || tokens.colors.surface;
|
|
57
|
-
|
|
60
|
+
|
|
61
|
+
const styles = useMemo(() => createAppHeaderStyles(tokens), [tokens]);
|
|
58
62
|
|
|
59
63
|
return (
|
|
60
|
-
<SafeAreaView
|
|
64
|
+
<SafeAreaView
|
|
65
|
+
style={[styles.safeArea, { backgroundColor: bgColor }]}
|
|
66
|
+
accessibilityLabel={accessibilityLabel || title}
|
|
67
|
+
accessibilityHint={accessibilityHint}
|
|
68
|
+
accessible={accessible !== false}
|
|
69
|
+
accessibilityRole="header"
|
|
70
|
+
>
|
|
61
71
|
<View style={[styles.container, { backgroundColor: bgColor }, style]}>
|
|
62
72
|
{/* Left Action */}
|
|
63
73
|
<View style={styles.leftContainer}>
|
|
@@ -28,6 +28,15 @@ export interface ContainerProps {
|
|
|
28
28
|
|
|
29
29
|
/** Test ID */
|
|
30
30
|
testID?: string;
|
|
31
|
+
|
|
32
|
+
/** Accessibility label for the container */
|
|
33
|
+
accessibilityLabel?: string;
|
|
34
|
+
|
|
35
|
+
/** Accessibility role for the container */
|
|
36
|
+
accessibilityRole?: 'region' | 'section' | 'article';
|
|
37
|
+
|
|
38
|
+
/** Whether the container is accessible */
|
|
39
|
+
accessible?: boolean;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
/**
|
|
@@ -47,6 +56,9 @@ export const Container: React.FC<ContainerProps> = ({
|
|
|
47
56
|
center = true,
|
|
48
57
|
style,
|
|
49
58
|
testID,
|
|
59
|
+
accessibilityLabel,
|
|
60
|
+
accessibilityRole = 'region',
|
|
61
|
+
accessible,
|
|
50
62
|
}) => {
|
|
51
63
|
const { maxContentWidth } = useResponsive();
|
|
52
64
|
const tokens = useAppDesignTokens();
|
|
@@ -69,7 +81,13 @@ export const Container: React.FC<ContainerProps> = ({
|
|
|
69
81
|
);
|
|
70
82
|
|
|
71
83
|
return (
|
|
72
|
-
<View
|
|
84
|
+
<View
|
|
85
|
+
style={[styles.container, style]}
|
|
86
|
+
testID={testID}
|
|
87
|
+
accessibilityLabel={accessibilityLabel}
|
|
88
|
+
accessibilityRole={accessibilityRole as any}
|
|
89
|
+
accessible={accessible !== false}
|
|
90
|
+
>
|
|
73
91
|
{children}
|
|
74
92
|
</View>
|
|
75
93
|
);
|
|
@@ -29,6 +29,15 @@ export interface FormLayoutProps {
|
|
|
29
29
|
|
|
30
30
|
/** Test ID */
|
|
31
31
|
testID?: string;
|
|
32
|
+
|
|
33
|
+
/** Accessibility label for the form */
|
|
34
|
+
accessibilityLabel?: string;
|
|
35
|
+
|
|
36
|
+
/** Accessibility hint for the form */
|
|
37
|
+
accessibilityHint?: string;
|
|
38
|
+
|
|
39
|
+
/** Whether the form is accessible */
|
|
40
|
+
accessible?: boolean;
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
/**
|
|
@@ -51,6 +60,9 @@ export const FormLayout: React.FC<FormLayoutProps> = ({
|
|
|
51
60
|
disableKeyboardAvoid = false,
|
|
52
61
|
disableScroll = false,
|
|
53
62
|
testID,
|
|
63
|
+
accessibilityLabel,
|
|
64
|
+
accessibilityHint,
|
|
65
|
+
accessible,
|
|
54
66
|
}) => {
|
|
55
67
|
const tokens = useAppDesignTokens();
|
|
56
68
|
const { insets } = useResponsive();
|
|
@@ -83,7 +95,14 @@ export const FormLayout: React.FC<FormLayoutProps> = ({
|
|
|
83
95
|
);
|
|
84
96
|
|
|
85
97
|
const content = (
|
|
86
|
-
<View
|
|
98
|
+
<View
|
|
99
|
+
style={styles.formContent}
|
|
100
|
+
testID={testID}
|
|
101
|
+
accessibilityLabel={accessibilityLabel || "Form"}
|
|
102
|
+
accessibilityHint={accessibilityHint}
|
|
103
|
+
accessible={accessible !== false}
|
|
104
|
+
accessibilityRole="form"
|
|
105
|
+
>
|
|
87
106
|
{children}
|
|
88
107
|
</View>
|
|
89
108
|
);
|