@umituz/react-native-design-system 2.9.20 → 2.9.22

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.9.20",
3
+ "version": "2.9.22",
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, offline, onboarding, and loading utilities",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Anonymous User Types
3
+ *
4
+ * Type definitions for device-based anonymous user identification.
5
+ * Device ID is persistent across app reinstalls but device-specific.
6
+ *
7
+ * @domain device
8
+ * @layer domain/types
9
+ */
10
+
11
+ /**
12
+ * Anonymous user data with persistent device-based identification
13
+ */
14
+ export interface AnonymousUser {
15
+ /** Persistent device-based user ID (stable across sessions and reinstalls) */
16
+ userId: string;
17
+ /** User-friendly device name (e.g., "iPhone13-A8F2") */
18
+ deviceName: string;
19
+ /** Display name for the anonymous user */
20
+ displayName: string;
21
+ /** Always true for anonymous users */
22
+ isAnonymous: boolean;
23
+ }
24
+
25
+ /**
26
+ * Options for anonymous user hook
27
+ */
28
+ export interface UseAnonymousUserOptions {
29
+ /** Custom display name for anonymous user */
30
+ anonymousDisplayName?: string;
31
+ /** Fallback user ID if device ID generation fails */
32
+ fallbackUserId?: string;
33
+ }
34
+
35
+ /**
36
+ * Return type for useAnonymousUser hook
37
+ */
38
+ export interface UseAnonymousUserResult {
39
+ /** Anonymous user data with persistent device-based ID */
40
+ anonymousUser: AnonymousUser | null;
41
+ /** Loading state */
42
+ isLoading: boolean;
43
+ /** Error message if ID generation failed */
44
+ error: string | null;
45
+ /** Refresh function to reload user data */
46
+ refresh: () => Promise<void>;
47
+ }
@@ -108,7 +108,8 @@ export {
108
108
  export type {
109
109
  AnonymousUser,
110
110
  UseAnonymousUserOptions,
111
- } from './presentation/hooks/useAnonymousUser';
111
+ UseAnonymousUserResult,
112
+ } from './domain/types/AnonymousUserTypes';
112
113
 
113
114
  export { useDeviceFeatures } from './presentation/hooks/useDeviceFeatures';
114
115
 
@@ -1,12 +1,28 @@
1
+ /**
2
+ * Secure Device ID Repository
3
+ *
4
+ * Uses iOS Keychain with AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY for:
5
+ * - Persistence across app reinstalls
6
+ * - Device-specific (no backup migration)
7
+ * - Accessible after first unlock (background tasks)
8
+ *
9
+ * @domain device
10
+ * @layer infrastructure/repositories
11
+ */
12
+
1
13
  import * as SecureStore from 'expo-secure-store';
2
14
 
3
15
  const DEVICE_ID_KEY = '@device/persistent_id';
4
16
  const MIGRATION_FLAG_KEY = '@device/migration_completed';
5
17
 
