@sudobility/auth-components-rn 1.0.1

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.
Files changed (42) hide show
  1. package/dist/AuthAction.d.ts +7 -0
  2. package/dist/AuthAction.d.ts.map +1 -0
  3. package/dist/AuthInline.d.ts +7 -0
  4. package/dist/AuthInline.d.ts.map +1 -0
  5. package/dist/AuthProvider.d.ts +15 -0
  6. package/dist/AuthProvider.d.ts.map +1 -0
  7. package/dist/AuthScreen.d.ts +7 -0
  8. package/dist/AuthScreen.d.ts.map +1 -0
  9. package/dist/Avatar.d.ts +8 -0
  10. package/dist/Avatar.d.ts.map +1 -0
  11. package/dist/EmailSignInForm.d.ts +7 -0
  12. package/dist/EmailSignInForm.d.ts.map +1 -0
  13. package/dist/EmailSignUpForm.d.ts +7 -0
  14. package/dist/EmailSignUpForm.d.ts.map +1 -0
  15. package/dist/ForgotPasswordForm.d.ts +7 -0
  16. package/dist/ForgotPasswordForm.d.ts.map +1 -0
  17. package/dist/ProviderButtons.d.ts +7 -0
  18. package/dist/ProviderButtons.d.ts.map +1 -0
  19. package/dist/index.cjs.js +1850 -0
  20. package/dist/index.cjs.js.map +1 -0
  21. package/dist/index.d.ts +15 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.esm.js +1850 -0
  24. package/dist/index.esm.js.map +1 -0
  25. package/dist/types.d.ts +313 -0
  26. package/dist/types.d.ts.map +1 -0
  27. package/package.json +64 -0
  28. package/src/AuthAction.tsx +107 -0
  29. package/src/AuthInline.tsx +124 -0
  30. package/src/AuthProvider.tsx +276 -0
  31. package/src/AuthScreen.tsx +117 -0
  32. package/src/Avatar.tsx +86 -0
  33. package/src/EmailSignInForm.tsx +130 -0
  34. package/src/EmailSignUpForm.tsx +161 -0
  35. package/src/ForgotPasswordForm.tsx +129 -0
  36. package/src/ProviderButtons.tsx +106 -0
  37. package/src/__tests__/AuthAction.test.tsx +261 -0
  38. package/src/__tests__/AuthProvider.test.tsx +459 -0
  39. package/src/__tests__/Avatar.test.tsx +165 -0
  40. package/src/index.ts +52 -0
  41. package/src/nativewind.d.ts +30 -0
  42. package/src/types.ts +383 -0
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@sudobility/auth-components-rn",
3
+ "version": "1.0.1",
4
+ "description": "React Native Authentication components for Sudobility with Firebase Auth support",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.esm.js",
7
+ "types": "dist/index.d.ts",
8
+ "react-native": "src/index.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "react-native": "./src/index.ts",
13
+ "import": "./dist/index.esm.js",
14
+ "require": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc && vite build",
23
+ "dev": "vite build --watch",
24
+ "type-check": "tsc --noEmit",
25
+ "test": "jest --passWithNoTests"
26
+ },
27
+ "peerDependencies": {
28
+ "@react-native-firebase/auth": ">=18.0.0",
29
+ "@sudobility/components-rn": "^1.0.0",
30
+ "@sudobility/design": "^1.1.0",
31
+ "react": "^18.0.0 || ^19.0.0",
32
+ "react-native": ">=0.72.0"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "@react-native-firebase/auth": {
36
+ "optional": true
37
+ }
38
+ },
39
+ "devDependencies": {
40
+ "@sudobility/components-rn": "^1.0.7",
41
+ "@sudobility/design": "^1.1.14",
42
+ "@types/react": "^18.3.0",
43
+ "@types/react-native": "^0.73.0",
44
+ "react": "^18.3.0",
45
+ "react-native": "^0.76.0",
46
+ "typescript": "^5.6.0",
47
+ "vite": "^6.0.0",
48
+ "vite-plugin-dts": "^4.3.0"
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "https://github.com/sudobility/components-rn.git",
53
+ "directory": "packages/auth-components-rn"
54
+ },
55
+ "keywords": [
56
+ "react-native",
57
+ "authentication",
58
+ "firebase",
59
+ "auth",
60
+ "components"
61
+ ],
62
+ "author": "Sudobility",
63
+ "license": "BUSL-1.1"
64
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * AuthAction - Authentication action component for headers in React Native
3
+ */
4
+
5
+ import React from 'react';
6
+ import { View, Text, Pressable } from 'react-native';
7
+ import { cn, Button } from '@sudobility/components-rn';
8
+ import type { AuthActionProps } from './types';
9
+ import { useAuthStatus } from './AuthProvider';
10
+ import { Avatar } from './Avatar';
11
+
12
+ /**
13
+ * Authentication action component for app headers
14
+ */
15
+ export const AuthAction: React.FC<AuthActionProps> = ({
16
+ className,
17
+ loginButtonVariant = 'primary',
18
+ size = 'md',
19
+ loginButtonContent,
20
+ avatarSize = 32,
21
+ menuItems: _menuItems = [],
22
+ showUserInfo = true,
23
+ renderUserInfo: _renderUserInfo,
24
+ renderAvatar,
25
+ onLoginPress,
26
+ onLogoutPress,
27
+ onTrack,
28
+ trackingLabel,
29
+ componentName = 'AuthAction',
30
+ }) => {
31
+ const { user, isAuthenticated, texts, signOut, loading } = useAuthStatus();
32
+
33
+ const handleLoginPress = () => {
34
+ onTrack?.({
35
+ action: 'login_press',
36
+ trackingLabel,
37
+ componentName,
38
+ });
39
+
40
+ const result = onLoginPress?.();
41
+ // If onLoginPress returns false, don't proceed with default behavior
42
+ if (result === false) return;
43
+ };
44
+
45
+ const handleLogoutPress = async () => {
46
+ onTrack?.({
47
+ action: 'logout_press',
48
+ trackingLabel,
49
+ componentName,
50
+ });
51
+
52
+ onLogoutPress?.();
53
+ await signOut();
54
+ };
55
+
56
+ // Not authenticated - show login button
57
+ if (!isAuthenticated || !user) {
58
+ return (
59
+ <Button
60
+ variant={loginButtonVariant}
61
+ size={size === 'md' ? 'default' : size}
62
+ onPress={handleLoginPress}
63
+ disabled={loading}
64
+ className={className}
65
+ >
66
+ {loginButtonContent || texts.login}
67
+ </Button>
68
+ );
69
+ }
70
+
71
+ // Authenticated - show avatar and user info
72
+ // For React Native, we show a simplified version without dropdown
73
+ // A full dropdown menu would require a modal or action sheet
74
+ return (
75
+ <View className={cn('flex-row items-center gap-3', className)}>
76
+ {showUserInfo && (
77
+ <View className='items-end'>
78
+ <Text className='text-sm font-medium text-gray-900 dark:text-white'>
79
+ {user.displayName || user.email}
80
+ </Text>
81
+ {user.displayName && user.email && (
82
+ <Text className='text-xs text-gray-500 dark:text-gray-400'>
83
+ {user.email}
84
+ </Text>
85
+ )}
86
+ </View>
87
+ )}
88
+
89
+ {renderAvatar ? (
90
+ renderAvatar(user)
91
+ ) : (
92
+ <Avatar user={user} size={avatarSize} />
93
+ )}
94
+
95
+ <Pressable
96
+ onPress={handleLogoutPress}
97
+ className='px-3 py-1.5 rounded-md bg-gray-100 dark:bg-gray-800 active:opacity-80'
98
+ accessibilityRole='button'
99
+ accessibilityLabel={texts.logout}
100
+ >
101
+ <Text className='text-sm text-gray-700 dark:text-gray-300'>
102
+ {texts.logout}
103
+ </Text>
104
+ </Pressable>
105
+ </View>
106
+ );
107
+ };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * AuthInline - Inline authentication component for React Native
3
+ */
4
+
5
+ import React, { useState } from 'react';
6
+ import { View, Text } from 'react-native';
7
+ import { Card } from '@sudobility/components-rn';
8
+ import type { AuthInlineProps, AuthMode } from './types';
9
+ import { useAuthStatus } from './AuthProvider';
10
+ import { ProviderButtons } from './ProviderButtons';
11
+ import { EmailSignInForm } from './EmailSignInForm';
12
+ import { EmailSignUpForm } from './EmailSignUpForm';
13
+ import { ForgotPasswordForm } from './ForgotPasswordForm';
14
+
15
+ /**
16
+ * Inline authentication component
17
+ */
18
+ export const AuthInline: React.FC<AuthInlineProps> = ({
19
+ initialMode = 'select',
20
+ className,
21
+ providers,
22
+ showTitle = true,
23
+ title,
24
+ onModeChange,
25
+ onSuccess,
26
+ variant = 'card',
27
+ onTrack,
28
+ trackingLabel,
29
+ componentName = 'AuthInline',
30
+ }) => {
31
+ const { texts, providerConfig } = useAuthStatus();
32
+ const [mode, setMode] = useState<AuthMode>(initialMode);
33
+
34
+ const activeProviders = providers || providerConfig.providers;
35
+
36
+ const handleModeChange = (newMode: AuthMode) => {
37
+ setMode(newMode);
38
+ onModeChange?.(newMode);
39
+ };
40
+
41
+ const getTitle = (): string => {
42
+ if (title) return title;
43
+ switch (mode) {
44
+ case 'select':
45
+ return texts.signInTitle;
46
+ case 'email-signin':
47
+ return texts.signInWithEmail;
48
+ case 'email-signup':
49
+ return texts.createAccount;
50
+ case 'forgot-password':
51
+ return texts.resetPassword;
52
+ }
53
+ };
54
+
55
+ const renderContent = () => {
56
+ switch (mode) {
57
+ case 'select':
58
+ return (
59
+ <ProviderButtons
60
+ providers={activeProviders}
61
+ onEmailPress={() => handleModeChange('email-signin')}
62
+ onTrack={onTrack}
63
+ trackingLabel={trackingLabel}
64
+ componentName={componentName}
65
+ />
66
+ );
67
+ case 'email-signin':
68
+ return (
69
+ <EmailSignInForm
70
+ onSwitchToSignUp={() => handleModeChange('email-signup')}
71
+ onSwitchToForgotPassword={() => handleModeChange('forgot-password')}
72
+ onSuccess={onSuccess}
73
+ onTrack={onTrack}
74
+ trackingLabel={trackingLabel}
75
+ componentName={componentName}
76
+ />
77
+ );
78
+ case 'email-signup':
79
+ return (
80
+ <EmailSignUpForm
81
+ onSwitchToSignIn={() => handleModeChange('email-signin')}
82
+ onSuccess={onSuccess}
83
+ onTrack={onTrack}
84
+ trackingLabel={trackingLabel}
85
+ componentName={componentName}
86
+ />
87
+ );
88
+ case 'forgot-password':
89
+ return (
90
+ <ForgotPasswordForm
91
+ onSwitchToSignIn={() => handleModeChange('email-signin')}
92
+ onTrack={onTrack}
93
+ trackingLabel={trackingLabel}
94
+ componentName={componentName}
95
+ />
96
+ );
97
+ }
98
+ };
99
+
100
+ const content = (
101
+ <View className='gap-4'>
102
+ {showTitle && (
103
+ <Text className='text-xl font-semibold text-gray-900 dark:text-white text-center'>
104
+ {getTitle()}
105
+ </Text>
106
+ )}
107
+ {renderContent()}
108
+ </View>
109
+ );
110
+
111
+ if (variant === 'flat') {
112
+ return <View className={className}>{content}</View>;
113
+ }
114
+
115
+ return (
116
+ <Card
117
+ variant={variant === 'bordered' ? 'bordered' : 'default'}
118
+ padding='lg'
119
+ className={className}
120
+ >
121
+ {content}
122
+ </Card>
123
+ );
124
+ };
@@ -0,0 +1,276 @@
1
+ /**
2
+ * AuthProvider - Context provider for authentication state in React Native
3
+ */
4
+
5
+ import React, {
6
+ createContext,
7
+ useContext,
8
+ useState,
9
+ useEffect,
10
+ useCallback,
11
+ useMemo,
12
+ } from 'react';
13
+ import type {
14
+ AuthUser,
15
+ AuthErrorTexts,
16
+ AuthContextValue,
17
+ AuthProviderProps,
18
+ } from './types';
19
+
20
+ const AuthContext = createContext<AuthContextValue | null>(null);
21
+
22
+ /**
23
+ * Default English error texts
24
+ */
25
+ export function createDefaultErrorTexts(): AuthErrorTexts {
26
+ return {
27
+ 'auth/user-not-found': 'No account found with this email.',
28
+ 'auth/wrong-password': 'Incorrect password.',
29
+ 'auth/invalid-email': 'Please enter a valid email address.',
30
+ 'auth/invalid-credential': 'Invalid email or password.',
31
+ 'auth/email-already-in-use': 'An account already exists with this email.',
32
+ 'auth/weak-password': 'Password should be at least 6 characters.',
33
+ 'auth/too-many-requests': 'Too many attempts. Please try again later.',
34
+ 'auth/network-request-failed':
35
+ 'Network error. Please check your connection.',
36
+ 'auth/popup-closed-by-user': 'Sign-in was cancelled.',
37
+ 'auth/popup-blocked': 'Sign-in popup was blocked.',
38
+ 'auth/account-exists-with-different-credential':
39
+ 'An account already exists with a different sign-in method.',
40
+ 'auth/operation-not-allowed': 'This sign-in method is not enabled.',
41
+ default: 'An error occurred. Please try again.',
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Hook to access auth context
47
+ */
48
+ export function useAuthStatus(): AuthContextValue {
49
+ const context = useContext(AuthContext);
50
+ if (!context) {
51
+ throw new Error('useAuthStatus must be used within an AuthProvider');
52
+ }
53
+ return context;
54
+ }
55
+
56
+ /**
57
+ * Auth provider component for React Native
58
+ */
59
+ export function AuthProvider({
60
+ children,
61
+ providerConfig,
62
+ texts,
63
+ errorTexts,
64
+ callbacks,
65
+ resolveErrorMessage,
66
+ }: AuthProviderProps): React.ReactElement {
67
+ const [user, setUser] = useState<AuthUser | null>(null);
68
+ const [loading, setLoading] = useState(true);
69
+ const [error, setError] = useState<string | null>(null);
70
+
71
+ // Resolve error message from code
72
+ const getErrorMessage = useCallback(
73
+ (code: string): string => {
74
+ if (resolveErrorMessage) {
75
+ return resolveErrorMessage(code);
76
+ }
77
+ const key = code as keyof AuthErrorTexts;
78
+ return errorTexts[key] || errorTexts.default;
79
+ },
80
+ [errorTexts, resolveErrorMessage]
81
+ );
82
+
83
+ // Handle auth errors
84
+ const handleError = useCallback(
85
+ (err: Error & { code?: string }) => {
86
+ const code = err.code || 'default';
87
+ const message = getErrorMessage(code);
88
+ setError(message);
89
+ callbacks?.onError?.(err, code);
90
+ },
91
+ [getErrorMessage, callbacks]
92
+ );
93
+
94
+ // Auth state listener - to be connected to Firebase Auth
95
+ useEffect(() => {
96
+ // This is a placeholder - in actual implementation,
97
+ // you would connect to @react-native-firebase/auth here
98
+ // Example:
99
+ // import auth from '@react-native-firebase/auth';
100
+ // const unsubscribe = auth().onAuthStateChanged((firebaseUser) => {
101
+ // if (firebaseUser) {
102
+ // setUser({
103
+ // uid: firebaseUser.uid,
104
+ // email: firebaseUser.email,
105
+ // displayName: firebaseUser.displayName,
106
+ // photoURL: firebaseUser.photoURL,
107
+ // isAnonymous: firebaseUser.isAnonymous,
108
+ // emailVerified: firebaseUser.emailVerified,
109
+ // providerId: firebaseUser.providerData[0]?.providerId || null,
110
+ // });
111
+ // } else {
112
+ // setUser(null);
113
+ // }
114
+ // setLoading(false);
115
+ // });
116
+ // return unsubscribe;
117
+
118
+ setLoading(false);
119
+ }, []);
120
+
121
+ // Sign in with Google
122
+ const signInWithGoogle = useCallback(async () => {
123
+ setLoading(true);
124
+ setError(null);
125
+ try {
126
+ // Placeholder - implement with @react-native-firebase/auth
127
+ // and @react-native-google-signin/google-signin
128
+ throw new Error('Google sign-in not implemented');
129
+ } catch (err) {
130
+ handleError(err as Error & { code?: string });
131
+ } finally {
132
+ setLoading(false);
133
+ }
134
+ }, [handleError]);
135
+
136
+ // Sign in with Apple
137
+ const signInWithApple = useCallback(async () => {
138
+ setLoading(true);
139
+ setError(null);
140
+ try {
141
+ // Placeholder - implement with @react-native-firebase/auth
142
+ // and @invertase/react-native-apple-authentication
143
+ throw new Error('Apple sign-in not implemented');
144
+ } catch (err) {
145
+ handleError(err as Error & { code?: string });
146
+ } finally {
147
+ setLoading(false);
148
+ }
149
+ }, [handleError]);
150
+
151
+ // Sign in with email/password
152
+ const signInWithEmail = useCallback(
153
+ async (_email: string, _password: string) => {
154
+ setLoading(true);
155
+ setError(null);
156
+ try {
157
+ // Placeholder - implement with @react-native-firebase/auth
158
+ // import auth from '@react-native-firebase/auth';
159
+ // await auth().signInWithEmailAndPassword(_email, _password);
160
+ throw new Error('Email sign-in not implemented');
161
+ } catch (err) {
162
+ handleError(err as Error & { code?: string });
163
+ } finally {
164
+ setLoading(false);
165
+ }
166
+ },
167
+ [handleError]
168
+ );
169
+
170
+ // Sign up with email/password
171
+ const signUpWithEmail = useCallback(
172
+ async (_email: string, _password: string, _displayName?: string) => {
173
+ setLoading(true);
174
+ setError(null);
175
+ try {
176
+ // Placeholder - implement with @react-native-firebase/auth
177
+ throw new Error('Email sign-up not implemented');
178
+ } catch (err) {
179
+ handleError(err as Error & { code?: string });
180
+ } finally {
181
+ setLoading(false);
182
+ }
183
+ },
184
+ [handleError]
185
+ );
186
+
187
+ // Reset password
188
+ const resetPassword = useCallback(
189
+ async (_email: string) => {
190
+ setLoading(true);
191
+ setError(null);
192
+ try {
193
+ // Placeholder - implement with @react-native-firebase/auth
194
+ throw new Error('Password reset not implemented');
195
+ } catch (err) {
196
+ handleError(err as Error & { code?: string });
197
+ } finally {
198
+ setLoading(false);
199
+ }
200
+ },
201
+ [handleError]
202
+ );
203
+
204
+ // Sign out
205
+ const signOut = useCallback(async () => {
206
+ setLoading(true);
207
+ setError(null);
208
+ try {
209
+ // Placeholder - implement with @react-native-firebase/auth
210
+ // import auth from '@react-native-firebase/auth';
211
+ // await auth().signOut();
212
+ setUser(null);
213
+ callbacks?.onSignOut?.();
214
+ } catch (err) {
215
+ handleError(err as Error & { code?: string });
216
+ } finally {
217
+ setLoading(false);
218
+ }
219
+ }, [handleError, callbacks]);
220
+
221
+ // Sign in anonymously
222
+ const signInAnonymously = useCallback(async () => {
223
+ setLoading(true);
224
+ setError(null);
225
+ try {
226
+ // Placeholder - implement with @react-native-firebase/auth
227
+ throw new Error('Anonymous sign-in not implemented');
228
+ } catch (err) {
229
+ handleError(err as Error & { code?: string });
230
+ } finally {
231
+ setLoading(false);
232
+ }
233
+ }, [handleError]);
234
+
235
+ // Clear error
236
+ const clearError = useCallback(() => {
237
+ setError(null);
238
+ }, []);
239
+
240
+ const value = useMemo<AuthContextValue>(
241
+ () => ({
242
+ user,
243
+ loading,
244
+ error,
245
+ isAuthenticated: !!user && !user.isAnonymous,
246
+ isAnonymous: user?.isAnonymous ?? false,
247
+ signInWithGoogle,
248
+ signInWithApple,
249
+ signInWithEmail,
250
+ signUpWithEmail,
251
+ resetPassword,
252
+ signOut,
253
+ signInAnonymously,
254
+ clearError,
255
+ texts,
256
+ providerConfig,
257
+ }),
258
+ [
259
+ user,
260
+ loading,
261
+ error,
262
+ signInWithGoogle,
263
+ signInWithApple,
264
+ signInWithEmail,
265
+ signUpWithEmail,
266
+ resetPassword,
267
+ signOut,
268
+ signInAnonymously,
269
+ clearError,
270
+ texts,
271
+ providerConfig,
272
+ ]
273
+ );
274
+
275
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
276
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * AuthScreen - Full-screen authentication component for React Native
3
+ */
4
+
5
+ import React, { useState } from 'react';
6
+ import { View, Text, ScrollView, SafeAreaView } from 'react-native';
7
+ import { cn } from '@sudobility/components-rn';
8
+ import type { AuthScreenProps, AuthMode } from './types';
9
+ import { useAuthStatus } from './AuthProvider';
10
+ import { ProviderButtons } from './ProviderButtons';
11
+ import { EmailSignInForm } from './EmailSignInForm';
12
+ import { EmailSignUpForm } from './EmailSignUpForm';
13
+ import { ForgotPasswordForm } from './ForgotPasswordForm';
14
+
15
+ /**
16
+ * Full-screen authentication component
17
+ */
18
+ export const AuthScreen: React.FC<AuthScreenProps> = ({
19
+ initialMode = 'select',
20
+ className,
21
+ providers,
22
+ showTitle = true,
23
+ title,
24
+ onModeChange,
25
+ onSuccess,
26
+ onTrack,
27
+ trackingLabel,
28
+ componentName = 'AuthScreen',
29
+ }) => {
30
+ const { texts, providerConfig } = useAuthStatus();
31
+ const [mode, setMode] = useState<AuthMode>(initialMode);
32
+
33
+ const activeProviders = providers || providerConfig.providers;
34
+
35
+ const handleModeChange = (newMode: AuthMode) => {
36
+ setMode(newMode);
37
+ onModeChange?.(newMode);
38
+ };
39
+
40
+ const getTitle = (): string => {
41
+ if (title) return title;
42
+ switch (mode) {
43
+ case 'select':
44
+ return texts.signInTitle;
45
+ case 'email-signin':
46
+ return texts.signInWithEmail;
47
+ case 'email-signup':
48
+ return texts.createAccount;
49
+ case 'forgot-password':
50
+ return texts.resetPassword;
51
+ }
52
+ };
53
+
54
+ const renderContent = () => {
55
+ switch (mode) {
56
+ case 'select':
57
+ return (
58
+ <ProviderButtons
59
+ providers={activeProviders}
60
+ onEmailPress={() => handleModeChange('email-signin')}
61
+ onTrack={onTrack}
62
+ trackingLabel={trackingLabel}
63
+ componentName={componentName}
64
+ />
65
+ );
66
+ case 'email-signin':
67
+ return (
68
+ <EmailSignInForm
69
+ onSwitchToSignUp={() => handleModeChange('email-signup')}
70
+ onSwitchToForgotPassword={() => handleModeChange('forgot-password')}
71
+ onSuccess={onSuccess}
72
+ onTrack={onTrack}
73
+ trackingLabel={trackingLabel}
74
+ componentName={componentName}
75
+ />
76
+ );
77
+ case 'email-signup':
78
+ return (
79
+ <EmailSignUpForm
80
+ onSwitchToSignIn={() => handleModeChange('email-signin')}
81
+ onSuccess={onSuccess}
82
+ onTrack={onTrack}
83
+ trackingLabel={trackingLabel}
84
+ componentName={componentName}
85
+ />
86
+ );
87
+ case 'forgot-password':
88
+ return (
89
+ <ForgotPasswordForm
90
+ onSwitchToSignIn={() => handleModeChange('email-signin')}
91
+ onTrack={onTrack}
92
+ trackingLabel={trackingLabel}
93
+ componentName={componentName}
94
+ />
95
+ );
96
+ }
97
+ };
98
+
99
+ return (
100
+ <SafeAreaView className={cn('flex-1 bg-white dark:bg-gray-900', className)}>
101
+ <ScrollView
102
+ className='flex-1'
103
+ contentContainerClassName='flex-grow justify-center px-6 py-8'
104
+ keyboardShouldPersistTaps='handled'
105
+ >
106
+ <View className='max-w-md w-full self-center gap-8'>
107
+ {showTitle && (
108
+ <Text className='text-2xl font-bold text-gray-900 dark:text-white text-center'>
109
+ {getTitle()}
110
+ </Text>
111
+ )}
112
+ {renderContent()}
113
+ </View>
114
+ </ScrollView>
115
+ </SafeAreaView>
116
+ );
117
+ };