Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
"prettier-plugin-tailwindcss": "^0.5.14",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.56.4",
"react-native": "0.81.5",
"react-native-gesture-handler": "~2.28.0",
"react-native-iap": "^15.2.0",
Expand Down
216 changes: 110 additions & 106 deletions src/components/mobile/MobileProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ import { AvatarCamera } from './AvatarCamera';
import { MobileFormInput } from './MobileFormInput';
import { StatisticsDisplay } from './StatisticsDisplay';

// Enable LayoutAnimation on Android
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}

// ─── Types ───────────────────────────────────────────────────────────────────

/**
Expand Down Expand Up @@ -247,55 +252,23 @@ export const MobileProfile: React.FC<MobileProfileProps> = ({
isLoading = false,
}) => {
const [profile, setProfile] = useState<ProfileData>(MOCK_PROFILE);
const {
applyPrefillToFields,
persistFields,
prefillValues,
isLoading: formCacheLoading,
} = useFormCache(PROFILE_FORM_CACHE_KEYS);
const unlockedCount = profile.achievements.filter(a => !a.isLocked).length;
const [activeTab, setActiveTab] = useState<ProfileTab>('overview');
const [isEditing, setIsEditing] = useState(false);
const [isCameraVisible, setIsCameraVisible] = useState(false);
const [isSaving, setIsSaving] = useState(false);

// Edit form state
const [editName, setEditName] = useState('');
const [editBio, setEditBio] = useState('');
const [editEmail, setEditEmail] = useState('');
const [editLocation, setEditLocation] = useState('');
const [editWebsite, setEditWebsite] = useState('');
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
const {
control,
handleSubmit,
reset,
formState: { errors: formErrors },
} = useForm({
defaultValues: { name: '', email: '', bio: '', location: '', website: '' },
});

useEffect(() => {
if (!isEditing || formCacheLoading) return;
applyPrefillToFields(
{
fullName: editName,
email: editEmail,
bio: editBio,
location: editLocation,
website: editWebsite,
},
{
fullName: setEditName,
email: setEditEmail,
bio: setEditBio,
location: setEditLocation,
website: setEditWebsite,
}
);
}, [
applyPrefillToFields,
editBio,
editEmail,
editLocation,
editName,
editWebsite,
formCacheLoading,
isEditing,
prefillValues,
]);
// Progressive disclosure: advanced profile fields collapsed by default
const [showAdvancedFields, setShowAdvancedFields] = useState(false);

if (isLoading) {
const bg = isDark ? '#0f172a' : '#f8fafc';
Expand Down Expand Up @@ -336,9 +309,6 @@ export const MobileProfile: React.FC<MobileProfileProps> = ({
</SafeAreaView>
);
}
// Progressive disclosure: advanced profile fields collapsed by default
const [showAdvancedFields, setShowAdvancedFields] = useState(false);

// Theme tokens
const bg = isDark ? '#0f172a' : '#f8fafc';
const cardBg = isDark ? '#1e293b' : '#fff';
Expand Down Expand Up @@ -404,22 +374,21 @@ export const MobileProfile: React.FC<MobileProfileProps> = ({
return;
}
setIsSaving(true);
// Simulate API call — replace with actual service call
await new Promise(resolve => setTimeout(resolve, 800));
setProfile(prev => ({
...prev,
name: editName.trim(),
bio: editBio.trim(),
email: editEmail.trim(),
location: editLocation.trim(),
website: editWebsite.trim(),
name: data.name.trim(),
bio: data.bio.trim(),
email: data.email.trim(),
location: data.location.trim(),
website: data.website.trim(),
}));
await persistFields({
fullName: editName.trim(),
email: editEmail.trim(),
bio: editBio.trim(),
location: editLocation.trim(),
website: editWebsite.trim(),
await cacheFormValues({
fullName: data.name.trim(),
email: data.email.trim(),
bio: data.bio.trim(),
location: data.location.trim(),
website: data.website.trim(),
});
setIsSaving(false);
setIsEditing(false);
Expand Down Expand Up @@ -548,7 +517,7 @@ export const MobileProfile: React.FC<MobileProfileProps> = ({
</TouchableOpacity>
<TouchableOpacity
style={styles.saveBtn}
onPress={handleSave}
onPress={() => void handleSave()}
disabled={isSaving}
accessibilityRole="button"
accessibilityLabel="Save profile changes"
Expand Down Expand Up @@ -674,38 +643,61 @@ export const MobileProfile: React.FC<MobileProfileProps> = ({
<Text style={[styles.cardTitle, { color: textPrimary }]}>Edit Profile</Text>

{/* ── Basic Fields (always visible) ── */}
<MobileFormInput
label="Full Name"
value={editName}
onChangeText={setEditName}
placeholder="Your full name"
required
error={formErrors?.name}
isDark={isDark}
cacheKey="fullName"
leftIcon={<User size={18} color="#94a3b8" />}
<Controller
control={control}
name="name"
rules={{ required: 'Name is required' }}
render={({ field: { onChange, value } }) => (
<MobileFormInput
label="Full Name"
value={value}
onChangeText={onChange}
placeholder="Your full name"
required
error={formErrors.name?.message}
isDark={isDark}
cacheKey="fullName"
leftIcon={<User size={18} color="#94a3b8" />}
/>
)}
/>
<MobileFormInput
label="Email"
value={editEmail}
onChangeText={setEditEmail}
placeholder="your@email.com"
keyboardType="email-address"
autoCapitalize="none"
required
error={formErrors?.email}
isDark={isDark}
cacheKey="email"
leftIcon={<Mail size={18} color="#94a3b8" />}
<Controller
control={control}
name="email"
rules={{
required: 'Email is required',
pattern: { value: /\S+@\S+\.\S+/, message: 'Enter a valid email address' },
}}
render={({ field: { onChange, value } }) => (
<MobileFormInput
label="Email"
value={value}
onChangeText={onChange}
placeholder="your@email.com"
keyboardType="email-address"
autoCapitalize="none"
required
error={formErrors.email?.message}
isDark={isDark}
cacheKey="email"
leftIcon={<Mail size={18} color="#94a3b8" />}
/>
)}
/>
<MobileFormInput
label="Bio"
value={editBio}
onChangeText={setEditBio}
placeholder="Tell us about yourself..."
multiline
isDark={isDark}
cacheKey="bio"
<Controller
control={control}
name="bio"
render={({ field: { onChange, value } }) => (
<MobileFormInput
label="Bio"
value={value}
onChangeText={onChange}
placeholder="Tell us about yourself..."
multiline
isDark={isDark}
cacheKey="bio"
/>
)}
/>

