@oxyhq/services 5.13.21 → 5.13.23
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/lib/commonjs/core/OxyServices.base.js +34 -21
- package/lib/commonjs/core/OxyServices.base.js.map +1 -1
- package/lib/commonjs/ui/components/ErrorBoundary.js +145 -0
- package/lib/commonjs/ui/components/ErrorBoundary.js.map +1 -0
- package/lib/commonjs/ui/navigation/OxyRouter.js +83 -30
- package/lib/commonjs/ui/navigation/OxyRouter.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountCenterScreen.js +19 -14
- package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/SessionManagementScreen.js +90 -77
- package/lib/commonjs/ui/screens/SessionManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/SignInScreen.js +1 -1
- package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
- package/lib/module/core/OxyServices.base.js +34 -21
- package/lib/module/core/OxyServices.base.js.map +1 -1
- package/lib/module/ui/components/ErrorBoundary.js +139 -0
- package/lib/module/ui/components/ErrorBoundary.js.map +1 -0
- package/lib/module/ui/navigation/OxyRouter.js +83 -31
- package/lib/module/ui/navigation/OxyRouter.js.map +1 -1
- package/lib/module/ui/screens/AccountCenterScreen.js +19 -14
- package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
- package/lib/module/ui/screens/SessionManagementScreen.js +91 -78
- package/lib/module/ui/screens/SessionManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/SignInScreen.js +1 -1
- package/lib/module/ui/screens/SignInScreen.js.map +1 -1
- package/lib/typescript/core/OxyServices.base.d.ts +16 -10
- package/lib/typescript/core/OxyServices.base.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.analytics.d.ts +0 -2
- package/lib/typescript/core/mixins/OxyServices.analytics.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.assets.d.ts +0 -2
- package/lib/typescript/core/mixins/OxyServices.assets.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.auth.d.ts +0 -2
- package/lib/typescript/core/mixins/OxyServices.auth.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.developer.d.ts +0 -2
- package/lib/typescript/core/mixins/OxyServices.developer.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.devices.d.ts +0 -2
- package/lib/typescript/core/mixins/OxyServices.devices.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.karma.d.ts +0 -2
- package/lib/typescript/core/mixins/OxyServices.karma.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.language.d.ts +0 -2
- package/lib/typescript/core/mixins/OxyServices.language.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.location.d.ts +0 -2
- package/lib/typescript/core/mixins/OxyServices.location.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.payment.d.ts +0 -2
- package/lib/typescript/core/mixins/OxyServices.payment.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.privacy.d.ts +0 -2
- package/lib/typescript/core/mixins/OxyServices.privacy.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.totp.d.ts +0 -2
- package/lib/typescript/core/mixins/OxyServices.totp.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.user.d.ts +0 -2
- package/lib/typescript/core/mixins/OxyServices.user.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.utility.d.ts +0 -2
- package/lib/typescript/core/mixins/OxyServices.utility.d.ts.map +1 -1
- package/lib/typescript/core/mixins/index.d.ts +0 -26
- package/lib/typescript/core/mixins/index.d.ts.map +1 -1
- package/lib/typescript/ui/components/ErrorBoundary.d.ts +31 -0
- package/lib/typescript/ui/components/ErrorBoundary.d.ts.map +1 -0
- package/lib/typescript/ui/navigation/OxyRouter.d.ts +3 -1
- package/lib/typescript/ui/navigation/OxyRouter.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountCenterScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/SessionManagementScreen.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/OxyServices.base.ts +37 -22
- package/src/ui/components/ErrorBoundary.tsx +154 -0
- package/src/ui/navigation/OxyRouter.tsx +90 -29
- package/src/ui/screens/AccountCenterScreen.tsx +17 -14
- package/src/ui/screens/SessionManagementScreen.tsx +81 -69
- package/src/ui/screens/SignInScreen.tsx +1 -1
|
@@ -1,13 +1,48 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { useState, useEffect, useCallback, memo } from 'react';
|
|
3
3
|
import { View, StyleSheet } from 'react-native';
|
|
4
4
|
import { OxyServices } from '../../core';
|
|
5
|
+
import ErrorBoundary from '../components/ErrorBoundary';
|
|
5
6
|
|
|
6
7
|
// Import types and route registry
|
|
7
8
|
import type { OxyRouterProps } from './types';
|
|
8
|
-
import { routes } from './routes';
|
|
9
|
+
import { routes, routeNames } from './routes';
|
|
9
10
|
import type { RouteName } from './routes';
|
|
10
11
|
|
|
12
|
+
// Helper function to validate route names at runtime
|
|
13
|
+
const isValidRouteName = (screen: string): screen is RouteName => {
|
|
14
|
+
return routeNames.includes(screen as RouteName);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Helper function for safe navigation with validation
|
|
18
|
+
const validateAndNavigate = (
|
|
19
|
+
screen: string,
|
|
20
|
+
props: Record<string, any>,
|
|
21
|
+
setCurrentScreen: (screen: RouteName) => void,
|
|
22
|
+
setScreenHistory: React.Dispatch<React.SetStateAction<RouteName[]>>,
|
|
23
|
+
setScreenPropsMap: React.Dispatch<React.SetStateAction<Partial<Record<RouteName, any>>>>
|
|
24
|
+
): boolean => {
|
|
25
|
+
if (!isValidRouteName(screen)) {
|
|
26
|
+
const errorMsg = `Invalid route name: "${screen}". Valid routes are: ${routeNames.join(', ')}`;
|
|
27
|
+
console.error('OxyRouter:', errorMsg);
|
|
28
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
29
|
+
console.error('Navigation error:', errorMsg);
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!routes[screen]) {
|
|
35
|
+
const errorMsg = `Route "${screen}" is registered but component is missing`;
|
|
36
|
+
console.error('OxyRouter:', errorMsg);
|
|
37
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
38
|
+
console.error('Navigation error:', errorMsg);
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
};
|
|
45
|
+
|
|
11
46
|
const OxyRouter: React.FC<OxyRouterProps> = ({
|
|
12
47
|
oxyServices,
|
|
13
48
|
initialScreen,
|
|
@@ -30,20 +65,19 @@ const OxyRouter: React.FC<OxyRouterProps> = ({
|
|
|
30
65
|
}
|
|
31
66
|
}, [currentScreen, adjustSnapPoints]);
|
|
32
67
|
|
|
33
|
-
// Memoized navigation methods
|
|
68
|
+
// Memoized navigation methods with validation
|
|
34
69
|
const navigate = useCallback((screen: RouteName, props: Record<string, any> = {}) => {
|
|
35
70
|
if (__DEV__) console.log('OxyRouter: navigate called', screen, props);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
setScreenPropsMap(prev => ({ ...prev, [screen]: props }));
|
|
41
|
-
} else {
|
|
42
|
-
console.error(`OxyRouter: Screen "${screen}" not found in routes:`, Object.keys(routes));
|
|
43
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
44
|
-
console.error(`Screen "${screen}" not found`);
|
|
45
|
-
}
|
|
71
|
+
|
|
72
|
+
// Validate route before navigating
|
|
73
|
+
if (!validateAndNavigate(screen, props, setCurrentScreen, setScreenHistory, setScreenPropsMap)) {
|
|
74
|
+
return; // Early return if validation fails
|
|
46
75
|
}
|
|
76
|
+
|
|
77
|
+
// All validations passed, proceed with navigation
|
|
78
|
+
setCurrentScreen(screen);
|
|
79
|
+
setScreenHistory(prev => [...prev, screen]);
|
|
80
|
+
setScreenPropsMap(prev => ({ ...prev, [screen]: props }));
|
|
47
81
|
}, []);
|
|
48
82
|
|
|
49
83
|
const goBack = useCallback(() => {
|
|
@@ -80,10 +114,20 @@ const OxyRouter: React.FC<OxyRouterProps> = ({
|
|
|
80
114
|
const handleNavigationEvent = (event: any) => {
|
|
81
115
|
if (event && event.detail) {
|
|
82
116
|
if (typeof event.detail === 'string') {
|
|
83
|
-
|
|
117
|
+
// Validate string route name before navigating
|
|
118
|
+
if (isValidRouteName(event.detail)) {
|
|
119
|
+
navigate(event.detail as RouteName);
|
|
120
|
+
} else {
|
|
121
|
+
console.error('OxyRouter: Invalid route name in event:', event.detail);
|
|
122
|
+
}
|
|
84
123
|
} else if (typeof event.detail === 'object' && event.detail.screen) {
|
|
85
124
|
const { screen, props } = event.detail;
|
|
86
|
-
|
|
125
|
+
// Validate route name before navigating
|
|
126
|
+
if (isValidRouteName(screen)) {
|
|
127
|
+
navigate(screen as RouteName, props || {});
|
|
128
|
+
} else {
|
|
129
|
+
console.error('OxyRouter: Invalid route name in event:', screen);
|
|
130
|
+
}
|
|
87
131
|
}
|
|
88
132
|
}
|
|
89
133
|
};
|
|
@@ -95,8 +139,14 @@ const OxyRouter: React.FC<OxyRouterProps> = ({
|
|
|
95
139
|
intervalId = setInterval(() => {
|
|
96
140
|
const globalNav = (globalThis as any).oxyNavigateEvent;
|
|
97
141
|
if (globalNav && globalNav.screen) {
|
|
98
|
-
|
|
99
|
-
(
|
|
142
|
+
// Validate route name before navigating
|
|
143
|
+
if (isValidRouteName(globalNav.screen)) {
|
|
144
|
+
navigate(globalNav.screen as RouteName, globalNav.props || {});
|
|
145
|
+
(globalThis as any).oxyNavigateEvent = null;
|
|
146
|
+
} else {
|
|
147
|
+
console.error('OxyRouter: Invalid route name in global event:', globalNav.screen);
|
|
148
|
+
(globalThis as any).oxyNavigateEvent = null; // Clear invalid event
|
|
149
|
+
}
|
|
100
150
|
}
|
|
101
151
|
}, 100);
|
|
102
152
|
}
|
|
@@ -110,7 +160,7 @@ const OxyRouter: React.FC<OxyRouterProps> = ({
|
|
|
110
160
|
};
|
|
111
161
|
}, [navigate]);
|
|
112
162
|
|
|
113
|
-
// Render the current screen component
|
|
163
|
+
// Render the current screen component with error boundary
|
|
114
164
|
const renderScreen = () => {
|
|
115
165
|
const CurrentScreen = (routes as any)[currentScreen]?.component;
|
|
116
166
|
if (!CurrentScreen) {
|
|
@@ -120,16 +170,22 @@ const OxyRouter: React.FC<OxyRouterProps> = ({
|
|
|
120
170
|
return <View style={styles.errorContainer} />;
|
|
121
171
|
}
|
|
122
172
|
return (
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
173
|
+
<ErrorBoundary
|
|
174
|
+
onError={(error, errorInfo) => {
|
|
175
|
+
console.error(`Error in screen "${currentScreen}":`, error, errorInfo);
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
<CurrentScreen
|
|
179
|
+
oxyServices={oxyServices}
|
|
180
|
+
navigate={navigate}
|
|
181
|
+
goBack={goBack}
|
|
182
|
+
onClose={onClose}
|
|
183
|
+
onAuthenticated={onAuthenticated}
|
|
184
|
+
theme={theme}
|
|
185
|
+
containerWidth={containerWidth}
|
|
186
|
+
{...(screenPropsMap[currentScreen] || {})}
|
|
187
|
+
/>
|
|
188
|
+
</ErrorBoundary>
|
|
133
189
|
);
|
|
134
190
|
};
|
|
135
191
|
|
|
@@ -140,6 +196,9 @@ const OxyRouter: React.FC<OxyRouterProps> = ({
|
|
|
140
196
|
);
|
|
141
197
|
};
|
|
142
198
|
|
|
199
|
+
// Memoize the router component to prevent unnecessary re-renders
|
|
200
|
+
const MemoizedOxyRouter = memo(OxyRouter);
|
|
201
|
+
|
|
143
202
|
const styles = StyleSheet.create({
|
|
144
203
|
container: {
|
|
145
204
|
flex: 1,
|
|
@@ -152,4 +211,6 @@ const styles = StyleSheet.create({
|
|
|
152
211
|
},
|
|
153
212
|
});
|
|
154
213
|
|
|
155
|
-
|
|
214
|
+
// Export both the memoized version (default) and the original for testing
|
|
215
|
+
export { OxyRouter };
|
|
216
|
+
export default MemoizedOxyRouter;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
175
|
-
const sessionItems =
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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:
|
|
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' ||
|
|
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) => {
|