@oxyhq/services 5.13.22 → 5.13.24

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.
Files changed (36) hide show
  1. package/lib/commonjs/core/mixins/OxyServices.assets.js +1 -1
  2. package/lib/commonjs/ui/components/ErrorBoundary.js +145 -0
  3. package/lib/commonjs/ui/components/ErrorBoundary.js.map +1 -0
  4. package/lib/commonjs/ui/navigation/OxyRouter.js +83 -30
  5. package/lib/commonjs/ui/navigation/OxyRouter.js.map +1 -1
  6. package/lib/commonjs/ui/screens/AccountCenterScreen.js +19 -14
  7. package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
  8. package/lib/commonjs/ui/screens/SessionManagementScreen.js +90 -77
  9. package/lib/commonjs/ui/screens/SessionManagementScreen.js.map +1 -1
  10. package/lib/commonjs/ui/screens/SignInScreen.js +1 -1
  11. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  12. package/lib/module/core/mixins/OxyServices.assets.js +1 -1
  13. package/lib/module/ui/components/ErrorBoundary.js +139 -0
  14. package/lib/module/ui/components/ErrorBoundary.js.map +1 -0
  15. package/lib/module/ui/navigation/OxyRouter.js +83 -31
  16. package/lib/module/ui/navigation/OxyRouter.js.map +1 -1
  17. package/lib/module/ui/screens/AccountCenterScreen.js +19 -14
  18. package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
  19. package/lib/module/ui/screens/SessionManagementScreen.js +91 -78
  20. package/lib/module/ui/screens/SessionManagementScreen.js.map +1 -1
  21. package/lib/module/ui/screens/SignInScreen.js +1 -1
  22. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  23. package/lib/typescript/core/mixins/OxyServices.assets.d.ts.map +1 -1
  24. package/lib/typescript/ui/components/ErrorBoundary.d.ts +31 -0
  25. package/lib/typescript/ui/components/ErrorBoundary.d.ts.map +1 -0
  26. package/lib/typescript/ui/navigation/OxyRouter.d.ts +3 -1
  27. package/lib/typescript/ui/navigation/OxyRouter.d.ts.map +1 -1
  28. package/lib/typescript/ui/screens/AccountCenterScreen.d.ts.map +1 -1
  29. package/lib/typescript/ui/screens/SessionManagementScreen.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/src/core/mixins/OxyServices.assets.ts +1 -1
  32. package/src/ui/components/ErrorBoundary.tsx +154 -0
  33. package/src/ui/navigation/OxyRouter.tsx +90 -29
  34. package/src/ui/screens/AccountCenterScreen.tsx +17 -14
  35. package/src/ui/screens/SessionManagementScreen.tsx +81 -69
  36. package/src/ui/screens/SignInScreen.tsx +1 -1
@@ -9,6 +9,7 @@ import {
9
9
  Alert,
10
10
  Platform,
11
11
  } from 'react-native';
12
+ import { useCallback, useMemo } from 'react';
12
13
  import type { BaseScreenProps } from '../navigation/types';
13
14
  import { useOxy } from '../context/OxyContext';
14
15
  import { packageInfo } from '../../constants/version';
@@ -39,7 +40,8 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
39
40
  const primaryColor = '#0066CC';
40
41
  const dangerColor = '#D32F2F';
41
42
 
