UNPKG

@oxyhq/services

Version:

OxyHQ Expo/React Native SDK — UI components, screens, and native features

614 lines (591 loc) 26.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = _interopRequireWildcard(require("react")); var _reactNative = require("react-native"); var _sonner = require("../../lib/sonner"); var _fonts = require("../styles/fonts.js"); var _confirmAction = require("../utils/confirmAction.js"); var _authStore = require("../stores/authStore.js"); var _index = require("../components/index.js"); var _useI18n = require("../hooks/useI18n.js"); var _useThemeStyles = require("../hooks/useThemeStyles.js"); var _useColorScheme = require("../hooks/useColorScheme.js"); var _theme = require("../constants/theme.js"); var _themeUtils = require("../utils/themeUtils.js"); var _userUtils = require("../utils/userUtils.js"); var _OxyContext = require("../context/OxyContext.js"); var _useAccountQueries = require("../hooks/queries/useAccountQueries.js"); var _useAccountMutations = require("../hooks/mutations/useAccountMutations.js"); var _spacing = require("../constants/spacing.js"); var _jsxRuntime = require("react/jsx-runtime"); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } const AccountSettingsScreen = ({ onClose, theme, goBack, navigate, initialField, initialSection, scrollTo }) => { // Use useOxy() hook for OxyContext values const { oxyServices, isAuthenticated } = (0, _OxyContext.useOxy)(); const { t } = (0, _useI18n.useI18n)(); // Use TanStack Query for user data const { data: user, isLoading: userLoading } = (0, _useAccountQueries.useCurrentUser)({ enabled: isAuthenticated }); const uploadAvatarMutation = (0, _useAccountMutations.useUploadAvatar)(); // Fallback to store for backward compatibility const userFromStore = (0, _authStore.useAuthStore)(state => state.user); const finalUser = user || userFromStore; const isUpdatingAvatar = uploadAvatarMutation.isPending; const [optimisticAvatarId, setOptimisticAvatarId] = (0, _react.useState)(null); const scrollViewRef = (0, _react.useRef)(null); const avatarSectionRef = (0, _react.useRef)(null); // Section refs for navigation const profilePictureSectionRef = (0, _react.useRef)(null); const basicInfoSectionRef = (0, _react.useRef)(null); const aboutSectionRef = (0, _react.useRef)(null); const quickActionsSectionRef = (0, _react.useRef)(null); const securitySectionRef = (0, _react.useRef)(null); // Section Y positions for scrolling const [profilePictureSectionY, setProfilePictureSectionY] = (0, _react.useState)(null); const [basicInfoSectionY, setBasicInfoSectionY] = (0, _react.useState)(null); const [aboutSectionY, setAboutSectionY] = (0, _react.useState)(null); const [quickActionsSectionY, setQuickActionsSectionY] = (0, _react.useState)(null); const [securitySectionY, setSecuritySectionY] = (0, _react.useState)(null); // Form state const [displayName, setDisplayName] = (0, _react.useState)(''); const [lastName, setLastName] = (0, _react.useState)(''); const [username, setUsername] = (0, _react.useState)(''); const [email, setEmail] = (0, _react.useState)(''); const [bio, setBio] = (0, _react.useState)(''); const [location, setLocation] = (0, _react.useState)(''); const [links, setLinks] = (0, _react.useState)([]); const [avatarFileId, setAvatarFileId] = (0, _react.useState)(''); // Navigation helper for editing fields const navigateToEditField = (0, _react.useCallback)(fieldType => { navigate?.('EditProfileField', { fieldType }); }, [navigate]); // Location and links state (for display only - modals handle editing) const [locations, setLocations] = (0, _react.useState)([]); const [linksMetadata, setLinksMetadata] = (0, _react.useState)([]); // Get theme colors using centralized hook const colorScheme = (0, _useColorScheme.useColorScheme)(); const themeStyles = (0, _useThemeStyles.useThemeStyles)(theme || 'light', colorScheme); // Extract colors for convenience - ensure it's always defined // useThemeStyles always returns colors, but add safety check for edge cases const colors = themeStyles.colors || _theme.Colors[(0, _themeUtils.normalizeColorScheme)(colorScheme, theme || 'light')]; // Track initialization to prevent unnecessary resets const isInitializedRef = (0, _react.useRef)(false); const previousUserIdRef = (0, _react.useRef)(null); const previousAvatarRef = (0, _react.useRef)(null); // Load user data - only reset fields when user actually changes (not just avatar) (0, _react.useEffect)(() => { if (finalUser) { const currentUserId = finalUser.id; const currentAvatar = typeof finalUser.avatar === 'string' ? finalUser.avatar : ''; const isNewUser = previousUserIdRef.current !== currentUserId; const isAvatarOnlyUpdate = !isNewUser && previousUserIdRef.current === currentUserId && previousAvatarRef.current !== currentAvatar && previousAvatarRef.current !== null; const shouldInitialize = !isInitializedRef.current || isNewUser; // Only reset all fields if it's a new user or first load // Skip reset if it's just an avatar update if (shouldInitialize && !isAvatarOnlyUpdate) { const userDisplayName = typeof finalUser.name === 'string' ? finalUser.name : finalUser.name?.first || finalUser.name?.full || ''; const userLastName = typeof finalUser.name === 'object' ? finalUser.name?.last || '' : ''; setDisplayName(userDisplayName); setLastName(userLastName); setUsername(finalUser.username || ''); setEmail(finalUser.email || ''); setBio(finalUser.bio || ''); setLocation(finalUser.location || ''); // Handle locations - convert single location to array format if (finalUser.locations && Array.isArray(finalUser.locations)) { setLocations(finalUser.locations.map((loc, index) => ({ id: loc.id || `existing-${index}`, name: loc.name, label: loc.label, coordinates: loc.coordinates }))); } else if (finalUser.location) { // Convert single location string to array format setLocations([{ id: 'existing-0', name: finalUser.location, label: 'Location' }]); } else { setLocations([]); } // Handle links - simple and direct like other fields if (finalUser.linksMetadata && Array.isArray(finalUser.linksMetadata)) { const urls = finalUser.linksMetadata.map(l => l.url); setLinks(urls); const metadataWithIds = finalUser.linksMetadata.map((link, index) => ({ ...link, id: link.id || `existing-${index}` })); setLinksMetadata(metadataWithIds); } else if (Array.isArray(finalUser.links)) { const simpleLinks = finalUser.links.map(l => typeof l === 'string' ? l : l.link).filter(Boolean); setLinks(simpleLinks); const linksWithMetadata = simpleLinks.map((url, index) => ({ url, title: url.replace(/^https?:\/\//, '').replace(/\/$/, ''), description: `Link to ${url}`, image: undefined, id: `existing-${index}` })); setLinksMetadata(linksWithMetadata); } else if (finalUser.website) { setLinks([finalUser.website]); setLinksMetadata([{ url: finalUser.website, title: finalUser.website.replace(/^https?:\/\//, '').replace(/\/$/, ''), description: `Link to ${finalUser.website}`, image: undefined, id: 'existing-0' }]); } else { setLinks([]); setLinksMetadata([]); } isInitializedRef.current = true; } // Update avatar only if it changed and we're not in optimistic/updating state // This allows the server response to update the avatar without resetting other fields // But don't override if we have a pending optimistic update if (currentAvatar !== avatarFileId && !isUpdatingAvatar && !optimisticAvatarId) { setAvatarFileId(currentAvatar); } // If we just finished updating and the server avatar matches our optimistic one, clear optimistic state // Also clear if the server avatar matches our current avatarFileId (update completed) if (isUpdatingAvatar === false && optimisticAvatarId) { if (currentAvatar === optimisticAvatarId || currentAvatar === avatarFileId) { setOptimisticAvatarId(null); } } previousUserIdRef.current = currentUserId; previousAvatarRef.current = currentAvatar; } }, [finalUser, avatarFileId, isUpdatingAvatar, optimisticAvatarId]); // Set initial editing field if provided via props (e.g., from navigation) // Handle initialSection prop to scroll to specific section const hasScrolledToSectionRef = (0, _react.useRef)(false); const previousInitialSectionRef = (0, _react.useRef)(undefined); const SCROLL_OFFSET = 100; // Offset to show section near top of viewport // Map section names to their Y positions const sectionYPositions = (0, _react.useMemo)(() => ({ profilePicture: profilePictureSectionY, basicInfo: basicInfoSectionY, about: aboutSectionY, quickActions: quickActionsSectionY, security: securitySectionY }), [profilePictureSectionY, basicInfoSectionY, aboutSectionY, quickActionsSectionY, securitySectionY]); (0, _react.useEffect)(() => { // If initialSection changed, reset the flag if (previousInitialSectionRef.current !== initialSection) { hasScrolledToSectionRef.current = false; previousInitialSectionRef.current = initialSection; } // Scroll to the specified section if initialSection is provided and we haven't scrolled yet if (initialSection && !hasScrolledToSectionRef.current) { const sectionY = sectionYPositions[initialSection]; if (sectionY !== null && sectionY !== undefined && scrollTo) { requestAnimationFrame(() => { requestAnimationFrame(() => { scrollTo(Math.max(0, sectionY - SCROLL_OFFSET), true); hasScrolledToSectionRef.current = true; }); }); } } }, [initialSection, sectionYPositions]); const handleAvatarRemove = () => { (0, _confirmAction.confirmAction)(t('editProfile.confirms.removeAvatar') || 'Remove your profile picture?', () => { setAvatarFileId(''); _sonner.toast.success(t('editProfile.toasts.avatarRemoved') || 'Avatar removed'); }); }; const { openAvatarPicker } = (0, _OxyContext.useOxy)(); // Handlers to navigate to edit screens const handleOpenDisplayNameModal = (0, _react.useCallback)(() => navigateToEditField('displayName'), [navigateToEditField]); const handleOpenUsernameModal = (0, _react.useCallback)(() => navigateToEditField('username'), [navigateToEditField]); const handleOpenEmailModal = (0, _react.useCallback)(() => navigateToEditField('email'), [navigateToEditField]); const handleOpenBioModal = (0, _react.useCallback)(() => navigateToEditField('bio'), [navigateToEditField]); const handleOpenLocationModal = (0, _react.useCallback)(() => navigateToEditField('locations'), [navigateToEditField]); const handleOpenLinksModal = (0, _react.useCallback)(() => navigateToEditField('links'), [navigateToEditField]); // Handle initialField prop - navigate to appropriate edit screen (0, _react.useEffect)(() => { if (initialField) { // Special handling for avatar - open avatar picker directly if (initialField === 'avatar') { setTimeout(() => { openAvatarPicker(); }, 300); } else { // Navigate to edit screen setTimeout(() => { const fieldTypeMap = { displayName: 'displayName', username: 'username', email: 'email', bio: 'bio', location: 'locations', locations: 'locations', links: 'links' }; const fieldType = fieldTypeMap[initialField]; if (fieldType) { navigateToEditField(fieldType); } }, 300); } } }, [initialField, openAvatarPicker, navigateToEditField]); // Memoize display name for avatar const displayNameForAvatar = (0, _react.useMemo)(() => (0, _userUtils.getDisplayName)(finalUser), [finalUser]); if (userLoading || !isAuthenticated) { return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.container, { backgroundColor: themeStyles.backgroundColor, justifyContent: 'center' }], children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "large", color: themeStyles.primaryColor }) }); } return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.container, { backgroundColor: themeStyles.backgroundColor }], children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.ScrollView, { ref: scrollViewRef, style: styles.content, contentContainerStyle: styles.scrollContent, showsVerticalScrollIndicator: false, children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: [styles.headerContainer, styles.headerSection], children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.modernTitle, { color: themeStyles.textColor, marginBottom: 0, marginTop: 0 }], children: t('accountOverview.items.editProfile.title') || t('editProfile.title') || 'Edit Profile' }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.modernSubtitle, { color: colors.secondaryText, marginBottom: 0, marginTop: 0 }], children: t('accountOverview.items.editProfile.subtitle') || t('editProfile.subtitle') || 'Manage your profile and preferences' })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { ref: ref => { avatarSectionRef.current = ref; profilePictureSectionRef.current = ref; }, style: styles.section, onLayout: event => { const { y } = event.nativeEvent.layout; setProfilePictureSectionY(y); }, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.sectionTitle, { color: colors.secondaryText }], children: t('editProfile.sections.profilePicture') || 'PROFILE PICTURE' }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.groupedSectionWrapper, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.GroupedSection, { items: [{ id: 'profile-photo', customIcon: optimisticAvatarId || avatarFileId ? isUpdatingAvatar ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Animated.View, { style: { position: 'relative', width: 36, height: 36 }, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.Image, { source: { uri: oxyServices.getFileDownloadUrl(optimisticAvatarId || avatarFileId, 'thumb') }, style: { width: 36, height: 36, borderRadius: 18, opacity: 0.6 } }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center', backgroundColor: colorScheme === 'dark' ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.7)', borderRadius: 18 }, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "small", color: colors.tint }) })] }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, { source: { uri: oxyServices.getFileDownloadUrl(optimisticAvatarId || avatarFileId, 'thumb') }, style: { width: 36, height: 36, borderRadius: 18 } }) : undefined, icon: !(optimisticAvatarId || avatarFileId) ? 'account-outline' : undefined, iconColor: colors.sidebarIconPersonalInfo, title: 'Profile Photo', subtitle: isUpdatingAvatar ? 'Updating profile picture...' : avatarFileId ? 'Tap to change your profile picture' : 'Tap to add a profile picture', onPress: isUpdatingAvatar ? undefined : openAvatarPicker, disabled: isUpdatingAvatar }, ...(avatarFileId && !isUpdatingAvatar ? [{ id: 'remove-profile-photo', icon: 'delete-outline', iconColor: colors.sidebarIconSharing, title: 'Remove Photo', subtitle: 'Delete current profile picture', onPress: handleAvatarRemove }] : [])] }) })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { ref: basicInfoSectionRef, style: styles.section, onLayout: event => { const { y } = event.nativeEvent.layout; setBasicInfoSectionY(y); }, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.sectionTitle, { color: colors.secondaryText }], children: t('editProfile.sections.basicInfo') || 'BASIC INFORMATION' }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.groupedSectionWrapper, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.GroupedSection, { items: [{ id: 'display-name', icon: 'account-outline', iconColor: colors.sidebarIconPersonalInfo, title: t('editProfile.items.displayName.title') || 'Display Name', subtitle: [displayName, lastName].filter(Boolean).join(' ') || t('editProfile.items.displayName.add') || 'Add your display name', onPress: handleOpenDisplayNameModal }, { id: 'username', icon: 'at', iconColor: colors.sidebarIconData, title: t('editProfile.items.username.title') || 'Username', subtitle: username || t('editProfile.items.username.choose') || 'Choose a username', onPress: handleOpenUsernameModal }, { id: 'email', icon: 'email-outline', iconColor: colors.sidebarIconSecurity, title: t('editProfile.items.email.title') || 'Email', subtitle: email || t('editProfile.items.email.add') || 'Add your email address', onPress: handleOpenEmailModal }] }) })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { ref: aboutSectionRef, style: styles.section, onLayout: event => { const { y } = event.nativeEvent.layout; setAboutSectionY(y); }, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.sectionTitle, { color: colors.secondaryText }], children: t('editProfile.sections.about') || 'ABOUT YOU' }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.groupedSectionWrapper, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.GroupedSection, { items: [{ id: 'bio', icon: 'text-box-outline', iconColor: colors.sidebarIconPersonalInfo, title: t('editProfile.items.bio.title') || 'Bio', subtitle: bio || t('editProfile.items.bio.placeholder') || 'Tell people about yourself', onPress: handleOpenBioModal }, { id: 'locations', icon: 'map-marker-outline', iconColor: colors.sidebarIconSharing, title: t('editProfile.items.locations.title') || 'Locations', subtitle: locations.length > 0 ? locations.length === 1 ? t('editProfile.items.locations.count', { count: locations.length }) || `${locations.length} location added` : t('editProfile.items.locations.count_plural', { count: locations.length }) || `${locations.length} locations added` : t('editProfile.items.locations.add') || 'Add your locations', onPress: handleOpenLocationModal }, { id: 'links', icon: 'link-variant', iconColor: colors.sidebarIconSharing, title: t('editProfile.items.links.title') || 'Links', subtitle: linksMetadata.length > 0 ? linksMetadata.length === 1 ? t('editProfile.items.links.count', { count: linksMetadata.length }) || `${linksMetadata.length} link added` : t('editProfile.items.links.count_plural', { count: linksMetadata.length }) || `${linksMetadata.length} links added` : t('editProfile.items.links.add') || 'Add your links', onPress: handleOpenLinksModal }] }) })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { ref: quickActionsSectionRef, style: styles.section, onLayout: event => { const { y } = event.nativeEvent.layout; setQuickActionsSectionY(y); }, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.sectionTitle, { color: colors.secondaryText }], children: t('editProfile.sections.quickActions') || 'QUICK ACTIONS' }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.groupedSectionWrapper, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.GroupedSection, { items: [{ id: 'preview-profile', icon: 'eye', iconColor: colors.sidebarIconHome, title: t('editProfile.items.previewProfile.title') || 'Preview Profile', subtitle: t('editProfile.items.previewProfile.subtitle') || 'See how your profile looks to others', onPress: () => navigate?.('Profile', { userId: finalUser?.id }) }, { id: 'privacy-settings', icon: 'shield-check', iconColor: colors.sidebarIconSecurity, title: t('editProfile.items.privacySettings.title') || 'Privacy Settings', subtitle: t('editProfile.items.privacySettings.subtitle') || 'Control who can see your profile', onPress: () => navigate?.('PrivacySettings') }, { id: 'verify-account', icon: 'check-circle', iconColor: colors.sidebarIconPersonalInfo, title: t('editProfile.items.verifyAccount.title') || 'Verify Account', subtitle: t('editProfile.items.verifyAccount.subtitle') || 'Get a verified badge', onPress: () => navigate?.('AccountVerification') }] }) })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { ref: securitySectionRef, style: styles.section, onLayout: event => { const { y } = event.nativeEvent.layout; setSecuritySectionY(y); }, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.sectionTitle, { color: colors.secondaryText }], children: t('editProfile.sections.security') || 'SECURITY' }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.groupedSectionWrapper })] })] }) }); }; const styles = _reactNative.StyleSheet.create({ container: { flexShrink: 1, width: '100%' }, content: { flexShrink: 1 }, scrollContent: (0, _spacing.createScreenContentStyle)(_spacing.HEADER_PADDING_TOP_SETTINGS), headerContainer: { width: '100%', maxWidth: 420, alignSelf: 'center', marginBottom: _spacing.SECTION_GAP_LARGE }, headerSection: { alignItems: 'flex-start', width: '100%', gap: _spacing.COMPONENT_GAP }, modernTitle: { fontFamily: _fonts.fontFamilies.interBold, fontWeight: _reactNative.Platform.OS === 'web' ? 'bold' : undefined, fontSize: 42, lineHeight: 50.4, // 42 * 1.2 textAlign: 'left', letterSpacing: -0.5 }, modernSubtitle: { fontSize: 18, lineHeight: 24, textAlign: 'left', maxWidth: 320, alignSelf: 'flex-start', opacity: 0.8 }, section: { marginBottom: _spacing.SECTION_GAP_LARGE }, sectionTitle: { fontSize: 13, fontWeight: '600', marginBottom: 8, marginTop: 4, textTransform: 'uppercase', letterSpacing: 0.5, fontFamily: _fonts.fontFamilies.interSemiBold }, groupedSectionWrapper: { backgroundColor: 'transparent' } }); var _default = exports.default = /*#__PURE__*/_react.default.memo(AccountSettingsScreen); //# sourceMappingURL=AccountSettingsScreen.js.map