@umituz/react-native-auth 2.5.4 → 2.5.6
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 +1 -1
- package/src/domain/entities/UserProfile.ts +19 -0
- package/src/index.ts +18 -0
- package/src/presentation/components/AccountActions.tsx +138 -0
- package/src/presentation/hooks/useAccountManagement.ts +41 -0
- package/src/presentation/hooks/useProfileEdit.ts +84 -0
- package/src/presentation/hooks/useProfileUpdate.ts +44 -0
- package/src/presentation/screens/AccountScreen.tsx +47 -0
- package/src/presentation/screens/EditProfileScreen.tsx +232 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-auth",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.6",
|
|
4
4
|
"description": "Authentication service for React Native apps - Secure, type-safe, and production-ready. Provider-agnostic design with dependency injection, configurable validation, and comprehensive error handling.",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Profile Types
|
|
3
|
+
* Domain types for user profile management across all apps
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface UserProfile {
|
|
7
|
+
uid: string;
|
|
8
|
+
email: string | null;
|
|
9
|
+
displayName: string | null;
|
|
10
|
+
photoURL: string | null;
|
|
11
|
+
isAnonymous: boolean;
|
|
12
|
+
createdAt: Date | null;
|
|
13
|
+
lastLoginAt: Date | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UpdateProfileParams {
|
|
17
|
+
displayName?: string;
|
|
18
|
+
photoURL?: string;
|
|
19
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -102,12 +102,28 @@ export type { UseAuthResult } from './presentation/hooks/useAuth';
|
|
|
102
102
|
export { useUserProfile } from './presentation/hooks/useUserProfile';
|
|
103
103
|
export type { UserProfileData, UseUserProfileParams } from './presentation/hooks/useUserProfile';
|
|
104
104
|
|
|
105
|
+
export { useAccountManagement } from './presentation/hooks/useAccountManagement';
|
|
106
|
+
export type { UseAccountManagementReturn } from './presentation/hooks/useAccountManagement';
|
|
107
|
+
|
|
108
|
+
export { useProfileUpdate } from './presentation/hooks/useProfileUpdate';
|
|
109
|
+
export type { UseProfileUpdateReturn } from './presentation/hooks/useProfileUpdate';
|
|
110
|
+
|
|
111
|
+
export { useProfileEdit } from './presentation/hooks/useProfileEdit';
|
|
112
|
+
export type { UseProfileEditReturn, ProfileEditFormState } from './presentation/hooks/useProfileEdit';
|
|
113
|
+
|
|
114
|
+
export type { UserProfile, UpdateProfileParams } from './domain/entities/UserProfile';
|
|
115
|
+
|
|
105
116
|
// =============================================================================
|
|
106
117
|
// PRESENTATION LAYER - Screens & Navigation
|
|
107
118
|
// =============================================================================
|
|
108
119
|
|
|
109
120
|
export { LoginScreen } from './presentation/screens/LoginScreen';
|
|
110
121
|
export { RegisterScreen } from './presentation/screens/RegisterScreen';
|
|
122
|
+
export { AccountScreen } from './presentation/screens/AccountScreen';
|
|
123
|
+
export type { AccountScreenConfig, AccountScreenProps } from './presentation/screens/AccountScreen';
|
|
124
|
+
export { EditProfileScreen } from './presentation/screens/EditProfileScreen';
|
|
125
|
+
export type { EditProfileConfig, EditProfileScreenProps } from './presentation/screens/EditProfileScreen';
|
|
126
|
+
|
|
111
127
|
export { AuthNavigator } from './presentation/navigation/AuthNavigator';
|
|
112
128
|
export type {
|
|
113
129
|
AuthStackParamList,
|
|
@@ -133,6 +149,8 @@ export { AuthBottomSheet } from './presentation/components/AuthBottomSheet';
|
|
|
133
149
|
export type { AuthBottomSheetProps } from './presentation/components/AuthBottomSheet';
|
|
134
150
|
export { ProfileSection } from './presentation/components/ProfileSection';
|
|
135
151
|
export type { ProfileSectionConfig, ProfileSectionProps } from './presentation/components/ProfileSection';
|
|
152
|
+
export { AccountActions } from './presentation/components/AccountActions';
|
|
153
|
+
export type { AccountActionsConfig, AccountActionsProps } from './presentation/components/AccountActions';
|
|
136
154
|
|
|
137
155
|
// =============================================================================
|
|
138
156
|
// PRESENTATION LAYER - Stores
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account Actions Component
|
|
3
|
+
* Provides logout and delete account functionality
|
|
4
|
+
* Only shown for authenticated (non-anonymous) users
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, Text, TouchableOpacity, StyleSheet, Alert } from "react-native";
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
10
|
+
|
|
11
|
+
export interface AccountActionsConfig {
|
|
12
|
+
logoutText?: string;
|
|
13
|
+
deleteAccountText?: string;
|
|
14
|
+
logoutConfirmTitle?: string;
|
|
15
|
+
logoutConfirmMessage?: string;
|
|
16
|
+
deleteConfirmTitle?: string;
|
|
17
|
+
deleteConfirmMessage?: string;
|
|
18
|
+
onLogout: () => Promise<void>;
|
|
19
|
+
onDeleteAccount: () => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AccountActionsProps {
|
|
23
|
+
config: AccountActionsConfig;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const AccountActions: React.FC<AccountActionsProps> = ({ config }) => {
|
|
27
|
+
const tokens = useAppDesignTokens();
|
|
28
|
+
const {
|
|
29
|
+
logoutText = "Log Out",
|
|
30
|
+
deleteAccountText = "Delete Account",
|
|
31
|
+
logoutConfirmTitle = "Log Out?",
|
|
32
|
+
logoutConfirmMessage = "Are you sure you want to log out?",
|
|
33
|
+
deleteConfirmTitle = "Delete Account?",
|
|
34
|
+
deleteConfirmMessage =
|
|
35
|
+
"This will permanently delete your account and all data. This action cannot be undone.",
|
|
36
|
+
onLogout,
|
|
37
|
+
onDeleteAccount,
|
|
38
|
+
} = config;
|
|
39
|
+
|
|
40
|
+
const handleLogout = () => {
|
|
41
|
+
Alert.alert(logoutConfirmTitle, logoutConfirmMessage, [
|
|
42
|
+
{ text: "Cancel", style: "cancel" },
|
|
43
|
+
{
|
|
44
|
+
text: logoutText,
|
|
45
|
+
style: "destructive",
|
|
46
|
+
onPress: async () => {
|
|
47
|
+
try {
|
|
48
|
+
await onLogout();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error("Logout failed:", error);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
]);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleDeleteAccount = () => {
|
|
58
|
+
Alert.alert(deleteConfirmTitle, deleteConfirmMessage, [
|
|
59
|
+
{ text: "Cancel", style: "cancel" },
|
|
60
|
+
{
|
|
61
|
+
text: deleteAccountText,
|
|
62
|
+
style: "destructive",
|
|
63
|
+
onPress: async () => {
|
|
64
|
+
try {
|
|
65
|
+
await onDeleteAccount();
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error("Delete account failed:", error);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<View style={styles.container}>
|
|
76
|
+
{/* Logout */}
|
|
77
|
+
<TouchableOpacity
|
|
78
|
+
style={[styles.actionButton, { borderColor: tokens.colors.border }]}
|
|
79
|
+
onPress={handleLogout}
|
|
80
|
+
activeOpacity={0.7}
|
|
81
|
+
>
|
|
82
|
+
<Text style={[styles.actionIcon, { color: tokens.colors.error }]}>
|
|
83
|
+
⎋
|
|
84
|
+
</Text>
|
|
85
|
+
<Text style={[styles.actionText, { color: tokens.colors.error }]}>
|
|
86
|
+
{logoutText}
|
|
87
|
+
</Text>
|
|
88
|
+
<Text style={[styles.chevron, { color: tokens.colors.textTertiary }]}>
|
|
89
|
+
›
|
|
90
|
+
</Text>
|
|
91
|
+
</TouchableOpacity>
|
|
92
|
+
|
|
93
|
+
{/* Delete Account */}
|
|
94
|
+
<TouchableOpacity
|
|
95
|
+
style={[styles.actionButton, { borderColor: tokens.colors.border }]}
|
|
96
|
+
onPress={handleDeleteAccount}
|
|
97
|
+
activeOpacity={0.7}
|
|
98
|
+
>
|
|
99
|
+
<Text style={[styles.actionIcon, { color: tokens.colors.error }]}>
|
|
100
|
+
🗑
|
|
101
|
+
</Text>
|
|
102
|
+
<Text style={[styles.actionText, { color: tokens.colors.error }]}>
|
|
103
|
+
{deleteAccountText}
|
|
104
|
+
</Text>
|
|
105
|
+
<Text style={[styles.chevron, { color: tokens.colors.textTertiary }]}>
|
|
106
|
+
›
|
|
107
|
+
</Text>
|
|
108
|
+
</TouchableOpacity>
|
|
109
|
+
</View>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const styles = StyleSheet.create({
|
|
114
|
+
container: {
|
|
115
|
+
gap: 12,
|
|
116
|
+
},
|
|
117
|
+
actionButton: {
|
|
118
|
+
flexDirection: "row",
|
|
119
|
+
alignItems: "center",
|
|
120
|
+
paddingVertical: 16,
|
|
121
|
+
paddingHorizontal: 16,
|
|
122
|
+
borderRadius: 12,
|
|
123
|
+
borderWidth: 1,
|
|
124
|
+
gap: 12,
|
|
125
|
+
},
|
|
126
|
+
actionIcon: {
|
|
127
|
+
fontSize: 20,
|
|
128
|
+
},
|
|
129
|
+
actionText: {
|
|
130
|
+
flex: 1,
|
|
131
|
+
fontSize: 16,
|
|
132
|
+
fontWeight: "500",
|
|
133
|
+
},
|
|
134
|
+
chevron: {
|
|
135
|
+
fontSize: 24,
|
|
136
|
+
fontWeight: "400",
|
|
137
|
+
},
|
|
138
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAccountManagement Hook
|
|
3
|
+
* Provides account management functionality (logout, delete)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from "react";
|
|
7
|
+
import { useAuth } from "./useAuth";
|
|
8
|
+
|
|
9
|
+
export interface UseAccountManagementReturn {
|
|
10
|
+
logout: () => Promise<void>;
|
|
11
|
+
deleteAccount: () => Promise<void>;
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const useAccountManagement = (): UseAccountManagementReturn => {
|
|
16
|
+
const { user, loading, signOut } = useAuth();
|
|
17
|
+
|
|
18
|
+
const logout = useCallback(async () => {
|
|
19
|
+
await signOut();
|
|
20
|
+
}, [signOut]);
|
|
21
|
+
|
|
22
|
+
const deleteAccount = useCallback(async () => {
|
|
23
|
+
if (!user) {
|
|
24
|
+
throw new Error("No user logged in");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (user.isAnonymous) {
|
|
28
|
+
throw new Error("Cannot delete anonymous account");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Note: Add user deletion logic via Firebase Admin SDK on backend
|
|
32
|
+
// Frontend should call backend API to delete user account
|
|
33
|
+
throw new Error("Account deletion requires backend implementation");
|
|
34
|
+
}, [user]);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
logout,
|
|
38
|
+
deleteAccount,
|
|
39
|
+
isLoading: loading,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useProfileEdit Hook
|
|
3
|
+
* Simple profile editing with form state management
|
|
4
|
+
* Apps provide image picker and backend update logic
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useCallback } from "react";
|
|
8
|
+
|
|
9
|
+
export interface ProfileEditFormState {
|
|
10
|
+
displayName: string;
|
|
11
|
+
email: string;
|
|
12
|
+
photoURL: string | null;
|
|
13
|
+
isModified: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UseProfileEditReturn {
|
|
17
|
+
formState: ProfileEditFormState;
|
|
18
|
+
setDisplayName: (value: string) => void;
|
|
19
|
+
setEmail: (value: string) => void;
|
|
20
|
+
setPhotoURL: (value: string | null) => void;
|
|
21
|
+
resetForm: (initial: Partial<ProfileEditFormState>) => void;
|
|
22
|
+
validateForm: () => { isValid: boolean; errors: string[] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const useProfileEdit = (
|
|
26
|
+
initialState: Partial<ProfileEditFormState> = {},
|
|
27
|
+
): UseProfileEditReturn => {
|
|
28
|
+
const [formState, setFormState] = useState<ProfileEditFormState>({
|
|
29
|
+
displayName: initialState.displayName || "",
|
|
30
|
+
email: initialState.email || "",
|
|
31
|
+
photoURL: initialState.photoURL || null,
|
|
32
|
+
isModified: false,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const setDisplayName = useCallback((value: string) => {
|
|
36
|
+
setFormState((prev) => ({ ...prev, displayName: value, isModified: true }));
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
const setEmail = useCallback((value: string) => {
|
|
40
|
+
setFormState((prev) => ({ ...prev, email: value, isModified: true }));
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const setPhotoURL = useCallback((value: string | null) => {
|
|
44
|
+
setFormState((prev) => ({ ...prev, photoURL: value, isModified: true }));
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const resetForm = useCallback((initial: Partial<ProfileEditFormState>) => {
|
|
48
|
+
setFormState({
|
|
49
|
+
displayName: initial.displayName || "",
|
|
50
|
+
email: initial.email || "",
|
|
51
|
+
photoURL: initial.photoURL || null,
|
|
52
|
+
isModified: false,
|
|
53
|
+
});
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
const validateForm = useCallback((): {
|
|
57
|
+
isValid: boolean;
|
|
58
|
+
errors: string[];
|
|
59
|
+
} => {
|
|
60
|
+
const errors: string[] = [];
|
|
61
|
+
|
|
62
|
+
if (!formState.displayName.trim()) {
|
|
63
|
+
errors.push("Display name is required");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (formState.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formState.email)) {
|
|
67
|
+
errors.push("Invalid email format");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
isValid: errors.length === 0,
|
|
72
|
+
errors,
|
|
73
|
+
};
|
|
74
|
+
}, [formState]);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
formState,
|
|
78
|
+
setDisplayName,
|
|
79
|
+
setEmail,
|
|
80
|
+
setPhotoURL,
|
|
81
|
+
resetForm,
|
|
82
|
+
validateForm,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useProfileUpdate Hook
|
|
3
|
+
* Hook for profile updates - implementation should be provided by app
|
|
4
|
+
* Apps should use Firebase SDK directly or backend API
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useCallback } from "react";
|
|
8
|
+
import { useAuth } from "./useAuth";
|
|
9
|
+
import type { UpdateProfileParams } from "../../domain/entities/UserProfile";
|
|
10
|
+
|
|
11
|
+
export interface UseProfileUpdateReturn {
|
|
12
|
+
updateProfile: (params: UpdateProfileParams) => Promise<void>;
|
|
13
|
+
isUpdating: boolean;
|
|
14
|
+
error: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const useProfileUpdate = (): UseProfileUpdateReturn => {
|
|
18
|
+
const { user } = useAuth();
|
|
19
|
+
const [isUpdating, setIsUpdating] = useState(false);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
const updateProfile = useCallback(
|
|
23
|
+
async (params: UpdateProfileParams) => {
|
|
24
|
+
if (!user) {
|
|
25
|
+
throw new Error("No user logged in");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (user.isAnonymous) {
|
|
29
|
+
throw new Error("Anonymous users cannot update profile");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Note: App should implement this via Firebase SDK
|
|
33
|
+
// Example: auth().currentUser?.updateProfile({ displayName, photoURL })
|
|
34
|
+
throw new Error("Profile update should be implemented by app");
|
|
35
|
+
},
|
|
36
|
+
[user],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
updateProfile,
|
|
41
|
+
isUpdating,
|
|
42
|
+
error,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account Screen
|
|
3
|
+
* Pure UI component for account management
|
|
4
|
+
* Business logic provided via props from app layer
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, ScrollView, StyleSheet } from "react-native";
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
10
|
+
import { ProfileSection, type ProfileSectionConfig } from "../components/ProfileSection";
|
|
11
|
+
import { AccountActions, type AccountActionsConfig } from "../components/AccountActions";
|
|
12
|
+
|
|
13
|
+
export interface AccountScreenConfig {
|
|
14
|
+
profile: ProfileSectionConfig;
|
|
15
|
+
accountActions: AccountActionsConfig;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AccountScreenProps {
|
|
19
|
+
config: AccountScreenConfig;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const AccountScreen: React.FC<AccountScreenProps> = ({ config }) => {
|
|
23
|
+
const tokens = useAppDesignTokens();
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<ScrollView
|
|
27
|
+
style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
|
|
28
|
+
contentContainerStyle={styles.content}
|
|
29
|
+
>
|
|
30
|
+
<ProfileSection profile={config.profile} />
|
|
31
|
+
<View style={styles.divider} />
|
|
32
|
+
<AccountActions config={config.accountActions} />
|
|
33
|
+
</ScrollView>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const styles = StyleSheet.create({
|
|
38
|
+
container: {
|
|
39
|
+
flex: 1,
|
|
40
|
+
},
|
|
41
|
+
content: {
|
|
42
|
+
padding: 16,
|
|
43
|
+
},
|
|
44
|
+
divider: {
|
|
45
|
+
height: 24,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit Profile Screen
|
|
3
|
+
* Pure UI for profile editing
|
|
4
|
+
* Business logic provided via props
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import {
|
|
9
|
+
View,
|
|
10
|
+
Text,
|
|
11
|
+
TextInput,
|
|
12
|
+
TouchableOpacity,
|
|
13
|
+
ScrollView,
|
|
14
|
+
StyleSheet,
|
|
15
|
+
ActivityIndicator,
|
|
16
|
+
} from "react-native";
|
|
17
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
18
|
+
|
|
19
|
+
export interface EditProfileConfig {
|
|
20
|
+
displayName: string;
|
|
21
|
+
email: string;
|
|
22
|
+
photoURL: string | null;
|
|
23
|
+
isLoading?: boolean;
|
|
24
|
+
isSaving?: boolean;
|
|
25
|
+
onChangeDisplayName: (value: string) => void;
|
|
26
|
+
onChangeEmail: (value: string) => void;
|
|
27
|
+
onChangePhoto?: () => void;
|
|
28
|
+
onSave: () => void;
|
|
29
|
+
onCancel?: () => void;
|
|
30
|
+
labels: {
|
|
31
|
+
title: string;
|
|
32
|
+
displayNameLabel: string;
|
|
33
|
+
displayNamePlaceholder: string;
|
|
34
|
+
emailLabel: string;
|
|
35
|
+
emailPlaceholder: string;
|
|
36
|
+
photoLabel: string;
|
|
37
|
+
changePhotoButton: string;
|
|
38
|
+
saveButton: string;
|
|
39
|
+
cancelButton: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface EditProfileScreenProps {
|
|
44
|
+
config: EditProfileConfig;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const EditProfileScreen: React.FC<EditProfileScreenProps> = ({
|
|
48
|
+
config,
|
|
49
|
+
}) => {
|
|
50
|
+
const tokens = useAppDesignTokens();
|
|
51
|
+
|
|
52
|
+
if (config.isLoading) {
|
|
53
|
+
return (
|
|
54
|
+
<View style={[styles.loading, { backgroundColor: tokens.colors.backgroundPrimary }]}>
|
|
55
|
+
<ActivityIndicator size="large" color={tokens.colors.primary} />
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<ScrollView
|
|
62
|
+
style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
|
|
63
|
+
contentContainerStyle={styles.content}
|
|
64
|
+
>
|
|
65
|
+
<Text style={[styles.title, { color: tokens.colors.text }]}>
|
|
66
|
+
{config.labels.title}
|
|
67
|
+
</Text>
|
|
68
|
+
|
|
69
|
+
{/* Display Name */}
|
|
70
|
+
<View style={styles.field}>
|
|
71
|
+
<Text style={[styles.label, { color: tokens.colors.textSecondary }]}>
|
|
72
|
+
{config.labels.displayNameLabel}
|
|
73
|
+
</Text>
|
|
74
|
+
<TextInput
|
|
75
|
+
style={[
|
|
76
|
+
styles.input,
|
|
77
|
+
{
|
|
78
|
+
backgroundColor: tokens.colors.surface,
|
|
79
|
+
color: tokens.colors.text,
|
|
80
|
+
borderColor: tokens.colors.border,
|
|
81
|
+
},
|
|
82
|
+
]}
|
|
83
|
+
value={config.displayName}
|
|
84
|
+
onChangeText={config.onChangeDisplayName}
|
|
85
|
+
placeholder={config.labels.displayNamePlaceholder}
|
|
86
|
+
placeholderTextColor={tokens.colors.textTertiary}
|
|
87
|
+
/>
|
|
88
|
+
</View>
|
|
89
|
+
|
|
90
|
+
{/* Email */}
|
|
91
|
+
<View style={styles.field}>
|
|
92
|
+
<Text style={[styles.label, { color: tokens.colors.textSecondary }]}>
|
|
93
|
+
{config.labels.emailLabel}
|
|
94
|
+
</Text>
|
|
95
|
+
<TextInput
|
|
96
|
+
style={[
|
|
97
|
+
styles.input,
|
|
98
|
+
{
|
|
99
|
+
backgroundColor: tokens.colors.surface,
|
|
100
|
+
color: tokens.colors.text,
|
|
101
|
+
borderColor: tokens.colors.border,
|
|
102
|
+
},
|
|
103
|
+
]}
|
|
104
|
+
value={config.email}
|
|
105
|
+
onChangeText={config.onChangeEmail}
|
|
106
|
+
placeholder={config.labels.emailPlaceholder}
|
|
107
|
+
placeholderTextColor={tokens.colors.textTertiary}
|
|
108
|
+
keyboardType="email-address"
|
|
109
|
+
autoCapitalize="none"
|
|
110
|
+
/>
|
|
111
|
+
</View>
|
|
112
|
+
|
|
113
|
+
{/* Photo */}
|
|
114
|
+
{config.onChangePhoto && (
|
|
115
|
+
<View style={styles.field}>
|
|
116
|
+
<Text style={[styles.label, { color: tokens.colors.textSecondary }]}>
|
|
117
|
+
{config.labels.photoLabel}
|
|
118
|
+
</Text>
|
|
119
|
+
<TouchableOpacity
|
|
120
|
+
style={[
|
|
121
|
+
styles.photoButton,
|
|
122
|
+
{ backgroundColor: tokens.colors.surface, borderColor: tokens.colors.border },
|
|
123
|
+
]}
|
|
124
|
+
onPress={config.onChangePhoto}
|
|
125
|
+
>
|
|
126
|
+
<Text style={[styles.photoButtonText, { color: tokens.colors.primary }]}>
|
|
127
|
+
{config.labels.changePhotoButton}
|
|
128
|
+
</Text>
|
|
129
|
+
</TouchableOpacity>
|
|
130
|
+
</View>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
{/* Actions */}
|
|
134
|
+
<View style={styles.actions}>
|
|
135
|
+
<TouchableOpacity
|
|
136
|
+
style={[styles.saveButton, { backgroundColor: tokens.colors.primary }]}
|
|
137
|
+
onPress={config.onSave}
|
|
138
|
+
disabled={config.isSaving}
|
|
139
|
+
>
|
|
140
|
+
{config.isSaving ? (
|
|
141
|
+
<ActivityIndicator size="small" color={tokens.colors.onPrimary} />
|
|
142
|
+
) : (
|
|
143
|
+
<Text style={[styles.saveButtonText, { color: tokens.colors.onPrimary }]}>
|
|
144
|
+
{config.labels.saveButton}
|
|
145
|
+
</Text>
|
|
146
|
+
)}
|
|
147
|
+
</TouchableOpacity>
|
|
148
|
+
|
|
149
|
+
{config.onCancel && (
|
|
150
|
+
<TouchableOpacity
|
|
151
|
+
style={[
|
|
152
|
+
styles.cancelButton,
|
|
153
|
+
{ backgroundColor: tokens.colors.surface, borderColor: tokens.colors.border },
|
|
154
|
+
]}
|
|
155
|
+
onPress={config.onCancel}
|
|
156
|
+
disabled={config.isSaving}
|
|
157
|
+
>
|
|
158
|
+
<Text style={[styles.cancelButtonText, { color: tokens.colors.text }]}>
|
|
159
|
+
{config.labels.cancelButton}
|
|
160
|
+
</Text>
|
|
161
|
+
</TouchableOpacity>
|
|
162
|
+
)}
|
|
163
|
+
</View>
|
|
164
|
+
</ScrollView>
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const styles = StyleSheet.create({
|
|
169
|
+
container: {
|
|
170
|
+
flex: 1,
|
|
171
|
+
},
|
|
172
|
+
content: {
|
|
173
|
+
padding: 16,
|
|
174
|
+
},
|
|
175
|
+
loading: {
|
|
176
|
+
flex: 1,
|
|
177
|
+
justifyContent: "center",
|
|
178
|
+
alignItems: "center",
|
|
179
|
+
},
|
|
180
|
+
title: {
|
|
181
|
+
fontSize: 24,
|
|
182
|
+
fontWeight: "600",
|
|
183
|
+
marginBottom: 24,
|
|
184
|
+
},
|
|
185
|
+
field: {
|
|
186
|
+
marginBottom: 20,
|
|
187
|
+
},
|
|
188
|
+
label: {
|
|
189
|
+
fontSize: 14,
|
|
190
|
+
fontWeight: "500",
|
|
191
|
+
marginBottom: 8,
|
|
192
|
+
},
|
|
193
|
+
input: {
|
|
194
|
+
borderWidth: 1,
|
|
195
|
+
borderRadius: 8,
|
|
196
|
+
padding: 12,
|
|
197
|
+
fontSize: 16,
|
|
198
|
+
},
|
|
199
|
+
photoButton: {
|
|
200
|
+
borderWidth: 1,
|
|
201
|
+
borderRadius: 8,
|
|
202
|
+
padding: 12,
|
|
203
|
+
alignItems: "center",
|
|
204
|
+
},
|
|
205
|
+
photoButtonText: {
|
|
206
|
+
fontSize: 14,
|
|
207
|
+
fontWeight: "500",
|
|
208
|
+
},
|
|
209
|
+
actions: {
|
|
210
|
+
marginTop: 32,
|
|
211
|
+
gap: 12,
|
|
212
|
+
},
|
|
213
|
+
saveButton: {
|
|
214
|
+
padding: 16,
|
|
215
|
+
borderRadius: 8,
|
|
216
|
+
alignItems: "center",
|
|
217
|
+
},
|
|
218
|
+
saveButtonText: {
|
|
219
|
+
fontSize: 16,
|
|
220
|
+
fontWeight: "600",
|
|
221
|
+
},
|
|
222
|
+
cancelButton: {
|
|
223
|
+
padding: 16,
|
|
224
|
+
borderRadius: 8,
|
|
225
|
+
alignItems: "center",
|
|
226
|
+
borderWidth: 1,
|
|
227
|
+
},
|
|
228
|
+
cancelButtonText: {
|
|
229
|
+
fontSize: 16,
|
|
230
|
+
fontWeight: "500",
|
|
231
|
+
},
|
|
232
|
+
});
|