42
- const handleLogout = async () => {
43
+ // Memoized logout handler - prevents unnecessary re-renders
44
+ const handleLogout = useCallback(async () => {
43
45
  try {
44
46
  await logout();
45
47
  if (onClose) {
@@ -49,14 +51,15 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
49
51
  console.error('Logout failed:', error);
50
52
  toast.error(t('common.errors.signOutFailed') || 'There was a problem signing you out. Please try again.');
51
53
  }
52
- };
54
+ }, [logout, onClose, t]);
53
55
 
54
- const confirmLogout = () => {
56
+ // Memoized confirm logout handler - prevents unnecessary re-renders
57
+ const confirmLogout = useCallback(() => {
55
58
  confirmAction(
56
59
  t('common.confirms.signOut') || 'Are you sure you want to sign out?',
57
60
  handleLogout
58
61
  );
59
- };
62
+ }, [handleLogout, t]);
60
63
 
61
64
  if (!isAuthenticated) {
62
65
  return (
@@ -91,14 +94,14 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
91
94
  {/* Quick Actions */}
92
95
  <Section title={t('accountCenter.sections.quickActions') || 'Quick Actions'} theme={theme} isFirst={true}>
93
96
  <QuickActions
94
- actions={[
97
+ actions={useMemo(() => [
95
98
  { id: 'overview', icon: 'person-circle', iconColor: '#007AFF', title: t('accountCenter.quickActions.overview') || 'Overview', onPress: () => navigate('AccountOverview') },
96
99
  { id: 'settings', icon: 'settings', iconColor: '#5856D6', title: t('accountCenter.quickActions.editProfile') || 'Edit Profile', onPress: () => navigate('EditProfile') },
97
100
  { id: 'sessions', icon: 'shield-checkmark', iconColor: '#30D158', title: t('accountCenter.quickActions.sessions') || 'Sessions', onPress: () => navigate('SessionManagement') },
98
101
  { id: 'premium', icon: 'star', iconColor: '#FFD700', title: t('accountCenter.quickActions.premium') || 'Premium', onPress: () => navigate('PremiumSubscription') },
99
102
  ...(user?.isPremium ? [{ id: 'billing', icon: 'card', iconColor: '#34C759', title: t('accountCenter.quickActions.billing') || 'Billing', onPress: () => navigate('PaymentGateway') }] : []),
100
103
  ...(sessions && sessions.length > 1 ? [{ id: 'switch', icon: 'swap-horizontal', iconColor: '#FF9500', title: t('accountCenter.quickActions.switch') || 'Switch', onPress: () => navigate('AccountSwitcher') }] : []),
101
- ]}
104
+ ], [user?.isPremium, sessions, navigate, t])}
102
105
  theme={theme}
103
106
  />
104
107
  </Section>
@@ -106,7 +109,7 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
106
109
  {/* Account Management */}
107
110
  <Section title={t('accountCenter.sections.accountManagement') || 'Account Management'} theme={theme}>
108
111
  <GroupedSection
109
- items={[
112
+ items={useMemo(() => [
110
113
  {
111
114
  id: 'overview',
112
115
  icon: 'person-circle',
@@ -155,7 +158,7 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
155
158
  subtitle: t('accountCenter.items.billing.subtitle') || 'Payment methods and invoices',
156
159
  onPress: () => navigate('PaymentGateway'),
157
160
  }] : []),
158
- ]}
161
+ ], [user?.isPremium, navigate, t])}
159
162
  theme={theme}
160
163
  />
161
164
  </Section>
@@ -164,7 +167,7 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
164
167
  {sessions && sessions.length > 1 && (
165
168
  <Section title={t('accountCenter.sections.multiAccount') || 'Multi-Account'} theme={theme}>
166
169
  <GroupedSection
167
- items={[
170
+ items={useMemo(() => [
168
171
  {
169
172
  id: 'switch',
170
173
  icon: 'people',
@@ -181,7 +184,7 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
181
184
  subtitle: t('accountCenter.items.addAccount.subtitle') || 'Sign in with a different account',
182
185
  onPress: () => navigate('SignIn'),
183
186
  },
184
- ]}
187
+ ], [sessions.length, navigate, t])}
185
188
  theme={theme}
186
189
  />
187
190
  </Section>
@@ -191,7 +194,7 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
191
194
  {(!sessions || sessions.length <= 1) && (
192
195
  <Section title={t('accountCenter.sections.addAccount') || 'Add Account'} theme={theme}>
193
196
  <GroupedSection
194
- items={[
197
+ items={useMemo(() => [
195
198
  {
196
199
  id: 'add',
197
200
  icon: 'person-add',
@@ -200,7 +203,7 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
200
203
  subtitle: t('accountCenter.items.addAccount.subtitle') || 'Sign in with a different account',
201
204
  onPress: () => navigate('SignIn'),
202
205
  },
203
- ]}
206
+ ], [navigate, t])}
204
207
  theme={theme}
205
208
  />
206
209
  </Section>
@@ -209,7 +212,7 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
209
212
  {/* Additional Options */}
210
213
  <Section title={t('accountCenter.sections.moreOptions') || 'More Options'} theme={theme}>
211
214
  <GroupedSection
212
- items={[
215
+ items={useMemo(() => [
213
216
  ...(Platform.OS !== 'web' ? [{
214
217
  id: 'notifications',
215
218
  icon: 'notifications',
@@ -242,7 +245,7 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
242
245
  subtitle: t('accountCenter.items.appInfo.subtitle') || 'Version and system details',
243
246
  onPress: () => navigate('AppInfo'),
244
247
  },
245
- ]}
248
+ ], [navigate, t])}
246
249
  theme={theme}
247
250
  />
248
251
  </Section>
@@ -1,5 +1,5 @@
1
1
  import type React from 'react';
2
- import { useState, useEffect } from 'react';
2
+ import { useState, useEffect, useCallback, useMemo } from 'react';
3
3
  import {
4
4
  View,
5
5
  Text,
@@ -39,7 +39,8 @@ const SessionManagementScreen: React.FC<BaseScreenProps> = ({
39
39
  const dangerColor = '#D32F2F';
40
40
  const successColor = '#2E7D32';
41
41
 
42
- const loadSessions = async (isRefresh = false) => {
42
+ // Memoized load sessions function - prevents unnecessary re-renders
43
+ const loadSessions = useCallback(async (isRefresh = false) => {
43
44
  try {
44
45
  if (isRefresh) {
45
46
  setRefreshing(true);
@@ -64,9 +65,10 @@ const SessionManagementScreen: React.FC<BaseScreenProps> = ({
64
65
  setLoading(false);
65
66
  setRefreshing(false);
66
67
  }
67
- };
68
+ }, [refreshSessions]);
68
69
 
69
- const handleLogoutSession = async (sessionId: string) => {
70
+ // Memoized logout session handler - prevents unnecessary re-renders
71
+ const handleLogoutSession = useCallback(async (sessionId: string) => {
70
72
  confirmAction('Are you sure you want to logout this session?', async () => {
71
73
  try {
72
74
  setActionLoading(sessionId);
@@ -80,9 +82,10 @@ const SessionManagementScreen: React.FC<BaseScreenProps> = ({
80
82
  setActionLoading(null);
81
83
  }
82
84
  });
83
- };
85
+ }, [logout, refreshSessions]);
84
86
 
85
- const handleLogoutOtherSessions = async () => {
87
+ // Memoized logout other sessions handler - prevents unnecessary re-renders
88
+ const handleLogoutOtherSessions = useCallback(async () => {
86
89
  const otherSessionsCount = userSessions.filter(s => s.sessionId !== activeSessionId).length;
87
90
  if (otherSessionsCount === 0) {
88
91
  toast.info('No other sessions to logout.');
@@ -108,9 +111,10 @@ const SessionManagementScreen: React.FC<BaseScreenProps> = ({
108
111
  }
109
112
  }
110
113
  );
111
- };
114
+ }, [userSessions, activeSessionId, logout, refreshSessions]);
112
115
 
113
- const handleLogoutAllSessions = async () => {
116
+ // Memoized logout all sessions handler - prevents unnecessary re-renders
117
+ const handleLogoutAllSessions = useCallback(async () => {
114
118
  confirmAction(
115
119
  'This will logout all sessions including this one and you will need to sign in again. Continue?',
116
120
  async () => {
@@ -124,10 +128,10 @@ const SessionManagementScreen: React.FC<BaseScreenProps> = ({
124
128
  }
125
129
  }
126
130
  );
127
- };
131
+ }, [logoutAll]);
128
132
 
129
- // Relative time formatter (past & future)
130
- const formatRelative = (dateString?: string) => {
133
+ // Memoized relative time formatter - prevents function recreation on every render
134
+ const formatRelative = useCallback((dateString?: string) => {
131
135
  if (!dateString) return 'Unknown';
132
136
  const date = new Date(dateString);
133
137
  const now = new Date();
@@ -142,9 +146,10 @@ const SessionManagementScreen: React.FC<BaseScreenProps> = ({
142
146
  const days = hrs / 24;
143
147
  if (days < 7) return isFuture ? `in ${fmt(days)}d` : `${fmt(days)}d ago`;
144
148
  return date.toLocaleDateString();
145
- };
149
+ }, []);
146
150
 
147
- const handleSwitchSession = async (sessionId: string) => {
151
+ // Memoized switch session handler - prevents unnecessary re-renders
152
+ const handleSwitchSession = useCallback(async (sessionId: string) => {
148
153
  if (sessionId === activeSessionId) return;
149
154
  setSwitchLoading(sessionId);
150
155
  try {
@@ -156,11 +161,11 @@ const SessionManagementScreen: React.FC<BaseScreenProps> = ({
156
161
  } finally {
157
162
  setSwitchLoading(null);
158
163
  }
159
- };
164
+ }, [activeSessionId, switchSession]);
160
165
 
161
166
  useEffect(() => {
162
167
  loadSessions();
163
- }, []);
168
+ }, [loadSessions]);
164
169
 
165
170
  if (loading) {
166
171
  return (
@@ -171,69 +176,76 @@ const SessionManagementScreen: React.FC<BaseScreenProps> = ({
171
176
  );
172
177
  }
173
178
 
174
- // Build grouped section items for sessions
175
- const sessionItems = userSessions.map((session: ClientSession) => {
176
- const isCurrent = session.sessionId === activeSessionId;
177
- const subtitleParts: string[] = [];
178
- if (session.deviceId) subtitleParts.push(`Device ${session.deviceId.substring(0, 10)}...`);
179
- subtitleParts.push(`Last ${formatRelative(session.lastActive)}`);
180
- subtitleParts.push(`Expires ${formatRelative(session.expiresAt)}`);
179
+ // Memoized session items - prevents unnecessary re-renders when dependencies haven't changed
180
+ const sessionItems = useMemo(() => {
181
+ return userSessions.map((session: ClientSession) => {
182
+ const isCurrent = session.sessionId === activeSessionId;
183
+ const subtitleParts: string[] = [];
184
+ if (session.deviceId) subtitleParts.push(`Device ${session.deviceId.substring(0, 10)}...`);
185
+ subtitleParts.push(`Last ${formatRelative(session.lastActive)}`);
186
+ subtitleParts.push(`Expires ${formatRelative(session.expiresAt)}`);
181
187
 
182
- return {
183
- id: session.sessionId,
184
- icon: isCurrent ? 'shield-checkmark' : 'laptop-outline',
185
- iconColor: isCurrent ? successColor : primaryColor,
186
- title: isCurrent ? 'Current Session' : `Session ${session.sessionId.substring(0, 8)}...`,
187
- subtitle: subtitleParts.join(' \u2022 '),
188
- showChevron: false,
189
- multiRow: true,
190
- customContentBelow: !isCurrent ? (
191
- <View style={styles.sessionActionsRow}>
192
- <TouchableOpacity
193
- onPress={() => handleSwitchSession(session.sessionId)}
194
- style={[styles.sessionPillButton, { backgroundColor: isDarkTheme ? '#1E2A38' : '#E6F2FF', borderColor: primaryColor }]}
195
- disabled={switchLoading === session.sessionId || actionLoading === session.sessionId}
196
- >
197
- {switchLoading === session.sessionId ? (
198
- <ActivityIndicator size="small" color={primaryColor} />
199
- ) : (
200
- <Text style={[styles.sessionPillText, { color: primaryColor }]}>Switch</Text>
201
- )}
202
- </TouchableOpacity>
203
- <TouchableOpacity
204
- onPress={() => handleLogoutSession(session.sessionId)}
205
- style={[styles.sessionPillButton, { backgroundColor: isDarkTheme ? '#3A1E1E' : '#FFEBEE', borderColor: dangerColor }]}
206
- disabled={actionLoading === session.sessionId || switchLoading === session.sessionId}
207
- >
208
- {actionLoading === session.sessionId ? (
209
- <ActivityIndicator size="small" color={dangerColor} />
210
- ) : (
211
- <Text style={[styles.sessionPillText, { color: dangerColor }]}>Logout</Text>
212
- )}
213
- </TouchableOpacity>
214
- </View>
215
- ) : (
216
- <View style={styles.sessionActionsRow}>
217
- <Text style={[styles.currentBadgeText, { color: successColor }]}>Active</Text>
218
- </View>
219
- ),
220
- selected: isCurrent,
221
- dense: true,
222
- };
223
- });
188
+ return {
189
+ id: session.sessionId,
190
+ icon: isCurrent ? 'shield-checkmark' : 'laptop-outline',
191
+ iconColor: isCurrent ? successColor : primaryColor,
192
+ title: isCurrent ? 'Current Session' : `Session ${session.sessionId.substring(0, 8)}...`,
193
+ subtitle: subtitleParts.join(' \u2022 '),
194
+ showChevron: false,
195
+ multiRow: true,
196
+ customContentBelow: !isCurrent ? (
197
+ <View style={styles.sessionActionsRow}>
198
+ <TouchableOpacity
199
+ onPress={() => handleSwitchSession(session.sessionId)}
200
+ style={[styles.sessionPillButton, { backgroundColor: isDarkTheme ? '#1E2A38' : '#E6F2FF', borderColor: primaryColor }]}
201
+ disabled={switchLoading === session.sessionId || actionLoading === session.sessionId}
202
+ >
203
+ {switchLoading === session.sessionId ? (
204
+ <ActivityIndicator size="small" color={primaryColor} />
205
+ ) : (
206
+ <Text style={[styles.sessionPillText, { color: primaryColor }]}>Switch</Text>
207
+ )}
208
+ </TouchableOpacity>
209
+ <TouchableOpacity
210
+ onPress={() => handleLogoutSession(session.sessionId)}
211
+ style={[styles.sessionPillButton, { backgroundColor: isDarkTheme ? '#3A1E1E' : '#FFEBEE', borderColor: dangerColor }]}
212
+ disabled={actionLoading === session.sessionId || switchLoading === session.sessionId}
213
+ >
214
+ {actionLoading === session.sessionId ? (
215
+ <ActivityIndicator size="small" color={dangerColor} />
216
+ ) : (
217
+ <Text style={[styles.sessionPillText, { color: dangerColor }]}>Logout</Text>
218
+ )}
219
+ </TouchableOpacity>
220
+ </View>
221
+ ) : (
222
+ <View style={styles.sessionActionsRow}>
223
+ <Text style={[styles.currentBadgeText, { color: successColor }]}>Active</Text>
224
+ </View>
225
+ ),
226
+ selected: isCurrent,
227
+ dense: true,
228
+ };
229
+ });
230
+ }, [userSessions, activeSessionId, formatRelative, successColor, primaryColor, isDarkTheme, switchLoading, actionLoading, handleSwitchSession, handleLogoutSession, dangerColor]);
231
+
232
+ // Memoized bulk action items - prevents unnecessary re-renders when dependencies haven't changed
233
+ const otherSessionsCount = useMemo(() =>
234
+ userSessions.filter(s => s.sessionId !== activeSessionId).length,
235
+ [userSessions, activeSessionId]
236
+ );
224
237
 
225
- // Bulk actions as grouped section items
226
- const bulkItems = [
238
+ const bulkItems = useMemo(() => [
227
239
  {
228
240
  id: 'logout-others',
229
241
  icon: 'exit-outline',
230
242
  iconColor: primaryColor,
231
243
  title: 'Logout Other Sessions',
232
- subtitle: userSessions.filter(s => s.sessionId !== activeSessionId).length === 0 ? 'No other sessions' : 'End all sessions except this one',
244
+ subtitle: otherSessionsCount === 0 ? 'No other sessions' : 'End all sessions except this one',
233
245
  onPress: handleLogoutOtherSessions,
234
246
  showChevron: false,
235
247
  customContent: actionLoading === 'others' ? <ActivityIndicator size="small" color={primaryColor} /> : undefined,
236
- disabled: actionLoading === 'others' || userSessions.filter(s => s.sessionId !== activeSessionId).length === 0,
248
+ disabled: actionLoading === 'others' || otherSessionsCount === 0,
237
249
  dense: true,
238
250
  },
239
251
  {
@@ -248,7 +260,7 @@ const SessionManagementScreen: React.FC<BaseScreenProps> = ({
248
260
  disabled: actionLoading === 'all',
249
261
  dense: true,
250
262
  },
251
- ];
263
+ ], [otherSessionsCount, primaryColor, dangerColor, handleLogoutOtherSessions, handleLogoutAllSessions, actionLoading]);
252
264
 
253
265
  return (
254
266
  <View style={[styles.container, { backgroundColor }]}>
@@ -129,7 +129,7 @@ const SignInScreen: React.FC<BaseScreenProps> = ({
129
129
  } finally {
130
130
  setIsValidating(false);
131
131
  }
132
- }, [oxyServices]);
132
+ }, [oxyServices, sessions]);
133
133
 
134
134
  // Input change handlers
135
135
  const handleUsernameChange = useCallback((text: string) => {