@umituz/react-native-settings 5.3.47 → 5.3.49

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-settings",
3
- "version": "5.3.47",
3
+ "version": "5.3.49",
4
4
  "description": "Complete settings hub for React Native apps - consolidated package with settings, localization, about, legal, appearance, feedback, FAQs, rating, and gamification",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./dist/index.d.ts",
@@ -24,6 +24,7 @@ export const useAboutInfo = (
24
24
 
25
25
  /**
26
26
  * Initialize app info with config
27
+ * FIXED: Set isInitializedRef BEFORE async operation to prevent race condition
27
28
  */
28
29
  const initialize = useCallback(
29
30
  async (config: AboutConfig) => {
@@ -31,10 +32,12 @@ export const useAboutInfo = (
31
32
  return;
32
33
  }
33
34
 
35
+ // FIX: Set ref immediately to prevent race condition from concurrent calls
36
+ isInitializedRef.current = true;
37
+
34
38
  await execute(async () => {
35
39
  const defaultAppInfo = createDefaultAppInfo(config);
36
40
  await repository.saveAppInfo(defaultAppInfo);
37
- isInitializedRef.current = true;
38
41
  return defaultAppInfo;
39
42
  });
40
43
  },
@@ -15,7 +15,7 @@ import { ScreenLayout } from "@umituz/react-native-design-system/layouts";
15
15
  import { FeedbackModal } from "../components/FeedbackModal";
16
16
  import { useFeatureRequests } from "../../infrastructure/useFeatureRequests";
17
17
  import type { FeedbackRating } from "../../domain/entities/FeedbackEntity";
18
- import type { FeatureRequestItem, VoteType } from "../../domain/entities/FeatureRequestEntity";
18
+ import type { FeatureRequestItem } from "../../domain/entities/FeatureRequestEntity";
19
19
  import type { FeedbackFormTexts } from "../components/FeedbackFormProps";
20
20
 
21
21
  interface FeatureRequestScreenProps {
@@ -21,7 +21,7 @@ import type { GamificationScreenProps } from "./types";
21
21
  * Internal component that renders the screen with all props
22
22
  */
23
23
  export const GamificationScreenInner: React.FC<GamificationScreenProps> = ({
24
- title,
24
+ title: _title,
25
25
  statsTitle,
26
26
  achievementsTitle,
27
27
  streakTitle,
@@ -63,16 +63,25 @@ export const updateAchievementProgress = (
63
63
  return Math.min(100, (value / definition.threshold) * 100);
64
64
  };
65
65
 
66
+ /**
67
+ * Check if streak is active based on last activity date
68
+ * Returns true if last activity was today or yesterday
69
+ * FIXED: Now uses date comparison instead of hour-based calculation to avoid timezone issues
70
+ */
66
71
  export const isStreakActive = (lastActivityDate: string | null): boolean => {
67
72
  if (!lastActivityDate) return false;
68
73
 
69
74
  const last = new Date(lastActivityDate);
70
75
  const now = new Date();
71
- const diffDays = Math.floor(
72
- (now.getTime() - last.getTime()) / (1000 * 60 * 60 * 24)
73
- );
74
76
 
75
- return diffDays <= 1;
77
+ // Normalize to midnight to compare days, not hours
78
+ const lastMidnight = new Date(last.getFullYear(), last.getMonth(), last.getDate());
79
+ const nowMidnight = new Date(now.getFullYear(), now.getMonth(), now.getDate());
80
+ const yesterdayMidnight = new Date(nowMidnight);
81
+ yesterdayMidnight.setDate(yesterdayMidnight.getDate() - 1);
82
+
83
+ // Active if last activity was today or yesterday
84
+ return lastMidnight.getTime() >= yesterdayMidnight.getTime();
76
85
  };
77
86
 
78
87
  export const isSameDay = (date1: Date, date2: Date): boolean => {
@@ -10,6 +10,7 @@ import { initReactI18next } from 'react-i18next';
10
10
  import { DEFAULT_LANGUAGE } from './languages';
11
11
  import { ResourceBuilder } from './ResourceBuilder';
12
12
  import { NamespaceResolver } from './NamespaceResolver';
13
+ import { isDev } from '../../../../utils/devUtils';
13
14
 
14
15
  export class I18nInitializer {
15
16
  private static reactI18nextInitialized = false;
@@ -52,7 +53,9 @@ export class I18nInitializer {
52
53
  returnNull: false,
53
54
  });
54
55
  } catch (error) {
55
- console.error('[I18nInitializer] Failed to initialize i18n:', languageCode, error);
56
+ if (isDev()) {
57
+ console.error('[I18nInitializer] Failed to initialize i18n:', languageCode, error);
58
+ }
56
59
  }
57
60
  }
58
61
 
@@ -47,7 +47,9 @@ export class LanguageSwitcher {
47
47
  isRTL: language?.isRTL ?? false,
48
48
  };
49
49
  } catch (error) {
50
- console.error('[LanguageSwitcher] Failed to switch language:', languageCode, error);
50
+ if (isDev()) {
51
+ console.error('[LanguageSwitcher] Failed to switch language:', languageCode, error);
52
+ }
51
53
  throw error;
52
54
  }
53
55
  }
@@ -1,4 +1,5 @@
1
1
  import * as Notifications from 'expo-notifications';
2
+ import { isDev } from "../../../../utils/devUtils";
2
3
  import type { ScheduleNotificationOptions, ScheduledNotification } from './types';
3
4
 
4
5
  export class NotificationScheduler {
@@ -54,7 +55,9 @@ export class NotificationScheduler {
54
55
 
55
56
  return notificationId;
56
57
  } catch (error) {
57
- console.error('Failed to schedule notification:', error);
58
+ if (isDev()) {
59
+ console.error('Failed to schedule notification:', error);
60
+ }
58
61
  throw new Error(`Notification scheduling failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
59
62
  }
60
63
  }
@@ -63,7 +66,9 @@ export class NotificationScheduler {
63
66
  try {
64
67
  await Notifications.cancelScheduledNotificationAsync(notificationId);
65
68
  } catch (error) {
66
- console.error('Failed to cancel notification:', notificationId, error);
69
+ if (isDev()) {
70
+ console.error('Failed to cancel notification:', notificationId, error);
71
+ }
67
72
  // Don't throw - canceling a non-existent notification is not critical
68
73
  }
69
74
  }
@@ -72,7 +77,9 @@ export class NotificationScheduler {
72
77
  try {
73
78
  await Notifications.cancelAllScheduledNotificationsAsync();
74
79
  } catch (error) {
75
- console.error('Failed to cancel all notifications:', error);
80
+ if (isDev()) {
81
+ console.error('Failed to cancel all notifications:', error);
82
+ }
76
83
  throw new Error(`Failed to cancel notifications: ${error instanceof Error ? error.message : 'Unknown error'}`);
77
84
  }
78
85
  }
@@ -90,7 +97,9 @@ export class NotificationScheduler {
90
97
  trigger: notification.trigger,
91
98
  }));
92
99
  } catch (error) {
93
- console.error('Failed to get scheduled notifications:', error);
100
+ if (isDev()) {
101
+ console.error('Failed to get scheduled notifications:', error);
102
+ }
94
103
  return []; // Return empty array as fallback
95
104
  }
96
105
  }
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { NotificationManager } from './NotificationManager';
11
+ import { isDev } from '../../../../utils/devUtils';
11
12
 
12
13
  export * from './types';
13
14
 
@@ -31,7 +32,9 @@ export class NotificationService {
31
32
  NotificationManager.configure();
32
33
  this.isConfigured = true;
33
34
  } catch (error) {
34
- console.error('[NotificationService] Failed to configure NotificationManager:', error);
35
+ if (isDev()) {
36
+ console.error('[NotificationService] Failed to configure NotificationManager:', error);
37
+ }
35
38
  }
36
39
  }
37
40
  }
@@ -119,8 +119,11 @@ export const useReminderActions = () => {
119
119
  throw error; // Re-throw to allow caller to handle
120
120
  }
121
121
  } else if (!reminder.enabled) {
122
+ // FIX: Create temporary updated reminder for trigger building
123
+ // This prevents state inconsistency where trigger is built with old state
124
+ const tempEnabledReminder = { ...reminder, enabled: true };
122
125
  try {
123
- const trigger = buildTrigger(reminder);
126
+ const trigger = buildTrigger(tempEnabledReminder);
124
127
  const notificationId = await scheduler.scheduleNotification({
125
128
  title: reminder.title,
126
129
  body: reminder.body,
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { RatingConfig, RatingState } from "../../domain/entities/RatingConfig";
7
+ import { isDev } from "../../../../utils/devUtils";
7
8
  import {
8
9
  getEventCount,
9
10
  incrementEventCount,
@@ -40,7 +41,9 @@ export async function trackEvent(eventType: string): Promise<void> {
40
41
  try {
41
42
  await incrementEventCount(eventType);
42
43
  } catch (error) {
43
- console.error('[RatingService] Failed to track event:', eventType, error);
44
+ if (isDev()) {
45
+ console.error('[RatingService] Failed to track event:', eventType, error);
46
+ }
44
47
  }
45
48
  }
46
49
 
@@ -90,7 +93,9 @@ export async function markPromptShown(eventType: string): Promise<void> {
90
93
  try {
91
94
  await setLastPromptDate(eventType, toISOString());
92
95
  } catch (error) {
93
- console.error('[RatingService] Failed to mark prompt shown:', eventType, error);
96
+ if (isDev()) {
97
+ console.error('[RatingService] Failed to mark prompt shown:', eventType, error);
98
+ }
94
99
  }
95
100
  }
96
101
 
@@ -101,7 +106,9 @@ export async function markRated(): Promise<void> {
101
106
  try {
102
107
  await setHasRated(true);
103
108
  } catch (error) {
104
- console.error('[RatingService] Failed to mark as rated:', error);
109
+ if (isDev()) {
110
+ console.error('[RatingService] Failed to mark as rated:', error);
111
+ }
105
112
  }
106
113
  }
107
114
 
@@ -112,7 +119,9 @@ export async function markDismissed(): Promise<void> {
112
119
  try {
113
120
  await setDismissed(true);
114
121
  } catch (error) {
115
- console.error('[RatingService] Failed to mark as dismissed:', error);
122
+ if (isDev()) {
123
+ console.error('[RatingService] Failed to mark as dismissed:', error);
124
+ }
116
125
  }
117
126
  }
118
127
 
@@ -130,6 +139,8 @@ export async function reset(eventType?: string): Promise<void> {
130
139
  try {
131
140
  await resetStorage(eventType);
132
141
  } catch (error) {
133
- console.error('[RatingService] Failed to reset:', eventType, error);
142
+ if (isDev()) {
143
+ console.error('[RatingService] Failed to reset:', eventType, error);
144
+ }
134
145
  }
135
146
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { storageRepository, unwrap } from "@umituz/react-native-design-system/storage";
7
+ import { isDev } from "../../../../utils/devUtils";
7
8
  import type { RatingState } from "../../domain/entities/RatingConfig";
8
9
 
9
10
  /**
@@ -36,7 +37,9 @@ export async function setEventCount(eventType: string, count: number): Promise<v
36
37
  try {
37
38
  await storageRepository.setString(KEYS.eventCount(eventType), count.toString());
38
39
  } catch (error) {
39
- console.error('[RatingStorage] Failed to set event count:', eventType, count, error);
40
+ if (isDev()) {
41
+ console.error('[RatingStorage] Failed to set event count:', eventType, count, error);
42
+ }
40
43
  }
41
44
  }
42
45
 
@@ -78,7 +81,9 @@ export async function setLastPromptDate(eventType: string, date: string): Promis
78
81
  try {
79
82
  await storageRepository.setString(KEYS.lastPrompt(eventType), date);
80
83
  } catch (error) {
81
- console.error('[RatingStorage] Failed to set last prompt date:', eventType, date, error);
84
+ if (isDev()) {
85
+ console.error('[RatingStorage] Failed to set last prompt date:', eventType, date, error);
86
+ }
82
87
  }
83
88
  }
84
89
 
@@ -101,7 +106,9 @@ export async function setHasRated(value: boolean): Promise<void> {
101
106
  try {
102
107
  await storageRepository.setString(KEYS.hasRated, value.toString());
103
108
  } catch (error) {
104
- console.error('[RatingStorage] Failed to set has rated:', value, error);
109
+ if (isDev()) {
110
+ console.error('[RatingStorage] Failed to set has rated:', value, error);
111
+ }
105
112
  }
106
113
  }
107
114
 
@@ -124,7 +131,9 @@ export async function setDismissed(value: boolean): Promise<void> {
124
131
  try {
125
132
  await storageRepository.setString(KEYS.dismissed, value.toString());
126
133
  } catch (error) {
127
- console.error('[RatingStorage] Failed to set dismissed:', value, error);
134
+ if (isDev()) {
135
+ console.error('[RatingStorage] Failed to set dismissed:', value, error);
136
+ }
128
137
  }
129
138
  }
130
139
 
@@ -165,6 +174,8 @@ export async function reset(eventType?: string): Promise<void> {
165
174
  );
166
175
  }
167
176
  } catch (error) {
168
- console.error('[RatingStorage] Failed to reset rating data:', eventType, error);
177
+ if (isDev()) {
178
+ console.error('[RatingStorage] Failed to reset rating data:', eventType, error);
179
+ }
169
180
  }
170
181
  }
@@ -9,7 +9,7 @@
9
9
  import { useMemo } from "react";
10
10
  import { useAuth, useUserProfile, useAuthHandlers } from "@umituz/react-native-auth";
11
11
  import { createUserProfileDisplay } from "../utils/userProfileUtils";
12
- import { createAccountConfig } from "../utils/accountConfigUtils";
12
+ import { createAccountConfig, type AccountTranslations } from "../utils/accountConfigUtils";
13
13
  import { useSettingsConfigFactory } from "../utils/settingsConfigFactory";
14
14
  import type { SettingsConfig, SettingsTranslations } from "../screens/types";
15
15
  import type { FeedbackFormData } from "../utils/config-creators";
@@ -147,7 +147,15 @@ export const useSettingsScreenConfig = (
147
147
  onSignIn: handleSignIn,
148
148
  onLogout: handleSignOut,
149
149
  onDeleteAccount: handleDeleteAccount,
150
- translations: translations?.account as any,
150
+ translations: (translations?.account?.logout &&
151
+ translations?.account?.deleteAccount &&
152
+ translations?.account?.logoutConfirmTitle &&
153
+ translations?.account?.logoutConfirmMessage &&
154
+ translations?.account?.deleteConfirmTitle &&
155
+ translations?.account?.deleteConfirmMessage &&
156
+ translations?.account?.cancel)
157
+ ? translations.account as AccountTranslations
158
+ : undefined,
151
159
  }), [user, userProfileData, handleSignIn, handleSignOut, handleDeleteAccount, translations]);
152
160
 
153
161
  return {
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { useCallback, useMemo } from "react";
7
7
  import { Linking } from "react-native";
8
+ import { isDev } from "../../../utils/devUtils";
8
9
  import type { AppInfo, LegalUrls } from "../types";
9
10
  import type { AboutConfig } from "../../../domains/about";
10
11
 
@@ -26,10 +27,14 @@ export const useNavigationHandlers = (
26
27
  if (canOpen) {
27
28
  await Linking.openURL(legalUrls.privacy);
28
29
  } else {
29
- console.warn('Cannot open privacy policy URL:', legalUrls.privacy);
30
+ if (isDev()) {
31
+ console.warn('Cannot open privacy policy URL:', legalUrls.privacy);
32
+ }
30
33
  }
31
34
  } catch (error) {
32
- console.error('Failed to open privacy policy:', error);
35
+ if (isDev()) {
36
+ console.error('Failed to open privacy policy:', error);
37
+ }
33
38
  }
34
39
  }, [legalUrls.privacy]);
35
40
 
@@ -39,10 +44,14 @@ export const useNavigationHandlers = (
39
44
  if (canOpen) {
40
45
  await Linking.openURL(legalUrls.terms);
41
46
  } else {
42
- console.warn('Cannot open terms of service URL:', legalUrls.terms);
47
+ if (isDev()) {
48
+ console.warn('Cannot open terms of service URL:', legalUrls.terms);
49
+ }
43
50
  }
44
51
  } catch (error) {
45
- console.error('Failed to open terms of service:', error);
52
+ if (isDev()) {
53
+ console.error('Failed to open terms of service:', error);
54
+ }
46
55
  }
47
56
  }, [legalUrls.terms]);
48
57
 
@@ -53,10 +62,14 @@ export const useNavigationHandlers = (
53
62
  if (canOpen) {
54
63
  await Linking.openURL(legalUrls.eula);
55
64
  } else {
56
- console.warn('Cannot open EULA URL:', legalUrls.eula);
65
+ if (isDev()) {
66
+ console.warn('Cannot open EULA URL:', legalUrls.eula);
67
+ }
57
68
  }
58
69
  } catch (error) {
59
- console.error('Failed to open EULA:', error);
70
+ if (isDev()) {
71
+ console.error('Failed to open EULA:', error);
72
+ }
60
73
  }
61
74
  }
62
75
  }, [legalUrls.eula]);
@@ -84,7 +84,7 @@ export const useSettingsScreens = (props: UseSettingsScreensProps): StackScreen[
84
84
 
85
85
  const appearanceScreen = {
86
86
  name: "Appearance",
87
- component: AppearanceScreen as any,
87
+ component: AppearanceScreen,
88
88
  options: { headerShown: false },
89
89
  };
90
90
 
@@ -121,7 +121,7 @@ export const useSettingsScreens = (props: UseSettingsScreensProps): StackScreen[
121
121
 
122
122
  const additionalStackScreens: StackScreen[] = (additionalScreens || []).map(convertAdditionalScreen);
123
123
 
124
- const gamificationScreen = createScreenWithProps("Gamification", GamificationScreen as any, { config: gamificationConfig });
124
+ const gamificationScreen = createScreenWithProps("Gamification", GamificationScreen, { config: gamificationConfig });
125
125
 
126
126
  const languageScreen = createScreenWithProps("LanguageSelection", LanguageSelectionScreen, {
127
127
  headerTitle: featureTranslations?.language?.title || "",
@@ -130,10 +130,10 @@ export const useSettingsScreens = (props: UseSettingsScreensProps): StackScreen[
130
130
 
131
131
  const accountScreen = createConditionalScreen(
132
132
  !!accountConfig && !!AccountScreen,
133
- () => createScreenWithProps("Account", AccountScreen as any, { config: accountConfig })
133
+ () => createScreenWithProps("Account", AccountScreen, { config: accountConfig })
134
134
  );
135
135
 
136
- const videoTutorialScreen = createScreenWithProps("VideoTutorial", VideoTutorialsScreen as any, {
136
+ const videoTutorialScreen = createScreenWithProps("VideoTutorial", VideoTutorialsScreen, {
137
137
  ...videoTutorialConfig,
138
138
  title: videoTutorialConfig?.title || featureTranslations?.videoTutorial?.title || "",
139
139
  });
@@ -20,7 +20,7 @@ export function createScreenWithProps<P>(
20
20
  return {
21
21
  name,
22
22
  options: { headerShown: false, ...options },
23
- children: (() => React.createElement(component as React.ComponentType, props as any)) as () => React.ReactElement,
23
+ children: (() => React.createElement(component, props)) as () => React.ReactElement,
24
24
  };
25
25
  }
26
26
 
@@ -28,11 +28,13 @@ export function createScreenWithProps<P>(
28
28
  * Convert additional screen to stack screen
29
29
  */
30
30
  export function convertAdditionalScreen(screen: AdditionalScreen): StackScreen {
31
- const stackScreen: Partial<StackScreen> = { name: screen.name };
32
- if (screen.component) stackScreen.component = screen.component;
33
- if (screen.children) stackScreen.children = screen.children as any;
34
- if (screen.options) stackScreen.options = screen.options;
35
- return stackScreen as StackScreen;
31
+ const stackScreen: StackScreen = {
32
+ name: screen.name,
33
+ component: screen.component,
34
+ children: screen.children,
35
+ options: screen.options,
36
+ };
37
+ return stackScreen;
36
38
  }
37
39
 
38
40
  /**