18
+ const KEYCHAIN_OPTIONS: SecureStore.SecureStoreOptions = {
19
+ keychainAccessible: SecureStore.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY,
20
+ };
21
+
6
22
  export class SecureDeviceIdRepository {
7
23
  async get(): Promise<string | null> {
8
24
  try {
9
- return await SecureStore.getItemAsync(DEVICE_ID_KEY);
25
+ return await SecureStore.getItemAsync(DEVICE_ID_KEY, KEYCHAIN_OPTIONS);
10
26
  } catch {
11
27
  return null;
12
28
  }
@@ -14,15 +30,15 @@ export class SecureDeviceIdRepository {
14
30
 
15
31
  async set(deviceId: string): Promise<void> {
16
32
  try {
17
- await SecureStore.setItemAsync(DEVICE_ID_KEY, deviceId);
33
+ await SecureStore.setItemAsync(DEVICE_ID_KEY, deviceId, KEYCHAIN_OPTIONS);
18
34
  } catch {
19
- // Silent fail per CLAUDE.md rules
35
+ // Silent fail
20
36
  }
21
37
  }
22
38
 
23
39
  async hasMigrated(): Promise<boolean> {
24
40
  try {
25
- const flag = await SecureStore.getItemAsync(MIGRATION_FLAG_KEY);
41
+ const flag = await SecureStore.getItemAsync(MIGRATION_FLAG_KEY, KEYCHAIN_OPTIONS);
26
42
  return flag === 'true';
27
43
  } catch {
28
44
  return false;
@@ -31,7 +47,15 @@ export class SecureDeviceIdRepository {
31
47
 
32
48
  async setMigrated(): Promise<void> {
33
49
  try {
34
- await SecureStore.setItemAsync(MIGRATION_FLAG_KEY, 'true');
50
+ await SecureStore.setItemAsync(MIGRATION_FLAG_KEY, 'true', KEYCHAIN_OPTIONS);
51
+ } catch {
52
+ // Silent fail
53
+ }
54
+ }
55
+
56
+ async remove(): Promise<void> {
57
+ try {
58
+ await SecureStore.deleteItemAsync(DEVICE_ID_KEY, KEYCHAIN_OPTIONS);
35
59
  } catch {
36
60
  // Silent fail
37
61
  }
@@ -12,6 +12,7 @@ import { Dimensions } from 'react-native';
12
12
  import * as Localization from 'expo-localization';
13
13
  import { DeviceInfoService } from './DeviceInfoService';
14
14
  import { ApplicationInfoService } from './ApplicationInfoService';
15
+ import { DeviceIdService } from './DeviceIdService';
15
16
  import { PersistentDeviceIdService } from './PersistentDeviceIdService';
16
17
 
17
18
  /**
@@ -21,7 +22,12 @@ import { PersistentDeviceIdService } from './PersistentDeviceIdService';
21
22
  */
22
23
  export interface DeviceExtras {
23
24
  [key: string]: string | number | boolean | undefined;
25
+ /** The stable ID stored in Keychain/SecureStore */
24
26
  deviceId?: string;
27
+ /** Alias for deviceId for clarity in Firestore */
28
+ persistentDeviceId?: string;
29
+ /** The raw native platform ID (IDFV on iOS, Android ID on Android) */
30
+ nativeDeviceId?: string;
25
31
  platform?: string;
26
32
  deviceModel?: string;
27
33
  deviceBrand?: string;
@@ -77,10 +83,11 @@ function getDeviceLocale(): string | undefined {
77
83
  */
78
84
  export async function collectDeviceExtras(): Promise<DeviceExtras> {
79
85
  try {
80
- const [deviceInfo, appInfo, deviceId] = await Promise.all([
86
+ const [deviceInfo, appInfo, deviceId, nativeDeviceId] = await Promise.all([
81
87
  DeviceInfoService.getDeviceInfo(),
82
88
  ApplicationInfoService.getApplicationInfo(),
83
89
  PersistentDeviceIdService.getDeviceId(),
90
+ DeviceIdService.getDeviceId(),
84
91
  ]);
85
92
 
86
93
  const locale = getDeviceLocale();
@@ -88,6 +95,8 @@ export async function collectDeviceExtras(): Promise<DeviceExtras> {
88
95
 
89
96
  return {
90
97
  deviceId,
98
+ persistentDeviceId: deviceId, // Explicitly named for Firestore clarity
99
+ nativeDeviceId: nativeDeviceId || undefined,
91
100
  platform: deviceInfo.platform,
92
101
  deviceModel: deviceInfo.modelName || undefined,
93
102
  deviceBrand: deviceInfo.brand || undefined,
@@ -1,32 +1,27 @@
1
+ import { generateUUID } from '../../../uuid';
2
+ import { SecureDeviceIdRepository } from '../repositories/SecureDeviceIdRepository';
3
+ import { LegacyDeviceIdRepository } from '../repositories/LegacyDeviceIdRepository';
4
+ import { DeviceIdService } from './DeviceIdService';
5
+
6
+ let cachedDeviceId: string | null = null;
7
+ let initializationPromise: Promise<string> | null = null;
8
+
1
9
  /**
2
10
  * Persistent Device ID Service
3
11
  *
4
12
  * Provides a stable device identifier that survives app reinstalls.
5
- * Priority: SecureStore (survives reinstall) > Native ID > Generated UUID
13
+ * Priority: SecureStore (true keychain persistence) > Migration > Native ID > Generated UUID
6
14
  *
7
15
  * @domain device
8
16
  * @layer infrastructure/services
9
17
  */
10
-
11
- import { SecureDeviceIdRepository } from '../repositories/SecureDeviceIdRepository';
12
- import { LegacyDeviceIdRepository } from '../repositories/LegacyDeviceIdRepository';
13
- import { DeviceIdService } from './DeviceIdService';
14
-
15
- let cachedDeviceId: string | null = null;
16
- let initializationPromise: Promise<string> | null = null;
17
-
18
- function generateUUID(): string {
19
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
20
- const r = (Math.random() * 16) | 0;
21
- const v = c === 'x' ? r : (r & 0x3) | 0x8;
22
- return v.toString(16);
23
- });
24
- }
25
-
26
18
  export class PersistentDeviceIdService {
27
19
  private static secureRepo = new SecureDeviceIdRepository();
28
20
  private static legacyRepo = new LegacyDeviceIdRepository();
29
21
 
22
+ /**
23
+ * Get device ID with caching and concurrent request handling
24
+ */
30
25
  static async getDeviceId(): Promise<string> {
31
26
  if (cachedDeviceId) {
32
27
  return cachedDeviceId;
@@ -40,34 +35,60 @@ export class PersistentDeviceIdService {
40
35
  return initializationPromise;
41
36
  }
42
37
 
38
+ /**
39
+ * Logic to establish a persistent ID
40
+ */
43
41
  private static async initializeDeviceId(): Promise<string> {
44
42
  try {
43
+ // 1. Try secure repository (Keychain)
45
44
  const secureId = await this.secureRepo.get();
46
45
 
47
46
  if (secureId) {
47
+ if (__DEV__) {
48
+ console.log('[PersistentDeviceIdService] Found secure device ID:', secureId);
49
+ }
48
50
  cachedDeviceId = secureId;
49
51
  return secureId;
50
52
  }
51
53
 
54
+ // 2. Try migration from legacy storage
52
55
  const migrated = await this.migrateFromLegacy();
53
56
  if (migrated) {
57
+ if (__DEV__) {
58
+ console.log('[PersistentDeviceIdService] Migrated ID from legacy storage:', migrated);
59
+ }
54
60
  cachedDeviceId = migrated;
55
61
  return migrated;
56
62
  }
57
63
 
64
+ // 3. Create brand new ID
58
65
  const newId = await this.createNewDeviceId();
59
- await this.secureRepo.set(newId);
60
- await this.legacyRepo.set(newId);
61
- cachedDeviceId = newId;
66
+
67
+ // Save to both for safety during transition
68
+ await Promise.all([
69
+ this.secureRepo.set(newId),
70
+ this.legacyRepo.set(newId),
71
+ ]);
72
+
73
+ if (__DEV__) {
74
+ console.log('[PersistentDeviceIdService] Created new persistent ID:', newId);
75
+ }
62
76
 
77
+ cachedDeviceId = newId;
63
78
  return newId;
64
- } catch {
65
- const fallbackId = generateUUID();
79
+ } catch (error) {
80
+ if (__DEV__) {
81
+ console.error('[PersistentDeviceIdService] Initialization failed, using fallback:', error);
82
+ }
83
+ const fallbackId = `fallback_${generateUUID()}`;
66
84
  cachedDeviceId = fallbackId;
67
85
  return fallbackId;
68
86
  }
69
87
  }
70
88
 
89
+ /**
90
+ * Handles migration while ensuring it only happens once
91
+ */
71
92
  private static async migrateFromLegacy(): Promise<string | null> {
72
93
  try {
73
94
  const hasMigrated = await this.secureRepo.hasMigrated();
@@ -81,6 +102,7 @@ export class PersistentDeviceIdService {
81
102
  return null;
82
103
  }
83
104
 
105
+ // Upgrade legacy to secure storage
84
106
  await this.secureRepo.set(legacyId);
85
107
  await this.secureRepo.setMigrated();
86
108
 
@@ -90,6 +112,9 @@ export class PersistentDeviceIdService {
90
112
  }
91
113
  }
92
114
 
115
+ /**
116
+ * Generates a new ID based on platform info if possible
117
+ */
93
118
  private static async createNewDeviceId(): Promise<string> {
94
119
  const nativeId = await DeviceIdService.getDeviceId();
95
120
 
@@ -97,9 +122,12 @@ export class PersistentDeviceIdService {
97
122
  return `device_${nativeId}`;
98
123
  }
99
124
 
100
- return `generated_${generateUUID()}`;
125
+ return `gen_${generateUUID()}`;
101
126
  }
102
127
 
128
+ /**
129
+ * Check if an ID exists without initializing
130
+ */
103
131
  static async hasStoredId(): Promise<boolean> {
104
132
  try {
105
133
  const secureId = await this.secureRepo.get();
@@ -109,16 +137,28 @@ export class PersistentDeviceIdService {
109
137
  }
110
138
  }
111
139
 
140
+ /**
141
+ * Fully clear all stored identifiers
142
+ */
112
143
  static async clearStoredId(): Promise<void> {
113
144
  try {
114
- await this.legacyRepo.remove();
145
+ await Promise.all([
146
+ this.secureRepo.remove(),
147
+ this.legacyRepo.remove(),
148
+ ]);
115
149
  cachedDeviceId = null;
116
150
  initializationPromise = null;
151
+ if (__DEV__) {
152
+ console.log('[PersistentDeviceIdService] All device identifiers cleared');
153
+ }
117
154
  } catch {
118
155
  // Silent fail
119
156
  }
120
157
  }
121
158
 
159
+ /**
160
+ * Get current cached ID synchronously
161
+ */
122
162
  static getCachedId(): string | null {
123
163
  return cachedDeviceId;
124
164
  }
@@ -2,8 +2,10 @@
2
2
  * Anonymous User Hook
3
3
  *
4
4
  * Provides persistent device-based user ID for anonymous users.
5
- * The ID is stable across app restarts and sessions.
6
- * Compatible with subscription services and Firebase Anonymous Auth.
5
+ * Uses iOS Keychain with AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY for:
6
+ * - Stable ID across app restarts and reinstalls
7
+ * - Device-specific (no backup migration)
8
+ * - Compatible with Firebase Anonymous Auth
7
9
  *
8
10
  * @domain device
9
11
  * @layer presentation/hooks
@@ -12,37 +14,18 @@
12
14
  import { useState, useEffect, useCallback } from 'react';
13
15
  import { PersistentDeviceIdService } from '../../infrastructure/services/PersistentDeviceIdService';
14
16
  import { DeviceService } from '../../infrastructure/services/DeviceService';
15
-
16
- /**
17
- * Anonymous user data
18
- */
19
- export interface AnonymousUser {
20
- /** Persistent device-based user ID (stable across sessions) */
21
- userId: string;
22
- /** User-friendly device name (e.g., "iPhone13-A8F2") */
23
- deviceName: string;
24
- /** Display name for the anonymous user */
25
- displayName: string;
26
- /** Always true for anonymous users */
27
- isAnonymous: boolean;
28
- }
29
-
30
- /**
31
- * useAnonymousUser hook options
32
- */
33
- export interface UseAnonymousUserOptions {
34
- /** Custom display name for anonymous user */
35
- anonymousDisplayName?: string;
36
- /** Fallback user ID if device ID generation fails */
37
- fallbackUserId?: string;
38
- }
17
+ import type {
18
+ AnonymousUser,
19
+ UseAnonymousUserOptions,
20
+ UseAnonymousUserResult,
21
+ } from '../../domain/types/AnonymousUserTypes';
39
22
 
40
23
  /**
41
24
  * useAnonymousUser hook for persistent device-based user identification
42
25
  *
43
26
  * USAGE:
44
27
  * ```typescript
45
- * import { useAnonymousUser } from '@umituz/react-native-device';
28
+ * import { useAnonymousUser } from '@umituz/react-native-design-system';
46
29
  *
47
30
  * const { anonymousUser, isLoading } = useAnonymousUser();
48
31
  *
@@ -57,7 +40,9 @@ export interface UseAnonymousUserOptions {
57
40
  * />
58
41
  * ```
59
42
  */
60
- export const useAnonymousUser = (options?: UseAnonymousUserOptions) => {
43
+ export const useAnonymousUser = (
44
+ options?: UseAnonymousUserOptions
45
+ ): UseAnonymousUserResult => {
61
46
  const [anonymousUser, setAnonymousUser] = useState<AnonymousUser | null>(null);
62
47
  const [isLoading, setIsLoading] = useState(true);
63
48
  const [error, setError] = useState<string | null>(null);
@@ -105,13 +90,9 @@ export const useAnonymousUser = (options?: UseAnonymousUserOptions) => {
105
90
  }, [loadAnonymousUser]);
106
91
 
107
92
  return {
108
- /** Anonymous user data with persistent device-based ID */
109
93
  anonymousUser,
110
- /** Loading state */
111
94
  isLoading,
112
- /** Error message if ID generation failed */
113
95
  error,
114
- /** Refresh function to reload user data */
115
96
  refresh,
116
97
  };
117
98
  };