@umituz/react-native-auth 3.4.30 → 3.4.32

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.
@@ -0,0 +1,327 @@
1
+ # useProfileUpdate & useProfileEdit
2
+
3
+ Hooks for profile update operations and profile editing form management.
4
+
5
+ ---
6
+
7
+ ## useProfileUpdate
8
+
9
+ Hook for profile update operations. Implementation should be provided by the app using Firebase SDK or backend API.
10
+
11
+ ### Usage
12
+
13
+ ```typescript
14
+ import { useProfileUpdate } from '@umituz/react-native-auth';
15
+
16
+ function ProfileSettings() {
17
+ const { updateProfile, isUpdating, error } = useProfileUpdate();
18
+
19
+ const handleUpdate = async (data: UpdateProfileParams) => {
20
+ try {
21
+ await updateProfile(data);
22
+ } catch (err) {
23
+ console.error(err);
24
+ }
25
+ };
26
+
27
+ return <ProfileForm onSave={handleUpdate} />;
28
+ }
29
+ ```
30
+
31
+ ### API
32
+
33
+ | Prop | Type | Description |
34
+ |------|------|-------------|
35
+ | `updateProfile` | `(params: UpdateProfileParams) => Promise<void>` | Profile update function |
36
+ | `isUpdating` | `boolean` | Update in progress |
37
+ | `error` | `string \| null` | Error message |
38
+
39
+ ### Create Your Own Implementation
40
+
41
+ ```typescript
42
+ function useProfileUpdate() {
43
+ const { user } = useAuth();
44
+ const [isUpdating, setIsUpdating] = useState(false);
45
+ const [error, setError] = useState<string | null>(null);
46
+
47
+ const updateProfile = async (params: UpdateProfileParams) => {
48
+ if (!user) {
49
+ throw new Error("No user logged in");
50
+ }
51
+
52
+ if (user.isAnonymous) {
53
+ throw new Error("Anonymous users cannot update profile");
54
+ }
55
+
56
+ setIsUpdating(true);
57
+ setError(null);
58
+
59
+ try {
60
+ // Update profile in Firebase Auth
61
+ await updateProfile(user, {
62
+ displayName: params.displayName,
63
+ photoURL: params.photoURL,
64
+ });
65
+
66
+ // Update user document in Firestore
67
+ await updateDoc(doc(db, 'users', user.uid), {
68
+ displayName: params.displayName,
69
+ photoURL: params.photoURL,
70
+ updatedAt: serverTimestamp(),
71
+ });
72
+ } catch (err) {
73
+ const message = err instanceof Error ? err.message : 'Update failed';
74
+ setError(message);
75
+ throw err;
76
+ } finally {
77
+ setIsUpdating(false);
78
+ }
79
+ };
80
+
81
+ return { updateProfile, isUpdating, error };
82
+ }
83
+ ```
84
+
85
+ ---
86
+
87
+ ## useProfileEdit
88
+
89
+ Hook for simple profile editing with form state management.
90
+
91
+ ### Usage
92
+
93
+ ```typescript
94
+ import { useProfileEdit } from '@umituz/react-native-auth';
95
+
96
+ function EditProfileScreen({ navigation }) {
97
+ const {
98
+ formState,
99
+ setDisplayName,
100
+ setEmail,
101
+ setPhotoURL,
102
+ resetForm,
103
+ validateForm,
104
+ } = useProfileEdit({
105
+ displayName: user?.displayName || '',
106
+ email: user?.email || '',
107
+ photoURL: user?.photoURL || null,
108
+ });
109
+
110
+ const handleSave = () => {
111
+ const { isValid, errors } = validateForm();
112
+
113
+ if (!isValid) {
114
+ Alert.alert('Error', errors.join('\n'));
115
+ return;
116
+ }
117
+
118
+ updateProfile({
119
+ displayName: formState.displayName,
120
+ photoURL: formState.photoURL,
121
+ });
122
+
123
+ navigation.goBack();
124
+ };
125
+
126
+ return (
127
+ <ScrollView>
128
+ <TextInput
129
+ value={formState.displayName}
130
+ onChangeText={setDisplayName}
131
+ placeholder="Full Name"
132
+ />
133
+
134
+ <TextInput
135
+ value={formState.email}
136
+ onChangeText={setEmail}
137
+ placeholder="Email"
138
+ editable={false}
139
+ />
140
+
141
+ <AvatarUploader
142
+ photoURL={formState.photoURL}
143
+ onImageSelected={setPhotoURL}
144
+ />
145
+
146
+ <View style={styles.buttons}>
147
+ <Button onPress={navigation.goBack}>Cancel</Button>
148
+ <Button
149
+ onPress={handleSave}
150
+ disabled={!formState.isModified}
151
+ >
152
+ Save
153
+ </Button>
154
+ </View>
155
+ </ScrollView>
156
+ );
157
+ }
158
+ ```
159
+
160
+ ### API
161
+
162
+ #### Return Value
163
+
164
+ | Prop | Type | Description |
165
+ |------|------|-------------|
166
+ | `formState` | `ProfileEditFormState` | Form state |
167
+ | `setDisplayName` | `(value: string) => void` | Set display name |
168
+ | `setEmail` | `(value: string) => void` | Set email |
169
+ | `setPhotoURL` | `(value: string \| null) => void` | Set photo URL |
170
+ | `resetForm` | `(initial: Partial<ProfileEditFormState>) => void` | Reset form |
171
+ | `validateForm` | `() => { isValid: boolean; errors: string[] }` | Validate form |
172
+
173
+ #### ProfileEditFormState
174
+
175
+ | Prop | Type | Description |
176
+ |------|------|-------------|
177
+ | `displayName` | `string` | Display name |
178
+ | `email` | `string` | Email |
179
+ | `photoURL` | `string \| null` | Photo URL |
180
+ | `isModified` | `boolean` | Form has been modified |
181
+
182
+ ### Validation
183
+
184
+ `validateForm()` checks:
185
+
186
+ - **Display name**: Cannot be empty
187
+ - **Email**: Valid email format (if provided)
188
+
189
+ ```typescript
190
+ const { isValid, errors } = validateForm();
191
+
192
+ if (!isValid) {
193
+ errors.forEach(error => console.log(error));
194
+ // ["Display name is required", "Invalid email format"]
195
+ }
196
+ ```
197
+
198
+ ## Examples
199
+
200
+ ### Profile Photo Upload
201
+
202
+ ```typescript
203
+ function ProfilePhotoSection() {
204
+ const { formState, setPhotoURL } = useProfileEdit(initialState);
205
+
206
+ const handlePickImage = async () => {
207
+ const result = await launchImageLibrary({
208
+ mediaType: 'photo',
209
+ quality: 0.8,
210
+ });
211
+
212
+ if (result.assets?.[0]) {
213
+ // Upload to storage and get URL
214
+ const url = await uploadToStorage(result.assets[0].uri);
215
+ setPhotoURL(url);
216
+ }
217
+ };
218
+
219
+ return (
220
+ <TouchableOpacity onPress={handlePickImage}>
221
+ {formState.photoURL ? (
222
+ <Image source={{ uri: formState.photoURL }} />
223
+ ) : (
224
+ <View style={styles.placeholder}>
225
+ <Text>Select Photo</Text>
226
+ </View>
227
+ )}
228
+ </TouchableOpacity>
229
+ );
230
+ }
231
+ ```
232
+
233
+ ### Unsaved Changes Warning
234
+
235
+ ```typescript
236
+ function EditProfileScreen({ navigation }) {
237
+ const {
238
+ formState,
239
+ resetForm,
240
+ validateForm
241
+ } = useProfileEdit(initialState);
242
+
243
+ useEffect(() => {
244
+ const unsubscribe = navigation.addListener('beforeRemove', (e) => {
245
+ if (!formState.isModified) {
246
+ return;
247
+ }
248
+
249
+ e.preventDefault();
250
+
251
+ Alert.alert(
252
+ 'Unsaved Changes',
253
+ 'You have unsaved changes. What would you like to do?',
254
+ [
255
+ { text: 'Don\'t Save', style: 'cancel' },
256
+ {
257
+ text: 'Save',
258
+ onPress: () => {
259
+ saveChanges();
260
+ navigation.dispatch(e.data.action);
261
+ }
262
+ },
263
+ {
264
+ text: 'Discard',
265
+ style: 'destructive',
266
+ onPress: () => {
267
+ resetForm(initialState);
268
+ navigation.dispatch(e.data.action);
269
+ }
270
+ },
271
+ ]
272
+ );
273
+ });
274
+
275
+ return unsubscribe;
276
+ }, [navigation, formState.isModified]);
277
+
278
+ // ...
279
+ }
280
+ ```
281
+
282
+ ### Custom Validation
283
+
284
+ ```typescript
285
+ function ExtendedProfileEdit() {
286
+ const {
287
+ formState,
288
+ setDisplayName,
289
+ setEmail,
290
+ setPhotoURL,
291
+ validateForm
292
+ } = useProfileEdit(initialState);
293
+
294
+ const handleSave = () => {
295
+ // Base validation
296
+ const { isValid, errors } = validateForm();
297
+
298
+ // Custom validation
299
+ const customErrors = [];
300
+
301
+ if (formState.displayName.length < 3) {
302
+ customErrors.push('Display name must be at least 3 characters');
303
+ }
304
+
305
+ if (formState.photoURL && !isValidImageUrl(formState.photoURL)) {
306
+ customErrors.push('Invalid image URL');
307
+ }
308
+
309
+ const allErrors = [...errors, ...customErrors];
310
+
311
+ if (allErrors.length > 0) {
312
+ Alert.alert('Error', allErrors.join('\n'));
313
+ return;
314
+ }
315
+
316
+ saveProfile();
317
+ };
318
+
319
+ // ...
320
+ }
321
+ ```
322
+
323
+ ## Related Hooks
324
+
325
+ - [`useUserProfile`](./useUserProfile.md) - Display profile data
326
+ - [`useAuth`](./useAuth.md) - Main auth state management
327
+ - [`useAccountManagement`](./useAccountManagement.md) - Account operations
@@ -0,0 +1,356 @@
1
+ # Social Login Hooks
2
+
3
+ Hooks for Google and Apple social authentication.
4
+
5
+ ---
6
+
7
+ ## useSocialLogin
8
+
9
+ General social login functionality. Wraps `@umituz/react-native-firebase`'s `useSocialAuth`.
10
+
11
+ ### Usage
12
+
13
+ ```typescript
14
+ import { useSocialLogin } from '@umituz/react-native-auth';
15
+
16
+ function LoginScreen() {
17
+ const {
18
+ signInWithGoogle,
19
+ signInWithApple,
20
+ googleLoading,
21
+ appleLoading,
22
+ googleConfigured,
23
+ appleAvailable,
24
+ } = useSocialLogin({
25
+ google: {
26
+ webClientId: 'your-web-client-id',
27
+ iosClientId: 'your-ios-client-id',
28
+ },
29
+ apple: { enabled: true },
30
+ });
31
+
32
+ return (
33
+ <View>
34
+ <Button onPress={signInWithGoogle} disabled={googleLoading || !googleConfigured}>
35
+ {googleLoading ? 'Signing in...' : 'Sign in with Google'}
36
+ </Button>
37
+
38
+ <Button onPress={signInWithApple} disabled={appleLoading || !appleAvailable}>
39
+ {appleLoading ? 'Signing in...' : 'Sign in with Apple'}
40
+ </Button>
41
+ </View>
42
+ );
43
+ }
44
+ ```
45
+
46
+ ### API
47
+
48
+ | Prop | Type | Description |
49
+ |------|------|-------------|
50
+ | `signInWithGoogle` | `() => Promise<SocialAuthResult>` | Google sign-in (use `useGoogleAuth` for OAuth) |
51
+ | `signInWithApple` | `() => Promise<SocialAuthResult>` | Apple sign-in |
52
+ | `googleLoading` | `boolean` | Google loading state |
53
+ | `appleLoading` | `boolean` | Apple loading state |
54
+ | `googleConfigured` | `boolean` | Google is configured |
55
+ | `appleAvailable` | `boolean` | Apple is available (iOS only) |
56
+
57
+ ---
58
+
59
+ ## useGoogleAuth
60
+
61
+ Handles complete Google OAuth flow with `expo-auth-session`.
62
+
63
+ ### Usage
64
+
65
+ ```typescript
66
+ import { useGoogleAuth } from '@umituz/react-native-auth';
67
+
68
+ function LoginScreen() {
69
+ const { signInWithGoogle, googleLoading, googleConfigured } = useGoogleAuth({
70
+ iosClientId: 'your-ios-client-id.apps.googleusercontent.com',
71
+ webClientId: 'your-web-client-id.apps.googleusercontent.com',
72
+ });
73
+
74
+ const handleGoogleSignIn = async () => {
75
+ const result = await signInWithGoogle();
76
+
77
+ if (result.success) {
78
+ console.log('Google sign-in successful');
79
+ } else {
80
+ Alert.alert('Error', result.error || 'Sign-in failed');
81
+ }
82
+ };
83
+
84
+ return (
85
+ <Button onPress={handleGoogleSignIn} disabled={googleLoading || !googleConfigured}>
86
+ Sign in with Google
87
+ </Button>
88
+ );
89
+ }
90
+ ```
91
+
92
+ ### API
93
+
94
+ #### Parameters
95
+
96
+ | Param | Type | Required | Description |
97
+ |-------|------|----------|-------------|
98
+ | `iosClientId` | `string` | No* | iOS Google Client ID |
99
+ | `webClientId` | `string` | No* | Web Google Client ID |
100
+ | `androidClientId` | `string` | No* | Android Google Client ID |
101
+
102
+ *At least one must be provided
103
+
104
+ #### Return Value
105
+
106
+ | Prop | Type | Description |
107
+ |------|------|-------------|
108
+ | `signInWithGoogle` | `() => Promise<SocialAuthResult>` | Google sign-in function |
109
+ | `googleLoading` | `boolean` | Loading state |
110
+ | `googleConfigured` | `boolean` | Is configured |
111
+
112
+ ### Examples
113
+
114
+ #### Google Sign-In Screen
115
+
116
+ ```typescript
117
+ function SocialLoginScreen() {
118
+ const { signInWithGoogle, googleLoading } = useGoogleAuth({
119
+ iosClientId: Config.GOOGLE_IOS_CLIENT_ID,
120
+ webClientId: Config.GOOGLE_WEB_CLIENT_ID,
121
+ });
122
+
123
+ const { signInWithApple, appleLoading, appleAvailable } = useAppleAuth();
124
+
125
+ return (
126
+ <View style={styles.container}>
127
+ <Text style={styles.title}>Sign In</Text>
128
+
129
+ <TouchableOpacity
130
+ style={styles.googleButton}
131
+ onPress={signInWithGoogle}
132
+ disabled={googleLoading}
133
+ >
134
+ {googleLoading ? (
135
+ <ActivityIndicator />
136
+ ) : (
137
+ <>
138
+ <GoogleIcon />
139
+ <Text>Continue with Google</Text>
140
+ </>
141
+ )}
142
+ </TouchableOpacity>
143
+
144
+ {Platform.OS === 'ios' && appleAvailable && (
145
+ <TouchableOpacity
146
+ style={styles.appleButton}
147
+ onPress={signInWithApple}
148
+ disabled={appleLoading}
149
+ >
150
+ {appleLoading ? (
151
+ <ActivityIndicator />
152
+ ) : (
153
+ <>
154
+ <AppleIcon />
155
+ <Text>Continue with Apple</Text>
156
+ </>
157
+ )}
158
+ </TouchableOpacity>
159
+ )}
160
+ </View>
161
+ );
162
+ }
163
+ ```
164
+
165
+ #### Error Handling
166
+
167
+ ```typescript
168
+ function LoginWithErrorHandling() {
169
+ const { signInWithGoogle, googleLoading } = useGoogleAuth({
170
+ webClientId: Config.GOOGLE_WEB_CLIENT_ID,
171
+ });
172
+
173
+ const handleGoogleSignIn = async () => {
174
+ try {
175
+ const result = await signInWithGoogle();
176
+
177
+ if (result.success) {
178
+ navigation.navigate('Home');
179
+ } else {
180
+ // User cancelled or error
181
+ if (result.error?.includes('cancelled')) {
182
+ return; // Silent cancel
183
+ }
184
+ Alert.alert('Error', result.error || 'Google sign-in failed');
185
+ }
186
+ } catch (error) {
187
+ Alert.alert('Error', 'An unexpected error occurred');
188
+ }
189
+ };
190
+
191
+ return <Button onPress={handleGoogleSignIn} />;
192
+ }
193
+ ```
194
+
195
+ ---
196
+
197
+ ## useAppleAuth
198
+
199
+ Convenience wrapper for Apple Sign-In.
200
+
201
+ ### Usage
202
+
203
+ ```typescript
204
+ import { useAppleAuth } from '@umituz/react-native-auth';
205
+ import { Platform } from 'react-native';
206
+
207
+ function LoginScreen() {
208
+ const { signInWithApple, appleLoading, appleAvailable } = useAppleAuth();
209
+
210
+ if (Platform.OS !== 'ios' || !appleAvailable) {
211
+ return null;
212
+ }
213
+
214
+ return (
215
+ <TouchableOpacity
216
+ onPress={signInWithApple}
217
+ disabled={appleLoading}
218
+ style={styles.appleButton}
219
+ >
220
+ {appleLoading ? (
221
+ <ActivityIndicator />
222
+ ) : (
223
+ <>
224
+ <AppleIcon />
225
+ <Text>Sign in with Apple</Text>
226
+ </>
227
+ )}
228
+ </TouchableOpacity>
229
+ );
230
+ }
231
+ ```
232
+
233
+ ### API
234
+
235
+ | Prop | Type | Description |
236
+ |------|------|-------------|
237
+ | `signInWithApple` | `() => Promise<SocialAuthResult>` | Apple sign-in function |
238
+ | `appleLoading` | `boolean` | Loading state |
239
+ | `appleAvailable` | `boolean` | Apple Sign-In is available (iOS only) |
240
+
241
+ ### Platform-Specific Button
242
+
243
+ ```typescript
244
+ function SocialLoginButtons() {
245
+ const { signInWithApple, appleLoading, appleAvailable } = useAppleAuth();
246
+ const { signInWithGoogle, googleLoading } = useGoogleAuth({
247
+ webClientId: Config.GOOGLE_WEB_CLIENT_ID,
248
+ });
249
+
250
+ return (
251
+ <View>
252
+ {/* Google - all platforms */}
253
+ <SocialButton
254
+ provider="google"
255
+ onPress={signInWithGoogle}
256
+ loading={googleLoading}
257
+ />
258
+
259
+ {/* Apple - iOS only */}
260
+ {Platform.OS === 'ios' && appleAvailable && (
261
+ <SocialButton
262
+ provider="apple"
263
+ onPress={signInWithApple}
264
+ loading={appleLoading}
265
+ />
266
+ )}
267
+ </View>
268
+ );
269
+ }
270
+ ```
271
+
272
+ ### Apple Sign-In with Error Handling
273
+
274
+ ```typescript
275
+ function AppleLoginButton() {
276
+ const { signInWithApple, appleLoading, appleAvailable } = useAppleAuth();
277
+
278
+ const handleAppleSignIn = async () => {
279
+ const result = await signInWithApple();
280
+
281
+ if (result.success) {
282
+ console.log('Apple sign-in successful');
283
+ } else {
284
+ // Handle error
285
+ if (result.error?.includes('cancelled')) {
286
+ console.log('User cancelled');
287
+ } else {
288
+ Alert.alert('Error', result.error || 'Apple sign-in failed');
289
+ }
290
+ }
291
+ };
292
+
293
+ if (!appleAvailable) {
294
+ return null;
295
+ }
296
+
297
+ return (
298
+ <TouchableOpacity onPress={handleAppleSignIn} disabled={appleLoading}>
299
+ <Text>Sign in with Apple</Text>
300
+ </TouchableOpacity>
301
+ );
302
+ }
303
+ ```
304
+
305
+ ## SocialAuthResult
306
+
307
+ All social login functions return the same type:
308
+
309
+ ```typescript
310
+ interface SocialAuthResult {
311
+ success: boolean;
312
+ error?: string;
313
+ user?: AuthUser;
314
+ }
315
+ ```
316
+
317
+ ## Configuration
318
+
319
+ ### Get Google Client ID
320
+
321
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com/)
322
+ 2. Create a new project or select existing
323
+ 3. Navigate to "APIs & Services" > "Credentials"
324
+ 4. Create "OAuth 2.0 Client IDs":
325
+ - **iOS**: For your iOS app
326
+ - **Android**: For your Android app
327
+ - **Web**: For Expo/web
328
+
329
+ ### Configure Apple Sign-In
330
+
331
+ 1. Go to [Apple Developer](https://developer.apple.com/)
332
+ 2. Navigate to "Certificates, Identifiers & Profiles" > "Identifiers"
333
+ 3. Select your App ID and enable "Sign In with Apple"
334
+ 4. Enable Apple Sign-In in Firebase Console
335
+
336
+ ## Important Notes
337
+
338
+ ### Google
339
+ - Requires `expo-web-browser` setup
340
+ - `WebBrowser.maybeCompleteAuthSession()` must be called in app root
341
+ - At least one client ID must be provided
342
+
343
+ ### Apple
344
+ - Only available on iOS
345
+ - Requires `expo-apple-authentication` setup
346
+ - Requires Apple Developer account
347
+ - Testing requires a physical device (may not work in simulator)
348
+
349
+ ## Related Hooks
350
+
351
+ - [`useAuth`](./useAuth.md) - Main auth state management
352
+ - [`useSocialLogin`](#usesociallogin) - General social login
353
+
354
+ ## Related Components
355
+
356
+ - [`SocialLoginButtons`](../components/SocialLoginButtons.md) - Social login button component