@umituz/react-native-design-system 2.6.126 → 2.6.128

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-design-system",
3
- "version": "2.6.126",
3
+ "version": "2.6.128",
4
4
  "description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive, safe area, exception, infinite scroll, UUID, image, timezone and offline utilities",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -72,6 +72,7 @@
72
72
  "expo-font": ">=12.0.0",
73
73
  "expo-image": ">=3.0.0",
74
74
  "expo-network": ">=8.0.0",
75
+ "expo-secure-store": ">=14.0.0",
75
76
  "expo-sharing": ">=12.0.0",
76
77
  "react": ">=19.0.0",
77
78
  "react-native": ">=0.81.0",
@@ -123,6 +124,7 @@
123
124
  "expo-haptics": "~14.0.0",
124
125
  "expo-image": "~3.0.11",
125
126
  "expo-localization": "~16.0.1",
127
+ "expo-secure-store": "~14.0.0",
126
128
  "expo-sharing": "~14.0.8",
127
129
  "react": "19.1.0",
128
130
  "react-native": "0.81.5",
@@ -0,0 +1,28 @@
1
+ export type ResetPeriod = 'daily' | 'weekly' | 'monthly' | 'never';
2
+
3
+ export interface FeatureLimit {
4
+ maxUses: number;
5
+ resetPeriod: ResetPeriod;
6
+ }
7
+
8
+ export interface DeviceFeatureConfig {
9
+ features: Record<string, FeatureLimit>;
10
+ }
11
+
12
+ export interface DeviceFeatureUsage {
13
+ usageCount: number;
14
+ lastResetAt: number;
15
+ firstUsedAt: number;
16
+ }
17
+
18
+ export interface DeviceFeatureAccess {
19
+ isAllowed: boolean;
20
+ remainingUses: number;
21
+ usageCount: number;
22
+ resetAt: number | null;
23
+ maxUses: number;
24
+ }
25
+
26
+ export const DEFAULT_FEATURE_CONFIG: DeviceFeatureConfig = {
27
+ features: {},
28
+ };
@@ -64,6 +64,16 @@ export {
64
64
 
65
65
  export { DeviceUtils } from './domain/entities/DeviceUtils';
66
66
 
67
+ export type {
68
+ DeviceFeatureConfig,
69
+ DeviceFeatureUsage,
70
+ DeviceFeatureAccess,
71
+ FeatureLimit,
72
+ ResetPeriod,
73
+ } from './domain/entities/DeviceFeatureConfig';
74
+
75
+ export { DEFAULT_FEATURE_CONFIG } from './domain/entities/DeviceFeatureConfig';
76
+
67
77
  export { DeviceTypeUtils } from './domain/entities/DeviceTypeUtils';
68
78
  export { DeviceMemoryUtils } from './domain/entities/DeviceMemoryUtils';
69
79
 
@@ -75,8 +85,11 @@ export { DeviceService } from './infrastructure/services/DeviceService';
75
85
  export { UserFriendlyIdService } from './infrastructure/services/UserFriendlyIdService';
76
86
  import { PersistentDeviceIdService } from './infrastructure/services/PersistentDeviceIdService';
77
87
  export { PersistentDeviceIdService };
88
+ export { DeviceFeatureService } from './infrastructure/services/DeviceFeatureService';
78
89
  export { collectDeviceExtras } from './infrastructure/services/DeviceExtrasCollector';
79
90
  export type { DeviceExtras } from './infrastructure/services/DeviceExtrasCollector';
91
+ export { SecureDeviceIdRepository } from './infrastructure/repositories/SecureDeviceIdRepository';
92
+ export { LegacyDeviceIdRepository } from './infrastructure/repositories/LegacyDeviceIdRepository';
80
93
 
81
94
  // ============================================================================
82
95
  // PRESENTATION - Device hooks
@@ -97,6 +110,8 @@ export type {
97
110
  UseAnonymousUserOptions,
98
111
  } from './presentation/hooks/useAnonymousUser';
99
112
 
113
+ export { useDeviceFeatures } from './presentation/hooks/useDeviceFeatures';
114
+
100
115
  // ============================================================================
101
116
  // UTILITIES
102
117
  // ============================================================================
@@ -0,0 +1,30 @@
1
+ import { storageRepository, unwrap } from '@umituz/react-native-storage';
2
+
3
+ const LEGACY_DEVICE_ID_KEY = '@device/persistent_id';
4
+
5
+ export class LegacyDeviceIdRepository {
6
+ async get(): Promise<string | null> {
7
+ try {
8
+ const result = await storageRepository.getString(LEGACY_DEVICE_ID_KEY, '');
9
+ return unwrap(result, '') || null;
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ async set(deviceId: string): Promise<void> {
16
+ try {
17
+ await storageRepository.setString(LEGACY_DEVICE_ID_KEY, deviceId);
18
+ } catch {
19
+ // Silent fail per CLAUDE.md rules
20
+ }
21
+ }
22
+
23
+ async remove(): Promise<void> {
24
+ try {
25
+ await storageRepository.removeItem(LEGACY_DEVICE_ID_KEY);
26
+ } catch {
27
+ // Silent fail
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,39 @@
1
+ import * as SecureStore from 'expo-secure-store';
2
+
3
+ const DEVICE_ID_KEY = '@device/persistent_id';
4
+ const MIGRATION_FLAG_KEY = '@device/migration_completed';
5
+
6
+ export class SecureDeviceIdRepository {
7
+ async get(): Promise<string | null> {
8
+ try {
9
+ return await SecureStore.getItemAsync(DEVICE_ID_KEY);
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ async set(deviceId: string): Promise<void> {
16
+ try {
17
+ await SecureStore.setItemAsync(DEVICE_ID_KEY, deviceId);
18
+ } catch {
19
+ // Silent fail per CLAUDE.md rules
20
+ }
21
+ }
22
+
23
+ async hasMigrated(): Promise<boolean> {
24
+ try {
25
+ const flag = await SecureStore.getItemAsync(MIGRATION_FLAG_KEY);
26
+ return flag === 'true';
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ async setMigrated(): Promise<void> {
33
+ try {
34
+ await SecureStore.setItemAsync(MIGRATION_FLAG_KEY, 'true');
35
+ } catch {
36
+ // Silent fail
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Device Feature Service
3
+ *
4
+ * Generic service for tracking device-based feature usage.
5
+ * Apps configure feature limits via props, service tracks usage.
6
+ *
7
+ * @domain device
8
+ * @layer infrastructure/services
9
+ */
10
+
11
+ import { storageRepository, unwrap } from '@umituz/react-native-storage';
12
+ import type {
13
+ DeviceFeatureConfig,
14
+ DeviceFeatureUsage,
15
+ DeviceFeatureAccess,
16
+ FeatureLimit,
17
+ ResetPeriod,
18
+ } from '../../domain/entities/DeviceFeatureConfig';
19
+ import { PersistentDeviceIdService } from './PersistentDeviceIdService';
20
+
21
+ export class DeviceFeatureService {
22
+ private static config: DeviceFeatureConfig = { features: {} };
23
+
24
+ static setConfig(config: DeviceFeatureConfig): void {
25
+ this.config = config;
26
+ }
27
+
28
+ static async checkFeatureAccess(
29
+ featureName: string
30
+ ): Promise<DeviceFeatureAccess> {
31
+ const deviceId = await PersistentDeviceIdService.getDeviceId();
32
+ const featureConfig = this.config.features[featureName];
33
+
34
+ if (!featureConfig) {
35
+ return {
36
+ isAllowed: true,
37
+ remainingUses: -1,
38
+ usageCount: 0,
39
+ resetAt: null,
40
+ maxUses: -1,
41
+ };
42
+ }
43
+
44
+ const usage = await this.getFeatureUsage(deviceId, featureName);
45
+ const shouldReset = this.shouldResetUsage(usage, featureConfig.resetPeriod);
46
+
47
+ if (shouldReset) {
48
+ await this.resetFeatureUsage(deviceId, featureName);
49
+ return {
50
+ isAllowed: true,
51
+ remainingUses: featureConfig.maxUses - 1,
52
+ usageCount: 0,
53
+ resetAt: this.calculateNextReset(featureConfig.resetPeriod),
54
+ maxUses: featureConfig.maxUses,
55
+ };
56
+ }
57
+
58
+ const isAllowed = usage.usageCount < featureConfig.maxUses;
59
+ const remainingUses = Math.max(
60
+ 0,
61
+ featureConfig.maxUses - usage.usageCount
62
+ );
63
+
64
+ return {
65
+ isAllowed,
66
+ remainingUses,
67
+ usageCount: usage.usageCount,
68
+ resetAt: this.calculateNextReset(featureConfig.resetPeriod),
69
+ maxUses: featureConfig.maxUses,
70
+ };
71
+ }
72
+
73
+ static async incrementFeatureUsage(featureName: string): Promise<void> {
74
+ const deviceId = await PersistentDeviceIdService.getDeviceId();
75
+ const usage = await this.getFeatureUsage(deviceId, featureName);
76
+
77
+ const updatedUsage: DeviceFeatureUsage = {
78
+ ...usage,
79
+ usageCount: usage.usageCount + 1,
80
+ };
81
+
82
+ await this.setFeatureUsage(deviceId, featureName, updatedUsage);
83
+ }
84
+
85
+ private static async getFeatureUsage(
86
+ deviceId: string,
87
+ featureName: string
88
+ ): Promise<DeviceFeatureUsage> {
89
+ const key = this.getStorageKey(deviceId, featureName);
90
+ const now = Date.now();
91
+ const defaultUsage: DeviceFeatureUsage = {
92
+ usageCount: 0,
93
+ lastResetAt: now,
94
+ firstUsedAt: now,
95
+ };
96
+
97
+ try {
98
+ const result = await storageRepository.getItem<DeviceFeatureUsage>(
99
+ key,
100
+ defaultUsage
101
+ );
102
+ return unwrap(result, defaultUsage);
103
+ } catch {
104
+ return defaultUsage;
105
+ }
106
+ }
107
+
108
+ private static async setFeatureUsage(
109
+ deviceId: string,
110
+ featureName: string,
111
+ usage: DeviceFeatureUsage
112
+ ): Promise<void> {
113
+ const key = this.getStorageKey(deviceId, featureName);
114
+ try {
115
+ await storageRepository.setItem(key, usage);
116
+ } catch {
117
+ // Silent fail
118
+ }
119
+ }
120
+
121
+ private static async resetFeatureUsage(
122
+ deviceId: string,
123
+ featureName: string
124
+ ): Promise<void> {
125
+ const now = Date.now();
126
+ const usage = await this.getFeatureUsage(deviceId, featureName);
127
+
128
+ const resetUsage: DeviceFeatureUsage = {
129
+ ...usage,
130
+ usageCount: 0,
131
+ lastResetAt: now,
132
+ };
133
+
134
+ await this.setFeatureUsage(deviceId, featureName, resetUsage);
135
+ }
136
+
137
+ private static shouldResetUsage(
138
+ usage: DeviceFeatureUsage,
139
+ resetPeriod: ResetPeriod
140
+ ): boolean {
141
+ if (resetPeriod === 'never') {
142
+ return false;
143
+ }
144
+
145
+ const now = Date.now();
146
+ const timeSinceReset = now - usage.lastResetAt;
147
+ const periods: Record<ResetPeriod, number> = {
148
+ daily: 24 * 60 * 60 * 1000,
149
+ weekly: 7 * 24 * 60 * 60 * 1000,
150
+ monthly: 30 * 24 * 60 * 60 * 1000,
151
+ never: Infinity,
152
+ };
153
+
154
+ return timeSinceReset >= periods[resetPeriod];
155
+ }
156
+
157
+ private static calculateNextReset(resetPeriod: ResetPeriod): number | null {
158
+ if (resetPeriod === 'never') {
159
+ return null;
160
+ }
161
+
162
+ const now = Date.now();
163
+ const periods: Record<ResetPeriod, number> = {
164
+ daily: 24 * 60 * 60 * 1000,
165
+ weekly: 7 * 24 * 60 * 60 * 1000,
166
+ monthly: 30 * 24 * 60 * 60 * 1000,
167
+ never: 0,
168
+ };
169
+
170
+ return now + periods[resetPeriod];
171
+ }
172
+
173
+ private static getStorageKey(deviceId: string, featureName: string): string {
174
+ return `@device/feature/${deviceId}/${featureName}`;
175
+ }
176
+ }
@@ -1,28 +1,20 @@
1
1
  /**
2
2
  * Persistent Device ID Service
3
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.
4
+ * Provides a stable device identifier that survives app reinstalls.
5
+ * Priority: SecureStore (survives reinstall) > Native ID > Generated UUID
7
6
  *
8
7
  * @domain device
9
8
  * @layer infrastructure/services
10
9
  */
11
10
 
12
- import { storageRepository, unwrap } from '@umituz/react-native-storage';
11
+ import { SecureDeviceIdRepository } from '../repositories/SecureDeviceIdRepository';
12
+ import { LegacyDeviceIdRepository } from '../repositories/LegacyDeviceIdRepository';
13
13
  import { DeviceIdService } from './DeviceIdService';
14
14
 
15
- const STORAGE_KEY = '@device/persistent_id';
16
-
17
- /** Cached ID to avoid repeated AsyncStorage reads */
18
15
  let cachedDeviceId: string | null = null;
19
-
20
- /** Promise to prevent race conditions during initialization */
21
16
  let initializationPromise: Promise<string> | null = null;
22
17
 
23
- /**
24
- * Generate a UUID v4 without external dependencies
25
- */
26
18
  function generateUUID(): string {
27
19
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
28
20
  const r = (Math.random() * 16) | 0;
@@ -31,21 +23,10 @@ function generateUUID(): string {
31
23
  });
32
24
  }
33
25
 
34
- /**
35
- * Service for managing persistent device identifiers
36
- */
37
26
  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
- */
27
+ private static secureRepo = new SecureDeviceIdRepository();
28
+ private static legacyRepo = new LegacyDeviceIdRepository();
29
+
49
30
  static async getDeviceId(): Promise<string> {
50
31
  if (cachedDeviceId) {
51
32
  return cachedDeviceId;
@@ -59,21 +40,24 @@ export class PersistentDeviceIdService {
59
40
  return initializationPromise;
60
41
  }
61
42
 
62
- /**
63
- * Initialize and persist device ID
64
- */
65
43
  private static async initializeDeviceId(): Promise<string> {
66
44
  try {
67
- const result = await storageRepository.getString(STORAGE_KEY, '');
68
- const storedId = unwrap(result, '');
45
+ const secureId = await this.secureRepo.get();
69
46
 
70
- if (storedId) {
71
- cachedDeviceId = storedId;
72
- return storedId;
47
+ if (secureId) {
48
+ cachedDeviceId = secureId;
49
+ return secureId;
50
+ }
51
+
52
+ const migrated = await this.migrateFromLegacy();
53
+ if (migrated) {
54
+ cachedDeviceId = migrated;
55
+ return migrated;
73
56
  }
74
57
 
75
58
  const newId = await this.createNewDeviceId();
76
- await storageRepository.setString(STORAGE_KEY, newId);
59
+ await this.secureRepo.set(newId);
60
+ await this.legacyRepo.set(newId);
77
61
  cachedDeviceId = newId;
78
62
 
79
63
  return newId;
@@ -84,9 +68,28 @@ export class PersistentDeviceIdService {
84
68
  }
85
69
  }
86
70
 
87
- /**
88
- * Create a new device ID from native source or generate one
89
- */
71
+ private static async migrateFromLegacy(): Promise<string | null> {
72
+ try {
73
+ const hasMigrated = await this.secureRepo.hasMigrated();
74
+ if (hasMigrated) {
75
+ return null;
76
+ }
77
+
78
+ const legacyId = await this.legacyRepo.get();
79
+ if (!legacyId) {
80
+ await this.secureRepo.setMigrated();
81
+ return null;
82
+ }
83
+
84
+ await this.secureRepo.set(legacyId);
85
+ await this.secureRepo.setMigrated();
86
+
87
+ return legacyId;
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
90
93
  private static async createNewDeviceId(): Promise<string> {
91
94
  const nativeId = await DeviceIdService.getDeviceId();
92
95
 
@@ -97,35 +100,25 @@ export class PersistentDeviceIdService {
97
100
  return `generated_${generateUUID()}`;
98
101
  }
99
102
 
100
- /**
101
- * Check if device ID exists in storage
102
- */
103
103
  static async hasStoredId(): Promise<boolean> {
104
104
  try {
105
- return await storageRepository.hasItem(STORAGE_KEY);
105
+ const secureId = await this.secureRepo.get();
106
+ return secureId !== null;
106
107
  } catch {
107
108
  return false;
108
109
  }
109
110
  }
110
111
 
111
- /**
112
- * Clear stored device ID (use with caution)
113
- * This will generate a new ID on next getDeviceId() call
114
- */
115
112
  static async clearStoredId(): Promise<void> {
116
113
  try {
117
- await storageRepository.removeItem(STORAGE_KEY);
114
+ await this.legacyRepo.remove();
118
115
  cachedDeviceId = null;
119
116
  initializationPromise = null;
120
117
  } catch {
121
- // Silent fail - non-critical operation
118
+ // Silent fail
122
119
  }
123
120
  }
124
121
 
125
- /**
126
- * Get the cached device ID without async operation
127
- * Returns null if not yet initialized
128
- */
129
122
  static getCachedId(): string | null {
130
123
  return cachedDeviceId;
131
124
  }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * useDeviceFeatures Hook
3
+ *
4
+ * Hook for checking and tracking device-based feature usage.
5
+ * Main app provides feature configuration via DesignSystemProvider.
6
+ *
7
+ * @param featureName - Name of the feature to track
8
+ * @returns Feature access status and usage tracking functions
9
+ */
10
+
11
+ import { useState, useEffect, useCallback } from 'react';
12
+ import { DeviceFeatureService } from '../../infrastructure/services/DeviceFeatureService';
13
+ import type { DeviceFeatureAccess } from '../../domain/entities/DeviceFeatureConfig';
14
+
15
+ interface UseDeviceFeaturesResult extends DeviceFeatureAccess {
16
+ incrementUsage: () => Promise<void>;
17
+ refresh: () => Promise<void>;
18
+ }
19
+
20
+ export function useDeviceFeatures(
21
+ featureName: string
22
+ ): UseDeviceFeaturesResult {
23
+ const [access, setAccess] = useState<DeviceFeatureAccess>({
24
+ isAllowed: true,
25
+ remainingUses: -1,
26
+ usageCount: 0,
27
+ resetAt: null,
28
+ maxUses: -1,
29
+ });
30
+
31
+ const checkAccess = useCallback(async () => {
32
+ try {
33
+ const result = await DeviceFeatureService.checkFeatureAccess(featureName);
34
+ setAccess(result);
35
+ } catch {
36
+ // Silent fail
37
+ }
38
+ }, [featureName]);
39
+
40
+ const incrementUsage = useCallback(async () => {
41
+ try {
42
+ await DeviceFeatureService.incrementFeatureUsage(featureName);
43
+ await checkAccess();
44
+ } catch {
45
+ // Silent fail
46
+ }
47
+ }, [featureName, checkAccess]);
48
+
49
+ const refresh = useCallback(async () => {
50
+ await checkAccess();
51
+ }, [checkAccess]);
52
+
53
+ useEffect(() => {
54
+ checkAccess();
55
+ }, [checkAccess]);
56
+
57
+ return {
58
+ ...access,
59
+ incrementUsage,
60
+ refresh,
61
+ };
62
+ }
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { useState, useEffect, useCallback } from 'react';
12
12
  import { DeviceService } from '../../infrastructure/services/DeviceService';
13
+ import { PersistentDeviceIdService } from '../../infrastructure/services/PersistentDeviceIdService';
13
14
  import type { DeviceInfo, ApplicationInfo, SystemInfo } from '../../domain/entities/Device';
14
15
 
15
16
 
@@ -153,8 +154,9 @@ export const useDeviceCapabilities = () => {
153
154
  };
154
155
 
155
156
  /**
156
- * useDeviceId hook for device unique identifier
157
+ * useDeviceId hook for persistent device identifier
157
158
  *
159
+ * Returns device ID that survives app reinstalls (SecureStore).
158
160
  * WARNING: Use with caution - user privacy considerations!
159
161
  */
160
162
  export const useDeviceId = () => {
@@ -166,7 +168,7 @@ export const useDeviceId = () => {
166
168
  setIsLoading(true);
167
169
 
168
170
  try {
169
- const id = await DeviceService.getDeviceId();
171
+ const id = await PersistentDeviceIdService.getDeviceId();
170
172
  setDeviceId(id);
171
173
  } catch {
172
174
  setDeviceId(null);
@@ -43,16 +43,26 @@ export {
43
43
  DeviceService,
44
44
  UserFriendlyIdService,
45
45
  PersistentDeviceIdService,
46
+ DeviceFeatureService,
47
+ SecureDeviceIdRepository,
48
+ LegacyDeviceIdRepository,
46
49
  useDeviceInfo,
47
50
  useDeviceCapabilities,
48
51
  useDeviceId,
49
52
  useAnonymousUser,
53
+ useDeviceFeatures,
50
54
  getAnonymousUserId,
51
55
  collectDeviceExtras,
56
+ DEFAULT_FEATURE_CONFIG,
52
57
  type DeviceInfo,
53
58
  type ApplicationInfo,
54
59
  type SystemInfo,
55
60
  type AnonymousUser,
56
61
  type UseAnonymousUserOptions,
57
62
  type DeviceExtras,
63
+ type DeviceFeatureConfig,
64
+ type DeviceFeatureUsage,
65
+ type DeviceFeatureAccess,
66
+ type FeatureLimit,
67
+ type ResetPeriod,
58
68
  } from '../device';