create-ern-boilerplate 0.0.9 → 0.0.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ern-boilerplate",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "Expo React Native boilerplate generator",
5
5
  "bin": {
6
6
  "create-ern-boilerplate": "./create.js",
@@ -1,4 +1,4 @@
1
- import React, { useState, useRef } from 'react';
1
+ import React, { useState, useRef, useEffect } from 'react';
2
2
  import {
3
3
  View,
4
4
  Text,
@@ -13,16 +13,23 @@ import {
13
13
  TextInput
14
14
  } from 'react-native';
15
15
  import { Link } from 'expo-router';
16
- import { Mail, Lock, Eye, EyeOff, LogIn, Sparkles, TentTree, Biohazard } from 'lucide-react-native';
16
+ import { Mail, Lock, Eye, EyeOff, LogIn, Sparkles, TentTree, Biohazard, Fingerprint } from 'lucide-react-native';
17
+ import * as LocalAuthentication from 'expo-local-authentication';
18
+ import * as SecureStore from 'expo-secure-store';
17
19
  import { useAuth } from '@/hooks/useAuth';
18
20
  import { useTheme } from '@/hooks/useTheme';
19
21
  import { Button } from '@/components/common/Button';
20
22
  import { validation } from '@/utils/validation';
21
23
  import { APP_CONFIG } from '@/utils/constants';
22
-
24
+ import Toast from 'react-native-toast-message';
23
25
 
24
26
  const { width, height } = Dimensions.get('window');
25
27
 
28
+ // Keys untuk menyimpan credentials di SecureStore
29
+ const BIOMETRIC_ENABLED_KEY = 'biometric_enabled';
30
+ const STORED_EMAIL_KEY = 'stored_email';
31
+ const STORED_PASSWORD_KEY = 'stored_password';
32
+
26
33
  export default function LoginScreen() {
27
34
  const { login } = useAuth();
28
35
  const { colors } = useTheme();
@@ -31,13 +38,20 @@ export default function LoginScreen() {
31
38
  const [showPassword, setShowPassword] = useState(false);
32
39
  const [errors, setErrors] = useState({ email: '', password: '' });
33
40
  const [loading, setLoading] = useState(false);
41
+
42
+ // Biometric states
43
+ const [isBiometricSupported, setIsBiometricSupported] = useState(false);
44
+ const [biometricEnabled, setBiometricEnabled] = useState(false);
45
+ const [biometricType, setBiometricType] = useState<string>('');
34
46
 
35
47
  // Animations
36
48
  const fadeAnim = useRef(new Animated.Value(0)).current;
37
49
  const slideAnim = useRef(new Animated.Value(50)).current;
38
50
  const scaleAnim = useRef(new Animated.Value(0.9)).current;
51
+ const biometricPulse = useRef(new Animated.Value(1)).current;
39
52
 
40
- React.useEffect(() => {
53
+ useEffect(() => {
54
+ // Animation startup
41
55
  Animated.parallel([
42
56
  Animated.timing(fadeAnim, {
43
57
  toValue: 1,
@@ -56,8 +70,157 @@ export default function LoginScreen() {
56
70
  useNativeDriver: true,
57
71
  }),
58
72
  ]).start();
73
+
74
+ // Check biometric support
75
+ checkBiometricSupport();
76
+ checkBiometricEnabled();
59
77
  }, []);
60
78
 
79
+ // Pulse animation untuk biometric button
80
+ const startBiometricPulse = () => {
81
+ Animated.loop(
82
+ Animated.sequence([
83
+ Animated.timing(biometricPulse, {
84
+ toValue: 1.1,
85
+ duration: 1000,
86
+ useNativeDriver: true,
87
+ }),
88
+ Animated.timing(biometricPulse, {
89
+ toValue: 1,
90
+ duration: 1000,
91
+ useNativeDriver: true,
92
+ }),
93
+ ])
94
+ ).start();
95
+ };
96
+
97
+ const checkBiometricSupport = async () => {
98
+ try {
99
+ const compatible = await LocalAuthentication.hasHardwareAsync();
100
+ setIsBiometricSupported(compatible);
101
+
102
+ if (compatible) {
103
+ const enrolled = await LocalAuthentication.isEnrolledAsync();
104
+ if (enrolled) {
105
+ const types = await LocalAuthentication.supportedAuthenticationTypesAsync();
106
+ if (types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT)) {
107
+ setBiometricType('Fingerprint');
108
+ } else if (types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION)) {
109
+ setBiometricType('Face ID');
110
+ } else {
111
+ setBiometricType('Biometric');
112
+ }
113
+ startBiometricPulse();
114
+ }
115
+ }
116
+ } catch (error) {
117
+ console.error('Error checking biometric support:', error);
118
+ }
119
+ };
120
+
121
+ const checkBiometricEnabled = async () => {
122
+ try {
123
+ const enabled = await SecureStore.getItemAsync(BIOMETRIC_ENABLED_KEY);
124
+ setBiometricEnabled(enabled === 'true');
125
+ } catch (error) {
126
+ console.error('Error checking biometric enabled:', error);
127
+ }
128
+ };
129
+
130
+ const saveBiometricCredentials = async (email: string, password: string) => {
131
+ try {
132
+ await SecureStore.setItemAsync(BIOMETRIC_ENABLED_KEY, 'true');
133
+ await SecureStore.setItemAsync(STORED_EMAIL_KEY, email);
134
+ await SecureStore.setItemAsync(STORED_PASSWORD_KEY, password);
135
+ setBiometricEnabled(true);
136
+ Toast.show({
137
+ type: 'success',
138
+ text1: 'Biometric Enabled',
139
+ text2: `${biometricType} login has been enabled`,
140
+ });
141
+ } catch (error) {
142
+ console.error('Error saving biometric credentials:', error);
143
+ Toast.show({
144
+ type: 'error',
145
+ text1: 'Error',
146
+ text2: 'Failed to enable biometric login',
147
+ });
148
+ }
149
+ };
150
+
151
+ const handleBiometricLogin = async () => {
152
+ try {
153
+ // Check if biometric is enrolled
154
+ const isEnrolled = await LocalAuthentication.isEnrolledAsync();
155
+ if (!isEnrolled) {
156
+ Alert.alert(
157
+ 'Biometric Not Set Up',
158
+ 'Please set up biometric authentication in your device settings first.',
159
+ [{ text: 'OK' }]
160
+ );
161
+ return;
162
+ }
163
+
164
+ // Authenticate with biometric
165
+ const result = await LocalAuthentication.authenticateAsync({
166
+ promptMessage: `Login with ${biometricType}`,
167
+ cancelLabel: 'Cancel',
168
+ disableDeviceFallback: false,
169
+ fallbackLabel: 'Use Password',
170
+ });
171
+
172
+ if (result.success) {
173
+ // Retrieve stored credentials
174
+ const storedEmail = await SecureStore.getItemAsync(STORED_EMAIL_KEY);
175
+ const storedPassword = await SecureStore.getItemAsync(STORED_PASSWORD_KEY);
176
+
177
+ if (storedEmail && storedPassword) {
178
+ setLoading(true);
179
+ try {
180
+ await login({ email: storedEmail, password: storedPassword });
181
+ Toast.show({
182
+ type: 'success',
183
+ text1: 'Welcome Back!',
184
+ text2: 'Login successful',
185
+ });
186
+ } catch (error: any) {
187
+ Toast.show({
188
+ type: 'error',
189
+ text1: 'Login Failed',
190
+ text2: error.message || 'An error occurred',
191
+ });
192
+ } finally {
193
+ setLoading(false);
194
+ }
195
+ } else {
196
+ Alert.alert(
197
+ 'No Saved Credentials',
198
+ 'Please login with email and password first to enable biometric login.',
199
+ [{ text: 'OK' }]
200
+ );
201
+ }
202
+ } else {
203
+ // Authentication failed or was cancelled
204
+ if (result.error === 'user_cancel' || result.error === 'system_cancel') {
205
+ // User cancelled, do nothing
206
+ } else {
207
+ Toast.show({
208
+ type: 'error',
209
+ text1: 'Authentication Failed',
210
+ text2: 'Please try again',
211
+ });
212
+ }
213
+ }
214
+ } catch (error) {
215
+ console.error('Biometric authentication error:', error);
216
+ Toast.show({
217
+ type: 'error',
218
+ text1: 'Error',
219
+ text2: 'Biometric authentication failed',
220
+ });
221
+ }
222
+ };
223
+
61
224
  const validateForm = () => {
62
225
  const emailError = validation.email(email);
63
226
  const passwordError = validation.password(password);
@@ -76,13 +239,67 @@ export default function LoginScreen() {
76
239
  setLoading(true);
77
240
  try {
78
241
  await login({ email, password });
242
+
243
+ // Jika login berhasil dan biometric supported, tawarkan untuk enable biometric
244
+ if (isBiometricSupported && !biometricEnabled) {
245
+ Alert.alert(
246
+ 'Enable Biometric Login?',
247
+ `Would you like to enable ${biometricType} login for faster access next time?`,
248
+ [
249
+ {
250
+ text: 'Not Now',
251
+ style: 'cancel',
252
+ },
253
+ {
254
+ text: 'Enable',
255
+ onPress: () => saveBiometricCredentials(email, password),
256
+ },
257
+ ]
258
+ );
259
+ }
79
260
  } catch (error: any) {
80
- Alert.alert('Login Failed', error.message || 'An error occurred');
261
+ Toast.show({
262
+ type: 'error',
263
+ text1: 'Login Failed',
264
+ text2: error.message || 'An error occurred',
265
+ });
81
266
  } finally {
82
267
  setLoading(false);
83
268
  }
84
269
  };
85
270
 
271
+ const disableBiometric = async () => {
272
+ Alert.alert(
273
+ 'Disable Biometric Login?',
274
+ 'You will need to login with email and password next time.',
275
+ [
276
+ {
277
+ text: 'Cancel',
278
+ style: 'cancel',
279
+ },
280
+ {
281
+ text: 'Disable',
282
+ style: 'destructive',
283
+ onPress: async () => {
284
+ try {
285
+ await SecureStore.deleteItemAsync(BIOMETRIC_ENABLED_KEY);
286
+ await SecureStore.deleteItemAsync(STORED_EMAIL_KEY);
287
+ await SecureStore.deleteItemAsync(STORED_PASSWORD_KEY);
288
+ setBiometricEnabled(false);
289
+ Toast.show({
290
+ type: 'success',
291
+ text1: 'Biometric Disabled',
292
+ text2: 'Biometric login has been disabled',
293
+ });
294
+ } catch (error) {
295
+ console.error('Error disabling biometric:', error);
296
+ }
297
+ },
298
+ },
299
+ ]
300
+ );
301
+ };
302
+
86
303
  return (
87
304
  <View style={[styles.container, { backgroundColor: colors.background }]}>
88
305
  {/* Decorative Background Elements */}
@@ -133,6 +350,54 @@ export default function LoginScreen() {
133
350
  </View>
134
351
  </View>
135
352
 
353
+ {/* Biometric Login Button - Show if enabled */}
354
+ {isBiometricSupported && biometricEnabled && (
355
+ <Animated.View style={{ transform: [{ scale: biometricPulse }] }}>
356
+ <TouchableOpacity
357
+ style={[styles.biometricButton, {
358
+ backgroundColor: `${colors.primary}15`,
359
+ borderColor: colors.primary,
360
+ }]}
361
+ onPress={handleBiometricLogin}
362
+ activeOpacity={0.7}
363
+ >
364
+ <View style={[styles.biometricIconWrapper, { backgroundColor: colors.primary }]}>
365
+ <Fingerprint size={28} color="#FFFFFF" strokeWidth={2} />
366
+ </View>
367
+ <View style={styles.biometricTextContainer}>
368
+ <Text style={[styles.biometricTitle, { color: colors.text }]}>
369
+ Login with {biometricType}
370
+ </Text>
371
+ <Text style={[styles.biometricSubtitle, { color: colors.textSecondary }]}>
372
+ Quick and secure access
373
+ </Text>
374
+ </View>
375
+ <TouchableOpacity
376
+ style={styles.biometricSettingsButton}
377
+ onPress={disableBiometric}
378
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
379
+ >
380
+ <Text style={[styles.biometricSettingsText, { color: colors.textSecondary }]}>
381
+ ⚙️
382
+ </Text>
383
+ </TouchableOpacity>
384
+ </TouchableOpacity>
385
+ </Animated.View>
386
+ )}
387
+
388
+ {/* Divider if biometric is shown */}
389
+ {isBiometricSupported && biometricEnabled && (
390
+ <View style={styles.dividerContainer}>
391
+ <View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
392
+ <View style={[styles.dividerTextWrapper, { backgroundColor: colors.background }]}>
393
+ <Text style={[styles.dividerText, { color: colors.textSecondary }]}>
394
+ {/* or sign in with email */}
395
+ </Text>
396
+ </View>
397
+ <View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
398
+ </View>
399
+ )}
400
+
136
401
  {/* Form Container with Card Style */}
137
402
  <View style={[styles.formCard, {
138
403
  backgroundColor: colors.card || colors.background,
@@ -249,7 +514,7 @@ export default function LoginScreen() {
249
514
  <View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
250
515
  <View style={[styles.dividerTextWrapper, { backgroundColor: colors.background }]}>
251
516
  <Text style={[styles.dividerText, { color: colors.textSecondary }]}>
252
- or
517
+ {/* or */}
253
518
  </Text>
254
519
  </View>
255
520
  <View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
@@ -372,6 +637,45 @@ const styles = StyleSheet.create({
372
637
  textAlign: 'center',
373
638
  opacity: 0.8,
374
639
  },
640
+ biometricButton: {
641
+ flexDirection: 'row',
642
+ alignItems: 'center',
643
+ borderRadius: 20,
644
+ padding: 20,
645
+ marginBottom: 24,
646
+ borderWidth: 2,
647
+ elevation: 2,
648
+ shadowColor: '#000',
649
+ shadowOffset: { width: 0, height: 2 },
650
+ shadowOpacity: 0.1,
651
+ shadowRadius: 8,
652
+ },
653
+ biometricIconWrapper: {
654
+ width: 56,
655
+ height: 56,
656
+ borderRadius: 28,
657
+ alignItems: 'center',
658
+ justifyContent: 'center',
659
+ marginRight: 16,
660
+ },
661
+ biometricTextContainer: {
662
+ flex: 1,
663
+ },
664
+ biometricTitle: {
665
+ fontSize: 16,
666
+ fontWeight: '700',
667
+ marginBottom: 4,
668
+ },
669
+ biometricSubtitle: {
670
+ fontSize: 13,
671
+ opacity: 0.7,
672
+ },
673
+ biometricSettingsButton: {
674
+ padding: 8,
675
+ },
676
+ biometricSettingsText: {
677
+ fontSize: 20,
678
+ },
375
679
  formCard: {
376
680
  borderRadius: 24,
377
681
  padding: 24,
@@ -452,7 +756,7 @@ const styles = StyleSheet.create({
452
756
  height: 1,
453
757
  },
454
758
  dividerTextWrapper: {
455
- paddingHorizontal: 16,
759
+ // paddingHorizontal: 16,
456
760
  position: 'absolute',
457
761
  left: '50%',
458
762
  transform: [{ translateX: -20 }],
@@ -77,22 +77,20 @@ export default function MainLayout() {
77
77
  }}
78
78
  />
79
79
 
80
- {isAdmin && (
81
- <Tabs.Screen
82
- name="settings"
83
- options={{
84
- title: 'Settings',
85
- tabBarIcon: ({ color, focused }) => (
86
- <View style={[
87
- styles.iconContainer,
88
- focused && { backgroundColor: `${colors.primary}15` }
89
- ]}>
90
- <Settings size={24} color={color} strokeWidth={focused ? 2.5 : 2} />
91
- </View>
92
- ),
93
- }}
94
- />
95
- )}
80
+ <Tabs.Screen
81
+ name="settings"
82
+ options={{
83
+ title: 'Settings',
84
+ tabBarIcon: ({ color, focused }) => (
85
+ <View style={[
86
+ styles.iconContainer,
87
+ focused && { backgroundColor: `${colors.primary}15` }
88
+ ]}>
89
+ <Settings size={24} color={color} strokeWidth={focused ? 2.5 : 2} />
90
+ </View>
91
+ ),
92
+ }}
93
+ />
96
94
  </Tabs>
97
95
  );
98
96
  }
@@ -29,16 +29,14 @@ export default function ProfileScreen() {
29
29
  const [saving, setSaving] = useState(false);
30
30
  const [currentUser, setCurrentUser] = useState<any>(null);
31
31
 
32
- // Profile data state
33
- const [profileData, setProfileData] = useState({
34
- name: '',
35
- email: '',
36
- phone: '',
37
- location: '',
38
- about: '',
39
- avatar: '',
40
- joinDate: '',
41
- });
32
+ // Profile data state - FIX: Separate state untuk setiap field
33
+ const [name, setName] = useState('');
34
+ const [email, setEmail] = useState('');
35
+ const [phone, setPhone] = useState('');
36
+ const [location, setLocation] = useState('');
37
+ const [about, setAbout] = useState('');
38
+ const [avatar, setAvatar] = useState('');
39
+ const [joinDate, setJoinDate] = useState('');
42
40
 
43
41
  // Load current user data
44
42
  useEffect(() => {
@@ -46,18 +44,19 @@ export default function ProfileScreen() {
46
44
  const userProfile = users.find(u => u.id === user.id || u.email === user.email);
47
45
  if (userProfile) {
48
46
  setCurrentUser(userProfile);
49
- setProfileData({
50
- name: userProfile.name || '',
51
- email: userProfile.email || '',
52
- phone: userProfile.phone || '',
53
- location: userProfile.location || '',
54
- about: userProfile.about || '',
55
- avatar: userProfile.avatar || '',
56
- joinDate: new Date(userProfile.createdAt).toLocaleDateString('en-US', {
47
+ setName(userProfile.name || '');
48
+ setEmail(userProfile.email || '');
49
+ setPhone(userProfile.phone || '');
50
+ setLocation(userProfile.location || '');
51
+ setAbout(userProfile.about || '');
52
+ setAvatar(userProfile.avatar || '');
53
+ setJoinDate(
54
+ new Date(userProfile.createdAt).toLocaleDateString('en-US', {
57
55
  year: 'numeric',
58
- month: 'long'
59
- }),
60
- });
56
+ month: 'long',
57
+ day: 'numeric'
58
+ })
59
+ );
61
60
  }
62
61
  }
63
62
  }, [user, users]);
@@ -69,11 +68,11 @@ export default function ProfileScreen() {
69
68
  try {
70
69
  const updateData: UpdateUserInput = {
71
70
  id: currentUser.id,
72
- name: profileData.name,
73
- email: profileData.email,
74
- phone: profileData.phone,
75
- location: profileData.location,
76
- about: profileData.about,
71
+ name,
72
+ email,
73
+ phone,
74
+ location,
75
+ about,
77
76
  role: currentUser.role,
78
77
  status: currentUser.status,
79
78
  avatar: currentUser.avatar,
@@ -83,6 +82,7 @@ export default function ProfileScreen() {
83
82
 
84
83
  if (result.success) {
85
84
  setIsEditing(false);
85
+ Alert.alert('Success', 'Profile updated successfully');
86
86
  }
87
87
  } catch (error) {
88
88
  Alert.alert('Error', 'Failed to update profile');
@@ -95,23 +95,16 @@ export default function ProfileScreen() {
95
95
  setIsEditing(false);
96
96
  // Reset to original values
97
97
  if (currentUser) {
98
- setProfileData({
99
- name: currentUser.name || '',
100
- email: currentUser.email || '',
101
- phone: currentUser.phone || '',
102
- location: currentUser.location || '',
103
- about: currentUser.about || '',
104
- avatar: currentUser.avatar || '',
105
- joinDate: new Date(currentUser.createdAt).toLocaleDateString('en-US', {
106
- year: 'numeric',
107
- month: 'long'
108
- }),
109
- });
98
+ setName(currentUser.name || '');
99
+ setEmail(currentUser.email || '');
100
+ setPhone(currentUser.phone || '');
101
+ setLocation(currentUser.location || '');
102
+ setAbout(currentUser.about || '');
110
103
  }
111
104
  };
112
105
 
113
- const InfoCard = ({ icon: Icon, label, value, editable = false, fieldKey }: any) => (
114
- <View style={[styles.infoCard, { backgroundColor: `${colors.primary}08` }]}>
106
+ const InfoCard = ({ icon: Icon, label, value, editable = false, onChangeText }: any) => (
107
+ <View style={[styles.infoCard, { backgroundColor: colors.card || colors.background }]}>
115
108
  <View style={[styles.iconWrapper, { backgroundColor: `${colors.primary}15` }]}>
116
109
  <Icon size={20} color={colors.primary} />
117
110
  </View>
@@ -119,11 +112,12 @@ export default function ProfileScreen() {
119
112
  <Text style={[styles.infoLabel, { color: colors.textSecondary }]}>{label}</Text>
120
113
  {isEditing && editable ? (
121
114
  <TextInput
122
- style={[styles.infoInput, { color: colors.text, borderColor: colors.border }]}
115
+ style={[styles.infoInput, { color: colors.text, borderColor: `${colors.primary}30` }]}
123
116
  value={value}
124
- onChangeText={(text) => setProfileData({ ...profileData, [fieldKey]: text })}
117
+ onChangeText={onChangeText}
125
118
  placeholder={label}
126
119
  placeholderTextColor={colors.textSecondary}
120
+ editable={!saving}
127
121
  />
128
122
  ) : (
129
123
  <Text style={[styles.infoValue, { color: colors.text }]}>{value || '-'}</Text>
@@ -141,7 +135,7 @@ export default function ProfileScreen() {
141
135
 
142
136
  if (loading && !currentUser) {
143
137
  return (
144
- <SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
138
+ <SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} edges={['top']}>
145
139
  <AppHeader
146
140
  variant="back"
147
141
  title="Profile"
@@ -161,7 +155,7 @@ export default function ProfileScreen() {
161
155
 
162
156
  if (!currentUser) {
163
157
  return (
164
- <SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
158
+ <SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} edges={['top']}>
165
159
  <AppHeader
166
160
  variant="back"
167
161
  title="Profile"
@@ -179,7 +173,7 @@ export default function ProfileScreen() {
179
173
  }
180
174
 
181
175
  return (
182
- <SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
176
+ <SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} edges={['top']}>
183
177
  <AppHeader
184
178
  variant="back"
185
179
  title="Profile"
@@ -190,38 +184,50 @@ export default function ProfileScreen() {
190
184
  <ScrollView showsVerticalScrollIndicator={false}>
191
185
  {/* Header with Avatar */}
192
186
  <View style={styles.header}>
193
- <Image
194
- source={{ uri: profileData.avatar }}
195
- style={styles.avatarContainer}
196
- defaultSource={require('@/assets/images/icon.png')} // optional fallback
197
- />
198
-
199
- <Text style={[styles.userName, { color: colors.text }]}>{profileData.name}</Text>
200
- <View style={[styles.roleBadge, { backgroundColor: `${colors.primary}15` }]}>
201
- <Text style={[styles.roleText, { color: colors.primary }]}>
202
- {currentUser.role.toUpperCase()}
203
- </Text>
187
+ <View style={[styles.avatarContainer, { backgroundColor: `${colors.primary}20` }]}>
188
+ {avatar ? (
189
+ <Image
190
+ source={{ uri: avatar }}
191
+ style={styles.avatarImage}
192
+ defaultSource={require('@/assets/images/icon.png')}
193
+ />
194
+ ) : (
195
+ <Text style={[styles.avatarText, { color: colors.primary }]}>
196
+ {name.charAt(0).toUpperCase()}
197
+ </Text>
198
+ )}
204
199
  </View>
205
- <View style={[
206
- styles.statusBadge,
207
- { backgroundColor: currentUser.status === 'active' ? '#10b98110' : '#ef444410' }
208
- ]}>
200
+
201
+ <Text style={[styles.userName, { color: colors.text }]}>{name}</Text>
202
+
203
+ <View style={styles.badgeContainer}>
204
+ <View style={[styles.roleBadge, { backgroundColor: `${colors.primary}15` }]}>
205
+ <Text style={[styles.roleText, { color: colors.primary }]}>
206
+ {currentUser.role.toUpperCase()}
207
+ </Text>
208
+ </View>
209
+
209
210
  <View style={[
210
- styles.statusDot,
211
- { backgroundColor: currentUser.status === 'active' ? '#10b981' : '#ef4444' }
212
- ]} />
213
- <Text style={[
214
- styles.statusText,
215
- { color: currentUser.status === 'active' ? '#10b981' : '#ef4444' }
211
+ styles.statusBadge,
212
+ { backgroundColor: currentUser.status === 'active' ? '#10b98115' : '#ef444415' }
216
213
  ]}>
217
- {currentUser.status === 'active' ? 'Active' : 'Inactive'}
218
- </Text>
214
+ <View style={[
215
+ styles.statusDot,
216
+ { backgroundColor: currentUser.status === 'active' ? '#10b981' : '#ef4444' }
217
+ ]} />
218
+ <Text style={[
219
+ styles.statusText,
220
+ { color: currentUser.status === 'active' ? '#10b981' : '#ef4444' }
221
+ ]}>
222
+ {currentUser.status === 'active' ? 'Active' : 'Inactive'}
223
+ </Text>
224
+ </View>
219
225
  </View>
220
226
  </View>
221
227
 
222
228
  {/* Stats */}
223
229
  <View style={styles.statsContainer}>
224
- <StatCard title="Articles Read" value="127" color="#10b981" />
230
+ <StatCard title="Articles" value="127" color="#10b981" />
225
231
  <StatCard title="Favorites" value="34" color="#f59e0b" />
226
232
  <StatCard title="Comments" value="89" color="#3b82f6" />
227
233
  </View>
@@ -231,7 +237,7 @@ export default function ProfileScreen() {
231
237
  <TouchableOpacity
232
238
  style={[styles.editButton, { backgroundColor: colors.primary }]}
233
239
  onPress={() => setIsEditing(true)}
234
- activeOpacity={0.8}
240
+ activeOpacity={0.7}
235
241
  >
236
242
  <Edit2 size={18} color="#FFFFFF" />
237
243
  <Text style={styles.editButtonText}>Edit Profile</Text>
@@ -239,21 +245,31 @@ export default function ProfileScreen() {
239
245
  ) : (
240
246
  <View style={styles.editActions}>
241
247
  <TouchableOpacity
242
- style={[styles.actionButton, styles.cancelButton, { borderColor: colors.border }]}
248
+ style={[styles.actionButton, styles.cancelButton, {
249
+ borderColor: colors.border,
250
+ backgroundColor: colors.card || colors.background
251
+ }]}
243
252
  onPress={handleCancel}
244
- activeOpacity={0.8}
253
+ activeOpacity={0.7}
245
254
  disabled={saving}
246
255
  >
247
256
  <X size={18} color={colors.text} />
248
257
  <Text style={[styles.actionButtonText, { color: colors.text }]}>Cancel</Text>
249
258
  </TouchableOpacity>
250
259
  <TouchableOpacity
251
- style={[styles.actionButton, styles.saveButton, { backgroundColor: colors.primary }]}
260
+ style={[styles.actionButton, styles.saveButton, {
261
+ backgroundColor: colors.primary,
262
+ opacity: saving ? 0.7 : 1
263
+ }]}
252
264
  onPress={handleSave}
253
- activeOpacity={0.8}
265
+ activeOpacity={0.7}
254
266
  disabled={saving}
255
267
  >
256
- <Save size={18} color="#FFFFFF" />
268
+ {saving ? (
269
+ <ActivityIndicator size="small" color="#FFFFFF" />
270
+ ) : (
271
+ <Save size={18} color="#FFFFFF" />
272
+ )}
257
273
  <Text style={[styles.actionButtonText, { color: '#FFFFFF' }]}>
258
274
  {saving ? 'Saving...' : 'Save Changes'}
259
275
  </Text>
@@ -263,61 +279,66 @@ export default function ProfileScreen() {
263
279
 
264
280
  {/* Profile Information */}
265
281
  <View style={styles.section}>
266
- <Text style={[styles.sectionTitle, { color: colors.text }]}>Profile Information</Text>
282
+ <Text style={[styles.sectionTitle, { color: colors.text }]}>Personal Information</Text>
267
283
 
268
284
  <InfoCard
269
285
  icon={User}
270
- label="Name"
271
- value={profileData.name}
286
+ label="Full Name"
287
+ value={name}
272
288
  editable
273
- fieldKey="name"
289
+ onChangeText={setName}
274
290
  />
275
291
  <InfoCard
276
292
  icon={Mail}
277
- label="Email"
278
- value={profileData.email}
293
+ label="Email Address"
294
+ value={email}
279
295
  editable
280
- fieldKey="email"
296
+ onChangeText={setEmail}
281
297
  />
282
298
  <InfoCard
283
299
  icon={Phone}
284
- label="Phone"
285
- value={profileData.phone}
300
+ label="Phone Number"
301
+ value={phone}
286
302
  editable
287
- fieldKey="phone"
303
+ onChangeText={setPhone}
288
304
  />
289
305
  <InfoCard
290
306
  icon={MapPin}
291
307
  label="Location"
292
- value={profileData.location}
308
+ value={location}
293
309
  editable
294
- fieldKey="location"
310
+ onChangeText={setLocation}
295
311
  />
296
312
  <InfoCard
297
313
  icon={Calendar}
298
314
  label="Member Since"
299
- value={profileData.joinDate}
315
+ value={joinDate}
300
316
  />
301
317
  </View>
302
318
 
303
319
  {/* Bio Section */}
304
320
  <View style={styles.section}>
305
- <Text style={[styles.sectionTitle, { color: colors.text }]}>About</Text>
306
- <View style={[styles.bioCard, { backgroundColor: `${colors.primary}08` }]}>
321
+ <Text style={[styles.sectionTitle, { color: colors.text }]}>About Me</Text>
322
+ <View style={[styles.bioCard, { backgroundColor: colors.card || colors.background }]}>
307
323
  {isEditing ? (
308
324
  <TextInput
309
- style={[styles.bioInput, { color: colors.text, borderColor: colors.border }]}
310
- value={profileData.about}
311
- onChangeText={(text) => setProfileData({ ...profileData, about: text })}
325
+ style={[styles.bioInput, {
326
+ color: colors.text,
327
+ borderColor: `${colors.primary}30`,
328
+ backgroundColor: colors.background
329
+ }]}
330
+ value={about}
331
+ onChangeText={setAbout}
312
332
  placeholder="Tell us about yourself..."
313
333
  placeholderTextColor={colors.textSecondary}
314
334
  multiline
315
335
  numberOfLines={4}
316
336
  textAlignVertical="top"
337
+ editable={!saving}
317
338
  />
318
339
  ) : (
319
340
  <Text style={[styles.bioText, { color: colors.text }]}>
320
- {profileData.about || 'No bio available'}
341
+ {about || 'No bio available. Add your bio by editing your profile.'}
321
342
  </Text>
322
343
  )}
323
344
  </View>
@@ -350,50 +371,53 @@ const styles = StyleSheet.create({
350
371
  },
351
372
  header: {
352
373
  alignItems: 'center',
353
- paddingTop: 60,
354
- paddingBottom: 24,
374
+ paddingTop: 40,
375
+ paddingBottom: 32,
355
376
  paddingHorizontal: 24,
356
377
  },
357
378
  avatarContainer: {
358
- width: 100,
359
- height: 100,
360
- borderRadius: 50,
379
+ width: 120,
380
+ height: 120,
381
+ borderRadius: 60,
361
382
  justifyContent: 'center',
362
383
  alignItems: 'center',
363
- marginBottom: 16,
364
- elevation: 4,
384
+ marginBottom: 20,
385
+ elevation: 6,
365
386
  shadowColor: '#000',
366
- shadowOffset: { width: 0, height: 2 },
367
- shadowOpacity: 0.1,
368
- shadowRadius: 8,
387
+ shadowOffset: { width: 0, height: 4 },
388
+ shadowOpacity: 0.15,
389
+ shadowRadius: 12,
390
+ overflow: 'hidden',
391
+ },
392
+ avatarImage: {
393
+ width: '100%',
394
+ height: '100%',
395
+ borderRadius: 60,
369
396
  },
370
397
  avatarText: {
371
- fontSize: 36,
398
+ fontSize: 48,
372
399
  fontWeight: '800',
373
- color: '#FFFFFF',
374
- },
375
- userAvatar: {
376
- // width: 100,
377
- // height: 50,
378
- borderRadius: 25,
379
- marginRight: 12,
380
400
  },
381
401
  userName: {
382
402
  fontSize: 28,
383
403
  fontWeight: '800',
384
- marginBottom: 8,
404
+ marginBottom: 12,
405
+ textAlign: 'center',
406
+ },
407
+ badgeContainer: {
408
+ flexDirection: 'row',
409
+ alignItems: 'center',
410
+ gap: 8,
385
411
  },
386
412
  roleBadge: {
387
413
  paddingHorizontal: 16,
388
414
  paddingVertical: 6,
389
415
  borderRadius: 20,
390
- marginBottom: 8,
391
416
  },
392
417
  roleText: {
393
- fontSize: 13,
418
+ fontSize: 12,
394
419
  fontWeight: '700',
395
- textTransform: 'uppercase',
396
- letterSpacing: 0.5,
420
+ letterSpacing: 0.8,
397
421
  },
398
422
  statusBadge: {
399
423
  flexDirection: 'row',
@@ -420,43 +444,50 @@ const styles = StyleSheet.create({
420
444
  },
421
445
  statCard: {
422
446
  flex: 1,
423
- padding: 16,
447
+ padding: 20,
424
448
  borderRadius: 16,
425
449
  alignItems: 'center',
426
- elevation: 1,
450
+ elevation: 2,
427
451
  shadowColor: '#000',
428
- shadowOffset: { width: 0, height: 1 },
429
- shadowOpacity: 0.05,
430
- shadowRadius: 4,
452
+ shadowOffset: { width: 0, height: 2 },
453
+ shadowOpacity: 0.08,
454
+ shadowRadius: 8,
431
455
  },
432
456
  statValue: {
433
- fontSize: 24,
457
+ fontSize: 28,
434
458
  fontWeight: '800',
435
459
  marginBottom: 4,
436
460
  },
437
461
  statTitle: {
438
462
  fontSize: 11,
439
463
  fontWeight: '600',
464
+ textTransform: 'uppercase',
465
+ letterSpacing: 0.5,
440
466
  },
441
467
  editButton: {
442
468
  flexDirection: 'row',
443
469
  alignItems: 'center',
444
470
  justifyContent: 'center',
445
471
  marginHorizontal: 24,
446
- marginBottom: 24,
447
- paddingVertical: 14,
472
+ marginBottom: 32,
473
+ paddingVertical: 16,
448
474
  borderRadius: 16,
449
475
  gap: 8,
476
+ elevation: 3,
477
+ shadowColor: '#000',
478
+ shadowOffset: { width: 0, height: 2 },
479
+ shadowOpacity: 0.1,
480
+ shadowRadius: 8,
450
481
  },
451
482
  editButtonText: {
452
483
  color: '#FFFFFF',
453
- fontSize: 15,
484
+ fontSize: 16,
454
485
  fontWeight: '700',
455
486
  },
456
487
  editActions: {
457
488
  flexDirection: 'row',
458
489
  paddingHorizontal: 24,
459
- marginBottom: 24,
490
+ marginBottom: 32,
460
491
  gap: 12,
461
492
  },
462
493
  actionButton: {
@@ -464,9 +495,14 @@ const styles = StyleSheet.create({
464
495
  flexDirection: 'row',
465
496
  alignItems: 'center',
466
497
  justifyContent: 'center',
467
- paddingVertical: 14,
498
+ paddingVertical: 16,
468
499
  borderRadius: 16,
469
500
  gap: 8,
501
+ elevation: 2,
502
+ shadowColor: '#000',
503
+ shadowOffset: { width: 0, height: 1 },
504
+ shadowOpacity: 0.08,
505
+ shadowRadius: 4,
470
506
  },
471
507
  cancelButton: {
472
508
  borderWidth: 2,
@@ -480,12 +516,13 @@ const styles = StyleSheet.create({
480
516
  },
481
517
  section: {
482
518
  paddingHorizontal: 24,
483
- marginBottom: 24,
519
+ marginBottom: 32,
484
520
  },
485
521
  sectionTitle: {
486
- fontSize: 18,
522
+ fontSize: 20,
487
523
  fontWeight: '800',
488
524
  marginBottom: 16,
525
+ letterSpacing: 0.3,
489
526
  },
490
527
  infoCard: {
491
528
  flexDirection: 'row',
@@ -493,14 +530,19 @@ const styles = StyleSheet.create({
493
530
  padding: 16,
494
531
  borderRadius: 16,
495
532
  marginBottom: 12,
533
+ elevation: 1,
534
+ shadowColor: '#000',
535
+ shadowOffset: { width: 0, height: 1 },
536
+ shadowOpacity: 0.05,
537
+ shadowRadius: 4,
496
538
  },
497
539
  iconWrapper: {
498
- width: 44,
499
- height: 44,
500
- borderRadius: 12,
540
+ width: 48,
541
+ height: 48,
542
+ borderRadius: 14,
501
543
  justifyContent: 'center',
502
544
  alignItems: 'center',
503
- marginRight: 12,
545
+ marginRight: 14,
504
546
  },
505
547
  infoContent: {
506
548
  flex: 1,
@@ -508,32 +550,40 @@ const styles = StyleSheet.create({
508
550
  infoLabel: {
509
551
  fontSize: 12,
510
552
  fontWeight: '600',
511
- marginBottom: 4,
553
+ marginBottom: 6,
554
+ textTransform: 'uppercase',
555
+ letterSpacing: 0.5,
512
556
  },
513
557
  infoValue: {
514
- fontSize: 15,
558
+ fontSize: 16,
515
559
  fontWeight: '600',
516
560
  },
517
561
  infoInput: {
518
- fontSize: 15,
562
+ fontSize: 16,
519
563
  fontWeight: '600',
520
- borderBottomWidth: 1,
521
- paddingVertical: 4,
564
+ borderBottomWidth: 2,
565
+ paddingVertical: 6,
566
+ paddingHorizontal: 4,
522
567
  },
523
568
  bioCard: {
524
- padding: 16,
569
+ padding: 20,
525
570
  borderRadius: 16,
571
+ elevation: 1,
572
+ shadowColor: '#000',
573
+ shadowOffset: { width: 0, height: 1 },
574
+ shadowOpacity: 0.05,
575
+ shadowRadius: 4,
526
576
  },
527
577
  bioText: {
528
578
  fontSize: 15,
529
- lineHeight: 22,
579
+ lineHeight: 24,
530
580
  },
531
581
  bioInput: {
532
582
  fontSize: 15,
533
- lineHeight: 22,
534
- minHeight: 80,
535
- borderWidth: 1,
583
+ lineHeight: 24,
584
+ minHeight: 120,
585
+ borderWidth: 2,
536
586
  borderRadius: 12,
537
- padding: 12,
587
+ padding: 16,
538
588
  },
539
589
  });
@@ -31,7 +31,8 @@
31
31
  "react-native-worklets": "0.5.1",
32
32
  "react-native-svg": "15.12.1",
33
33
  "react-native-toast-message": "^2.3.3",
34
- "expo-linear-gradient": "~15.0.7"
34
+ "expo-linear-gradient": "~15.0.7",
35
+ "expo-local-authentication": "~17.0.7"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@babel/core": "^7.26.0",