@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.
- package/dist/AuthAction.d.ts +7 -0
- package/dist/AuthAction.d.ts.map +1 -0
- package/dist/AuthInline.d.ts +7 -0
- package/dist/AuthInline.d.ts.map +1 -0
- package/dist/AuthProvider.d.ts +15 -0
- package/dist/AuthProvider.d.ts.map +1 -0
- package/dist/AuthScreen.d.ts +7 -0
- package/dist/AuthScreen.d.ts.map +1 -0
- package/dist/Avatar.d.ts +8 -0
- package/dist/Avatar.d.ts.map +1 -0
- package/dist/EmailSignInForm.d.ts +7 -0
- package/dist/EmailSignInForm.d.ts.map +1 -0
- package/dist/EmailSignUpForm.d.ts +7 -0
- package/dist/EmailSignUpForm.d.ts.map +1 -0
- package/dist/ForgotPasswordForm.d.ts +7 -0
- package/dist/ForgotPasswordForm.d.ts.map +1 -0
- package/dist/ProviderButtons.d.ts +7 -0
- package/dist/ProviderButtons.d.ts.map +1 -0
- package/dist/index.cjs.js +1850 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +1850 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/types.d.ts +313 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +64 -0
- package/src/AuthAction.tsx +107 -0
- package/src/AuthInline.tsx +124 -0
- package/src/AuthProvider.tsx +276 -0
- package/src/AuthScreen.tsx +117 -0
- package/src/Avatar.tsx +86 -0
- package/src/EmailSignInForm.tsx +130 -0
- package/src/EmailSignUpForm.tsx +161 -0
- package/src/ForgotPasswordForm.tsx +129 -0
- package/src/ProviderButtons.tsx +106 -0
- package/src/__tests__/AuthAction.test.tsx +261 -0
- package/src/__tests__/AuthProvider.test.tsx +459 -0
- package/src/__tests__/Avatar.test.tsx +165 -0
- package/src/index.ts +52 -0
- package/src/nativewind.d.ts +30 -0
- 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
|
+
};
|