{/* ── Progressive Disclosure: Advanced Details ── */}
Expand Down Expand Up @@ -735,25 +727,37 @@ export const MobileProfile: React.FC<MobileProfileProps> = ({
{/* ── Advanced Fields (expandable) ── */}
{showAdvancedFields && (
<View style={styles.disclosureContent}>
<MobileFormInput
label="Location"
value={editLocation}
onChangeText={setEditLocation}
placeholder="City, Country"
isDark={isDark}
cacheKey="location"
leftIcon={<MapPin size={18} color="#94a3b8" />}
<Controller
control={control}
name="location"
render={({ field: { onChange, value } }) => (
<MobileFormInput
label="Location"
value={value}
onChangeText={onChange}
placeholder="City, Country"
isDark={isDark}
cacheKey="location"
leftIcon={<MapPin size={18} color="#94a3b8" />}
/>
)}
/>
<MobileFormInput
label="Website"
value={editWebsite}
onChangeText={setEditWebsite}
placeholder="yourwebsite.com"
keyboardType="url"
autoCapitalize="none"
isDark={isDark}
cacheKey="website"
leftIcon={<Globe size={18} color="#94a3b8" />}
<Controller
control={control}
name="website"
render={({ field: { onChange, value } }) => (
<MobileFormInput
label="Website"
value={value}
onChangeText={onChange}
placeholder="yourwebsite.com"
keyboardType="url"
autoCapitalize="none"
isDark={isDark}
cacheKey="website"
leftIcon={<Globe size={18} color="#94a3b8" />}
/>
)}
/>
</View>
)}
Expand Down
43 changes: 24 additions & 19 deletions src/pages/mobile/MobileLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Mail,
} from 'lucide-react-native';
import React, { useEffect, useRef, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import {
ActivityIndicator,
Alert,
Expand All @@ -33,6 +34,11 @@ import authService, { AuthResult } from '../../services/mobileAuth';
import * as secureStorage from '../../services/secureStorage';
import { validateEmail, validateRequired } from '../../utils/validation';

interface LoginFormValues {
email: string;
password: string;
}

// ─── Types ────────────────────────────────────────────────────────────────────

interface MobileLoginProps {
Expand All @@ -54,17 +60,20 @@ export const MobileLogin: React.FC<MobileLoginProps> = ({
onRegister,
isDark = false,
}) => {
// ── State ────────────────────────────────────────────────────────────────
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// ── Form ─────────────────────────────────────────────────────────────────
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<LoginFormValues>({ defaultValues: { email: '', password: '' } });

const [showPassword, setShowPassword] = useState(false);
const [rememberMe, setRememberMe] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [serverError, setServerError] = useState<string | null>(null);
const [showBiometricModal, setShowBiometricModal] = useState(false);
const [passwordFocused, setPasswordFocused] = useState(false);

const emailRef = useRef<TextInput>(null);
const passwordRef = useRef<TextInput>(null);

const {
Expand Down Expand Up @@ -97,19 +106,16 @@ export const MobileLogin: React.FC<MobileLoginProps> = ({
authService.getRememberedEmail(),
secureStorage.isRememberMeEnabled(),
]);
if (savedEmail) setEmail(savedEmail);
if (savedEmail) setValue('email', savedEmail);
if (savedRememberMe) setRememberMe(true);
}
loadRemembered();
}, []);
}, [setValue]);

// ── Auto-trigger biometric on mount if enabled ───────────────────────────
useEffect(() => {
if (biometricEnabled && biometricAvailable) {
// Small delay so screen renders first
const timer = setTimeout(() => {
setShowBiometricModal(true);
}, 600);
const timer = setTimeout(() => setShowBiometricModal(true), 600);
return () => clearTimeout(timer);
}
}, [biometricEnabled, biometricAvailable]);
Expand All @@ -130,14 +136,13 @@ export const MobileLogin: React.FC<MobileLoginProps> = ({
setIsLoading(true);
try {
const result = await authService.login({
email: email.trim().toLowerCase(),
password,
email: data.email.trim().toLowerCase(),
password: data.password,
rememberMe,
});
onLoginSuccess(result);
} catch (err) {
const msg = err instanceof Error ? err.message : 'Login failed. Please try again.';
setError(msg);
setServerError(err instanceof Error ? err.message : 'Login failed. Please try again.');
} finally {
setIsLoading(false);
}
Expand Down Expand Up @@ -206,11 +211,11 @@ export const MobileLogin: React.FC<MobileLoginProps> = ({
{/* ── Card ── */}
<View style={[styles.card, { backgroundColor: cardBg, borderColor }]}>
{/* Error banner */}
{error && (
{displayError && (
<View style={styles.errorBanner}>
<AlertCircle size={scale(14)} color="#dc2626" />
<Text allowFontScaling={false} style={styles.errorText}>
{error}
{displayError}
</Text>
</View>
)}
Expand Down Expand Up @@ -325,7 +330,7 @@ export const MobileLogin: React.FC<MobileLoginProps> = ({
{/* Primary CTA */}
<TouchableOpacity
style={[styles.loginBtn, { opacity: isLoading ? 0.7 : 1 }]}
onPress={handlePasswordLogin}
onPress={handleSubmit(onSubmit)}
disabled={isLoading}
activeOpacity={0.85}
>
Expand Down
Loading
Loading