@umituz/react-native-design-system 2.6.125 → 2.6.127
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 +3 -1
- package/src/device/domain/entities/DeviceFeatureConfig.ts +28 -0
- package/src/device/index.ts +15 -0
- package/src/device/infrastructure/repositories/LegacyDeviceIdRepository.ts +30 -0
- package/src/device/infrastructure/repositories/SecureDeviceIdRepository.ts +39 -0
- package/src/device/infrastructure/services/DeviceFeatureService.ts +176 -0
- package/src/device/infrastructure/services/PersistentDeviceIdService.ts +45 -52
- package/src/device/presentation/hooks/useDeviceFeatures.ts +62 -0
- package/src/device/presentation/hooks/useDeviceInfo.ts +4 -2
- package/src/exports/device.ts +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.127",
|
|
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
|
+
};
|
package/src/device/index.ts
CHANGED
|
@@ -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
|
|
5
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
|
68
|
-
const storedId = unwrap(result, '');
|
|
45
|
+
const secureId = await this.secureRepo.get();
|
|
69
46
|
|
|
70
|
-
if (
|
|
71
|
-
cachedDeviceId =
|
|
72
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
114
|
+
await this.legacyRepo.remove();
|
|
118
115
|
cachedDeviceId = null;
|
|
119
116
|
initializationPromise = null;
|
|
120
117
|
} catch {
|
|
121
|
-
// Silent fail
|
|
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
|
|
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
|
|
171
|
+
const id = await PersistentDeviceIdService.getDeviceId();
|
|
170
172
|
setDeviceId(id);
|
|
171
173
|
} catch {
|
|
172
174
|
setDeviceId(null);
|
package/src/exports/device.ts
CHANGED
|
@@ -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';
|