UNPKG

@oxyhq/services

Version:

Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀

630 lines (621 loc) • 25.9 kB
"use strict"; import React, { useState, useMemo, useCallback } from 'react'; import { View, Text, TouchableOpacity, StyleSheet, ActivityIndicator, ScrollView, Alert, Platform, Image } from 'react-native'; import { useOxy } from '../context/OxyContext'; import Avatar from '../components/Avatar'; import OxyIcon from '../components/icon/OxyIcon'; import { toast } from '../../lib/sonner'; import { confirmAction } from '../utils/confirmAction'; import { Header, Section, GroupedSection, GroupedItem } from '../components'; import { useI18n } from '../hooks/useI18n'; /** * AccountOverviewScreen - Optimized for performance * * Performance optimizations implemented: * - useMemo for theme calculations (only recalculates when theme changes) * - useMemo for additional accounts filtering (only recalculates when dependencies change) * - useCallback for event handlers to prevent unnecessary re-renders * - React.memo wrapper to prevent re-renders when props haven't changed * - GroupedSection components for better organization and cleaner code */ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; const AccountOverviewScreen = ({ onClose, theme, navigate }) => { const { user, logout, isLoading, sessions, activeSessionId, oxyServices, isAuthenticated } = useOxy(); const { t } = useI18n(); const [showMoreAccounts, setShowMoreAccounts] = useState(false); const [additionalAccountsData, setAdditionalAccountsData] = useState([]); const [loadingAdditionalAccounts, setLoadingAdditionalAccounts] = useState(false); // Memoize theme-related calculations to prevent unnecessary recalculations const themeStyles = useMemo(() => { const isDarkTheme = theme === 'dark'; return { isDarkTheme, textColor: isDarkTheme ? '#FFFFFF' : '#000000', backgroundColor: isDarkTheme ? '#121212' : '#FFFFFF', secondaryBackgroundColor: isDarkTheme ? '#222222' : '#F5F5F5', borderColor: isDarkTheme ? '#444444' : '#E0E0E0', primaryColor: '#d169e5', dangerColor: '#D32F2F', iconColor: isDarkTheme ? '#BBBBBB' : '#666666' }; }, [theme]); // Memoize additional accounts filtering to prevent recalculation on every render const additionalAccounts = useMemo(() => sessions.filter(session => session.sessionId !== activeSessionId && session.userId !== user?.id), [sessions, activeSessionId, user?.id]); // Load user profiles for additional accounts React.useEffect(() => { const loadAdditionalAccountsData = async () => { if (!oxyServices || additionalAccounts.length === 0) { setAdditionalAccountsData([]); return; } setLoadingAdditionalAccounts(true); try { const accountsData = await Promise.all(additionalAccounts.map(async session => { try { const userProfile = await oxyServices.getUserBySession(session.sessionId); return { id: session.sessionId, sessionId: session.sessionId, username: userProfile.username, email: userProfile.email, name: userProfile.name, avatar: userProfile.avatar, userProfile }; } catch (error) { console.error(`Failed to load profile for session ${session.sessionId}:`, error); return { id: session.sessionId, sessionId: session.sessionId, username: 'Unknown User', email: 'No email available', avatar: null, userProfile: null }; } })); setAdditionalAccountsData(accountsData); } catch (error) { console.error('Failed to load additional accounts:', error); setAdditionalAccountsData([]); } finally { setLoadingAdditionalAccounts(false); } }; loadAdditionalAccountsData(); }, [sessions, activeSessionId, user?.id, oxyServices]); // Feature settings (with mock values) const features = { safeSearch: false, language: 'English' }; // Memoize event handlers to prevent recreation on every render const handleLogout = useCallback(async () => { try { await logout(); if (onClose) { onClose(); } } catch (error) { console.error('Logout failed:', error); toast.error(t('common.errors.signOutFailed')); } }, [logout, onClose]); const confirmLogout = useCallback(() => { confirmAction(t('common.confirms.signOut'), handleLogout); }, [handleLogout]); const handleAddAccount = useCallback(() => { toast.info(t('accountOverview.addAccountComing')); }, [t]); const handleSignOutAll = useCallback(() => { confirmAction(t('common.confirms.signOutAll'), handleLogout); }, [handleLogout]); const handleDownloadData = useCallback(async () => { if (!oxyServices || !user) { toast.error(t('accountOverview.items.downloadData.error') || 'Service not available'); return; } try { Alert.alert(t('accountOverview.items.downloadData.confirmTitle') || 'Download Account Data', t('accountOverview.items.downloadData.confirmMessage') || 'Choose the format for your account data export:', [{ text: t('common.cancel') || 'Cancel', style: 'cancel' }, { text: 'JSON', onPress: async () => { try { toast.loading(t('accountOverview.items.downloadData.downloading') || 'Preparing download...'); const blob = await oxyServices.downloadAccountData('json'); // Create download link for web if (Platform.OS === 'web') { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `account-data-${Date.now()}.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); toast.success(t('accountOverview.items.downloadData.success') || 'Data downloaded successfully'); } else { // For React Native, you'd need to use a library like expo-file-system toast.success(t('accountOverview.items.downloadData.success') || 'Data downloaded successfully'); } } catch (error) { console.error('Failed to download data:', error); toast.error(error?.message || t('accountOverview.items.downloadData.error') || 'Failed to download data'); } } }, { text: 'CSV', onPress: async () => { try { toast.loading(t('accountOverview.items.downloadData.downloading') || 'Preparing download...'); const blob = await oxyServices.downloadAccountData('csv'); // Create download link for web if (Platform.OS === 'web') { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `account-data-${Date.now()}.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); toast.success(t('accountOverview.items.downloadData.success') || 'Data downloaded successfully'); } else { // For React Native, you'd need to use a library like expo-file-system toast.success(t('accountOverview.items.downloadData.success') || 'Data downloaded successfully'); } } catch (error) { console.error('Failed to download data:', error); toast.error(error?.message || t('accountOverview.items.downloadData.error') || 'Failed to download data'); } } }]); } catch (error) { console.error('Failed to initiate download:', error); toast.error(error?.message || t('accountOverview.items.downloadData.error') || 'Failed to download data'); } }, [oxyServices, user, t]); const handleDeleteAccount = useCallback(() => { if (!user) { toast.error(t('accountOverview.items.deleteAccount.error') || 'User not available'); return; } confirmAction(t('accountOverview.items.deleteAccount.confirmMessage') || `This action cannot be undone. This will permanently delete your account and all associated data.\n\nAre you sure you want to delete your account?`, async () => { // For React Native, we'd need a separate modal for password/confirmation input // For now, we'll use a simplified confirmation // In production, you'd want to create a modal with password and username confirmation fields if (!oxyServices) { toast.error(t('accountOverview.items.deleteAccount.error') || 'Service not available'); return; } Alert.alert(t('accountOverview.items.deleteAccount.confirmTitle') || 'Delete Account', t('accountOverview.items.deleteAccount.finalConfirm') || `This is your final warning. Your account will be permanently deleted and cannot be recovered. Type "${user.username}" to confirm.`, [{ text: t('common.cancel') || 'Cancel', style: 'cancel' }, { text: t('accountOverview.items.deleteAccount.confirm') || 'Delete Forever', style: 'destructive', onPress: async () => { try { // Note: In a production app, you'd want to show a modal with password and username confirmation fields // For now, we'll require the user to enter these via a custom modal or prompt toast.error(t('accountOverview.items.deleteAccount.passwordRequired') || 'Password confirmation required. This feature needs a modal with password input.'); } catch (error) { console.error('Failed to delete account:', error); toast.error(error?.message || t('accountOverview.items.deleteAccount.error') || 'Failed to delete account'); } } }]); }); }, [user, oxyServices, logout, onClose, t]); if (!isAuthenticated) { return /*#__PURE__*/_jsx(View, { style: [styles.container, { backgroundColor: themeStyles.backgroundColor }], children: /*#__PURE__*/_jsx(Text, { style: [styles.message, { color: themeStyles.textColor }], children: t('common.status.notSignedIn') }) }); } if (isLoading) { return /*#__PURE__*/_jsx(View, { style: [styles.container, { backgroundColor: themeStyles.backgroundColor, justifyContent: 'center' }], children: /*#__PURE__*/_jsx(ActivityIndicator, { size: "large", color: themeStyles.primaryColor }) }); } return /*#__PURE__*/_jsxs(View, { style: [styles.container, { backgroundColor: '#f2f2f2' }], children: [/*#__PURE__*/_jsx(Header, { title: t('accountOverview.title'), theme: theme, onBack: onClose, variant: "minimal", elevation: "subtle" }), /*#__PURE__*/_jsxs(ScrollView, { style: styles.content, children: [/*#__PURE__*/_jsx(Section, { title: t('accountOverview.sections.profile'), theme: theme, isFirst: true, children: /*#__PURE__*/_jsx(GroupedSection, { items: [{ id: 'profile-info', icon: 'person', iconColor: '#007AFF', title: user ? typeof user.name === 'string' ? user.name : user.name?.full || user.name?.first || user.username : t('common.status.loading') || 'Loading...', subtitle: user ? user.email || user.username : t('common.status.loading') || 'Loading...', onPress: () => toast.info(t('accountOverview.manageComing')), customContent: /*#__PURE__*/_jsxs(_Fragment, { children: [/*#__PURE__*/_jsx(View, { style: styles.userIcon, children: /*#__PURE__*/_jsx(Avatar, { uri: user?.avatar ? oxyServices.getFileDownloadUrl(user.avatar, 'thumb') : undefined, name: user?.name?.full, size: 40, theme: theme }) }), /*#__PURE__*/_jsx(TouchableOpacity, { style: styles.manageButton, onPress: () => toast.info(t('accountOverview.manageComing')), children: /*#__PURE__*/_jsx(Text, { style: styles.manageButtonText, children: t('accountOverview.actions.manage') }) })] }) }], theme: theme }) }), /*#__PURE__*/_jsx(Section, { title: t('accountOverview.sections.accountSettings'), theme: theme, children: /*#__PURE__*/_jsx(GroupedSection, { items: [{ id: 'edit-profile', icon: 'person-circle', iconColor: '#007AFF', title: t('accountOverview.items.editProfile.title'), subtitle: t('accountOverview.items.editProfile.subtitle'), onPress: () => navigate?.('EditProfile', { activeTab: 'profile' }) }, { id: 'security-privacy', icon: 'shield-checkmark', iconColor: '#30D158', title: t('accountOverview.items.security.title'), subtitle: t('accountOverview.items.security.subtitle'), onPress: () => navigate?.('EditProfile', { activeTab: 'password' }) }, { id: 'notifications', icon: 'notifications', iconColor: '#FF9500', title: t('accountOverview.items.notifications.title'), subtitle: t('accountOverview.items.notifications.subtitle'), onPress: () => navigate?.('EditProfile', { activeTab: 'notifications' }) }, { id: 'premium-subscription', icon: 'star', iconColor: '#FFD700', title: t('accountOverview.items.premium.title'), subtitle: user?.isPremium ? t('accountOverview.items.premium.manage') : t('accountOverview.items.premium.upgrade'), onPress: () => navigate?.('PremiumSubscription') }, ...(user?.isPremium ? [{ id: 'billing-management', icon: 'card', iconColor: '#34C759', title: t('accountOverview.items.billing.title'), subtitle: t('accountOverview.items.billing.subtitle'), onPress: () => toast.info(t('accountOverview.items.billing.coming')) }] : [])], theme: theme }) }), showMoreAccounts && /*#__PURE__*/_jsx(Section, { title: `${t('accountOverview.sections.additionalAccounts') || 'Additional Accounts'}${additionalAccountsData.length > 0 ? ` (${additionalAccountsData.length})` : ''}`, theme: theme, children: loadingAdditionalAccounts ? /*#__PURE__*/_jsx(GroupedSection, { items: [{ id: 'loading-accounts', icon: 'sync', iconColor: '#007AFF', title: t('accountOverview.loadingAdditional.title') || 'Loading accounts...', subtitle: t('accountOverview.loadingAdditional.subtitle') || 'Please wait while we load your additional accounts', customContent: /*#__PURE__*/_jsxs(View, { style: styles.loadingContainer, children: [/*#__PURE__*/_jsx(ActivityIndicator, { size: "small", color: "#007AFF" }), /*#__PURE__*/_jsx(Text, { style: styles.loadingText, children: t('accountOverview.loadingAdditional.title') || 'Loading accounts...' })] }) }], theme: theme }) : additionalAccountsData.length > 0 ? /*#__PURE__*/_jsx(GroupedSection, { items: additionalAccountsData.map((account, index) => ({ id: `account-${account.id}`, icon: 'person', iconColor: '#5856D6', title: typeof account.name === 'object' ? account.name?.full || account.name?.first || account.username : account.name || account.username, subtitle: account.email || account.username, onPress: () => { toast.info(t('accountOverview.items.accountSwitcher.switchPrompt', { username: account.username }) || `Switch to ${account.username}?`); }, customContent: /*#__PURE__*/_jsxs(_Fragment, { children: [/*#__PURE__*/_jsx(View, { style: styles.userIcon, children: account.avatar ? /*#__PURE__*/_jsx(Image, { source: { uri: oxyServices.getFileDownloadUrl(account.avatar, 'thumb') }, style: styles.accountAvatarImage }) : /*#__PURE__*/_jsx(View, { style: styles.accountAvatarFallback, children: /*#__PURE__*/_jsx(Text, { style: styles.accountAvatarText, children: account.username?.charAt(0).toUpperCase() || '?' }) }) }), /*#__PURE__*/_jsx(OxyIcon, { name: "chevron-forward", size: 16, color: "#ccc" })] }) })), theme: theme }) : /*#__PURE__*/_jsx(GroupedSection, { items: [{ id: 'no-accounts', icon: 'person-outline', iconColor: '#ccc', title: t('accountOverview.additional.noAccounts.title') || 'No other accounts', subtitle: t('accountOverview.additional.noAccounts.subtitle') || 'Add another account to switch between them' }], theme: theme }) }), showMoreAccounts && /*#__PURE__*/_jsx(Section, { title: t('accountOverview.sections.accountManagement') || 'Account Management', theme: theme, children: /*#__PURE__*/_jsx(GroupedSection, { items: [{ id: 'add-account', icon: 'add', iconColor: '#007AFF', title: t('accountOverview.items.addAccount.title') || 'Add Another Account', subtitle: t('accountOverview.items.addAccount.subtitle') || 'Sign in with a different account', onPress: handleAddAccount }, { id: 'sign-out-all', icon: 'log-out', iconColor: '#FF3B30', title: t('accountOverview.items.signOutAll.title') || 'Sign out of all accounts', subtitle: t('accountOverview.items.signOutAll.subtitle') || 'Remove all accounts from this device', onPress: handleSignOutAll }], theme: theme }) }), /*#__PURE__*/_jsx(Section, { title: t('accountOverview.sections.quickActions'), theme: theme, children: /*#__PURE__*/_jsx(GroupedSection, { items: [{ id: 'account-switcher', icon: 'people', iconColor: '#5856D6', title: showMoreAccounts ? t('accountOverview.items.accountSwitcher.titleHide') : t('accountOverview.items.accountSwitcher.titleShow'), subtitle: showMoreAccounts ? t('accountOverview.items.accountSwitcher.subtitleHide') : additionalAccountsData.length > 0 ? t('accountOverview.items.accountSwitcher.subtitleSwitchBetween', { count: String(additionalAccountsData.length + 1) }) : loadingAdditionalAccounts ? t('accountOverview.items.accountSwitcher.subtitleLoading') : t('accountOverview.items.accountSwitcher.subtitleManageMultiple'), onPress: () => setShowMoreAccounts(!showMoreAccounts) }, { id: 'history-view', icon: 'time', iconColor: '#007AFF', title: t('accountOverview.items.history.title') || 'History', subtitle: t('accountOverview.items.history.subtitle') || 'View and manage your search history', onPress: () => navigate?.('HistoryView') }, { id: 'saves-collections', icon: 'bookmark', iconColor: '#FF9500', title: t('accountOverview.items.saves.title') || 'Saves & Collections', subtitle: t('accountOverview.items.saves.subtitle') || 'View your saved items and collections', onPress: () => navigate?.('SavesCollections') }, { id: 'download-data', icon: 'download', iconColor: '#34C759', title: t('accountOverview.items.downloadData.title'), subtitle: t('accountOverview.items.downloadData.subtitle'), onPress: handleDownloadData }, { id: 'delete-account', icon: 'trash', iconColor: '#FF3B30', title: t('accountOverview.items.deleteAccount.title'), subtitle: t('accountOverview.items.deleteAccount.subtitle'), onPress: handleDeleteAccount }], theme: theme }) }), /*#__PURE__*/_jsx(Section, { title: t('accountOverview.sections.support'), theme: theme, children: /*#__PURE__*/_jsx(GroupedSection, { items: [{ id: 'search-settings', icon: 'search', iconColor: '#007AFF', title: t('accountOverview.items.searchSettings.title') || 'Search Settings', subtitle: t('accountOverview.items.searchSettings.subtitle') || 'SafeSearch and personalization', onPress: () => navigate?.('SearchSettings') }, { id: 'language-settings', icon: 'language', iconColor: '#32D74B', title: t('accountOverview.items.language.title') || 'Language', subtitle: t('accountOverview.items.language.subtitle') || 'Choose your preferred language', onPress: () => navigate?.('LanguageSelector') }, { id: 'account-preferences', icon: 'settings', iconColor: '#8E8E93', title: t('accountOverview.items.preferences.title'), subtitle: t('accountOverview.items.preferences.subtitle'), onPress: () => toast.info(t('accountOverview.items.preferences.coming')) }, { id: 'help-support', icon: 'help-circle', iconColor: '#007AFF', title: t('accountOverview.items.help.title'), subtitle: t('accountOverview.items.help.subtitle'), onPress: () => navigate?.('HelpSupport') }, { id: 'privacy-policy', icon: 'shield-checkmark', iconColor: '#30D158', title: t('accountOverview.items.privacyPolicy.title') || 'Privacy Policy', subtitle: t('accountOverview.items.privacyPolicy.subtitle') || 'How we handle your data', onPress: () => navigate?.('LegalDocuments', { initialStep: 1 }) }, { id: 'terms-of-service', icon: 'document-text', iconColor: '#007AFF', title: t('accountOverview.items.termsOfService.title') || 'Terms of Service', subtitle: t('accountOverview.items.termsOfService.subtitle') || 'Terms and conditions of use', onPress: () => navigate?.('LegalDocuments', { initialStep: 2 }) }, { id: 'connected-apps', icon: 'link', iconColor: '#32D74B', title: t('accountOverview.items.connectedApps.title'), subtitle: t('accountOverview.items.connectedApps.subtitle'), onPress: () => toast.info(t('accountOverview.items.connectedApps.coming')) }, { id: 'about', icon: 'information-circle', iconColor: '#8E8E93', title: t('accountOverview.items.about.title'), subtitle: t('accountOverview.items.about.subtitle'), onPress: () => navigate?.('AppInfo') }], theme: theme }) }), /*#__PURE__*/_jsx(Section, { title: t('accountOverview.sections.actions'), theme: theme, children: /*#__PURE__*/_jsx(GroupedItem, { icon: "log-out", iconColor: "#FF3B30", title: t('accountOverview.items.signOut.title'), subtitle: t('accountOverview.items.signOut.subtitle'), theme: theme, onPress: confirmLogout, isFirst: true, isLast: true, showChevron: false }) })] })] }); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f2f2f2' }, content: { flex: 1, padding: 16 }, userIcon: { marginRight: 12 }, manageButton: { backgroundColor: '#007AFF', paddingHorizontal: 16, paddingVertical: 8, borderRadius: 16 }, manageButtonText: { color: '#fff', fontSize: 14, fontWeight: '500' }, accountAvatarImage: { width: 40, height: 40, borderRadius: 20 }, accountAvatarFallback: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#d169e5', alignItems: 'center', justifyContent: 'center' }, accountAvatarText: { color: 'white', fontSize: 18, fontWeight: 'bold' }, message: { fontSize: 16, textAlign: 'center', marginTop: 24, color: '#333' }, loadingContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 20, gap: 12 }, loadingText: { fontSize: 16, color: '#666' } }); export default /*#__PURE__*/React.memo(AccountOverviewScreen); //# sourceMappingURL=AccountOverviewScreen.js.map