@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/src/Avatar.tsx
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Avatar - User avatar component for React Native
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { useState } from 'react';
|
|
6
|
+
import { View, Text, Image, Pressable, type ViewProps } from 'react-native';
|
|
7
|
+
import { cn } from '@sudobility/components-rn';
|
|
8
|
+
import type { AvatarProps } from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get initials from display name or email
|
|
12
|
+
*/
|
|
13
|
+
function getInitials(name: string | null, email: string | null): string {
|
|
14
|
+
if (name) {
|
|
15
|
+
const parts = name.trim().split(/\s+/);
|
|
16
|
+
if (parts.length >= 2) {
|
|
17
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
18
|
+
}
|
|
19
|
+
return name.slice(0, 2).toUpperCase();
|
|
20
|
+
}
|
|
21
|
+
if (email) {
|
|
22
|
+
return email.slice(0, 2).toUpperCase();
|
|
23
|
+
}
|
|
24
|
+
return '?';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Avatar component with photo URL and initials fallback
|
|
29
|
+
*/
|
|
30
|
+
export const Avatar: React.FC<AvatarProps & ViewProps> = ({
|
|
31
|
+
user,
|
|
32
|
+
size = 32,
|
|
33
|
+
className,
|
|
34
|
+
onPress,
|
|
35
|
+
...props
|
|
36
|
+
}) => {
|
|
37
|
+
const [imageError, setImageError] = useState(false);
|
|
38
|
+
|
|
39
|
+
const hasValidPhoto = user.photoURL && !imageError;
|
|
40
|
+
const initials = getInitials(user.displayName, user.email);
|
|
41
|
+
|
|
42
|
+
const sizeStyle = {
|
|
43
|
+
width: size,
|
|
44
|
+
height: size,
|
|
45
|
+
borderRadius: size / 2,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const content = hasValidPhoto ? (
|
|
49
|
+
<Image
|
|
50
|
+
source={{ uri: user.photoURL! }}
|
|
51
|
+
style={sizeStyle}
|
|
52
|
+
className='bg-gray-200 dark:bg-gray-700'
|
|
53
|
+
onError={() => setImageError(true)}
|
|
54
|
+
accessibilityLabel={user.displayName || 'User avatar'}
|
|
55
|
+
/>
|
|
56
|
+
) : (
|
|
57
|
+
<View
|
|
58
|
+
style={sizeStyle}
|
|
59
|
+
className={cn('items-center justify-center bg-blue-600', className)}
|
|
60
|
+
>
|
|
61
|
+
<Text style={{ fontSize: size * 0.4 }} className='font-medium text-white'>
|
|
62
|
+
{initials}
|
|
63
|
+
</Text>
|
|
64
|
+
</View>
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
if (onPress) {
|
|
68
|
+
return (
|
|
69
|
+
<Pressable
|
|
70
|
+
onPress={onPress}
|
|
71
|
+
className={cn('active:opacity-80', className)}
|
|
72
|
+
accessibilityRole='button'
|
|
73
|
+
accessibilityLabel={user.displayName || 'User avatar'}
|
|
74
|
+
{...props}
|
|
75
|
+
>
|
|
76
|
+
{content}
|
|
77
|
+
</Pressable>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<View className={className} {...props}>
|
|
83
|
+
{content}
|
|
84
|
+
</View>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmailSignInForm - Email sign-in form for React Native
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { useState } from 'react';
|
|
6
|
+
import { View, Text, TextInput, Pressable } from 'react-native';
|
|
7
|
+
import { cn } from '@sudobility/components-rn';
|
|
8
|
+
import type { EmailSignInFormProps } from './types';
|
|
9
|
+
import { useAuthStatus } from './AuthProvider';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Email sign-in form component
|
|
13
|
+
*/
|
|
14
|
+
export const EmailSignInForm: React.FC<EmailSignInFormProps> = ({
|
|
15
|
+
onSwitchToSignUp,
|
|
16
|
+
onSwitchToForgotPassword,
|
|
17
|
+
onSuccess,
|
|
18
|
+
onTrack,
|
|
19
|
+
trackingLabel,
|
|
20
|
+
componentName = 'EmailSignInForm',
|
|
21
|
+
}) => {
|
|
22
|
+
const { texts, signInWithEmail, loading, error, clearError } =
|
|
23
|
+
useAuthStatus();
|
|
24
|
+
const [email, setEmail] = useState('');
|
|
25
|
+
const [password, setPassword] = useState('');
|
|
26
|
+
|
|
27
|
+
const handleSubmit = async () => {
|
|
28
|
+
onTrack?.({
|
|
29
|
+
action: 'form_submit',
|
|
30
|
+
trackingLabel,
|
|
31
|
+
componentName,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
clearError();
|
|
35
|
+
await signInWithEmail(email, password);
|
|
36
|
+
onSuccess?.();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<View className='gap-4'>
|
|
41
|
+
<View className='gap-2'>
|
|
42
|
+
<Text className='text-sm font-medium text-gray-900 dark:text-white'>
|
|
43
|
+
{texts.email}
|
|
44
|
+
</Text>
|
|
45
|
+
<TextInput
|
|
46
|
+
value={email}
|
|
47
|
+
onChangeText={setEmail}
|
|
48
|
+
placeholder={texts.emailPlaceholder}
|
|
49
|
+
keyboardType='email-address'
|
|
50
|
+
autoCapitalize='none'
|
|
51
|
+
autoCorrect={false}
|
|
52
|
+
className='px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
|
|
53
|
+
placeholderTextColor='#9CA3AF'
|
|
54
|
+
/>
|
|
55
|
+
</View>
|
|
56
|
+
|
|
57
|
+
<View className='gap-2'>
|
|
58
|
+
<Text className='text-sm font-medium text-gray-900 dark:text-white'>
|
|
59
|
+
{texts.password}
|
|
60
|
+
</Text>
|
|
61
|
+
<TextInput
|
|
62
|
+
value={password}
|
|
63
|
+
onChangeText={setPassword}
|
|
64
|
+
placeholder={texts.passwordPlaceholder}
|
|
65
|
+
secureTextEntry
|
|
66
|
+
className='px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
|
|
67
|
+
placeholderTextColor='#9CA3AF'
|
|
68
|
+
/>
|
|
69
|
+
</View>
|
|
70
|
+
|
|
71
|
+
{error && (
|
|
72
|
+
<Text className='text-sm text-red-600 dark:text-red-400'>{error}</Text>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
<Pressable
|
|
76
|
+
onPress={handleSubmit}
|
|
77
|
+
disabled={loading || !email || !password}
|
|
78
|
+
className={cn(
|
|
79
|
+
'py-3 px-4 rounded-lg items-center justify-center',
|
|
80
|
+
'bg-blue-600 active:bg-blue-700',
|
|
81
|
+
(loading || !email || !password) && 'opacity-50'
|
|
82
|
+
)}
|
|
83
|
+
accessibilityRole='button'
|
|
84
|
+
accessibilityLabel={texts.signIn}
|
|
85
|
+
>
|
|
86
|
+
<Text className='font-medium text-white'>
|
|
87
|
+
{loading ? texts.loading : texts.signIn}
|
|
88
|
+
</Text>
|
|
89
|
+
</Pressable>
|
|
90
|
+
|
|
91
|
+
<Pressable
|
|
92
|
+
onPress={() => {
|
|
93
|
+
onTrack?.({
|
|
94
|
+
action: 'switch_mode',
|
|
95
|
+
trackingLabel,
|
|
96
|
+
componentName,
|
|
97
|
+
});
|
|
98
|
+
onSwitchToForgotPassword();
|
|
99
|
+
}}
|
|
100
|
+
className='items-center py-2'
|
|
101
|
+
accessibilityRole='button'
|
|
102
|
+
>
|
|
103
|
+
<Text className='text-sm text-blue-600 dark:text-blue-400'>
|
|
104
|
+
{texts.forgotPassword}
|
|
105
|
+
</Text>
|
|
106
|
+
</Pressable>
|
|
107
|
+
|
|
108
|
+
<View className='flex-row items-center justify-center gap-1'>
|
|
109
|
+
<Text className='text-sm text-gray-500 dark:text-gray-400'>
|
|
110
|
+
{texts.noAccount}
|
|
111
|
+
</Text>
|
|
112
|
+
<Pressable
|
|
113
|
+
onPress={() => {
|
|
114
|
+
onTrack?.({
|
|
115
|
+
action: 'switch_mode',
|
|
116
|
+
trackingLabel,
|
|
117
|
+
componentName,
|
|
118
|
+
});
|
|
119
|
+
onSwitchToSignUp();
|
|
120
|
+
}}
|
|
121
|
+
accessibilityRole='button'
|
|
122
|
+
>
|
|
123
|
+
<Text className='text-sm font-medium text-blue-600 dark:text-blue-400'>
|
|
124
|
+
{texts.signUp}
|
|
125
|
+
</Text>
|
|
126
|
+
</Pressable>
|
|
127
|
+
</View>
|
|
128
|
+
</View>
|
|
129
|
+
);
|
|
130
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmailSignUpForm - Email sign-up form for React Native
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { useState } from 'react';
|
|
6
|
+
import { View, Text, TextInput, Pressable } from 'react-native';
|
|
7
|
+
import { cn } from '@sudobility/components-rn';
|
|
8
|
+
import type { EmailSignUpFormProps } from './types';
|
|
9
|
+
import { useAuthStatus } from './AuthProvider';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Email sign-up form component
|
|
13
|
+
*/
|
|
14
|
+
export const EmailSignUpForm: React.FC<EmailSignUpFormProps> = ({
|
|
15
|
+
onSwitchToSignIn,
|
|
16
|
+
onSuccess,
|
|
17
|
+
onTrack,
|
|
18
|
+
trackingLabel,
|
|
19
|
+
componentName = 'EmailSignUpForm',
|
|
20
|
+
}) => {
|
|
21
|
+
const { texts, signUpWithEmail, loading, error, clearError } =
|
|
22
|
+
useAuthStatus();
|
|
23
|
+
const [email, setEmail] = useState('');
|
|
24
|
+
const [password, setPassword] = useState('');
|
|
25
|
+
const [confirmPassword, setConfirmPassword] = useState('');
|
|
26
|
+
const [displayName, setDisplayName] = useState('');
|
|
27
|
+
const [localError, setLocalError] = useState<string | null>(null);
|
|
28
|
+
|
|
29
|
+
const handleSubmit = async () => {
|
|
30
|
+
setLocalError(null);
|
|
31
|
+
clearError();
|
|
32
|
+
|
|
33
|
+
// Validate passwords match
|
|
34
|
+
if (password !== confirmPassword) {
|
|
35
|
+
setLocalError(texts.passwordMismatch);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Validate password length
|
|
40
|
+
if (password.length < 6) {
|
|
41
|
+
setLocalError(texts.passwordTooShort);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onTrack?.({
|
|
46
|
+
action: 'form_submit',
|
|
47
|
+
trackingLabel,
|
|
48
|
+
componentName,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await signUpWithEmail(email, password, displayName || undefined);
|
|
52
|
+
onSuccess?.();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const displayError = localError || error;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<View className='gap-4'>
|
|
59
|
+
<View className='gap-2'>
|
|
60
|
+
<Text className='text-sm font-medium text-gray-900 dark:text-white'>
|
|
61
|
+
{texts.displayName}
|
|
62
|
+
</Text>
|
|
63
|
+
<TextInput
|
|
64
|
+
value={displayName}
|
|
65
|
+
onChangeText={setDisplayName}
|
|
66
|
+
placeholder={texts.displayNamePlaceholder}
|
|
67
|
+
autoCapitalize='words'
|
|
68
|
+
className='px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
|
|
69
|
+
placeholderTextColor='#9CA3AF'
|
|
70
|
+
/>
|
|
71
|
+
</View>
|
|
72
|
+
|
|
73
|
+
<View className='gap-2'>
|
|
74
|
+
<Text className='text-sm font-medium text-gray-900 dark:text-white'>
|
|
75
|
+
{texts.email}
|
|
76
|
+
</Text>
|
|
77
|
+
<TextInput
|
|
78
|
+
value={email}
|
|
79
|
+
onChangeText={setEmail}
|
|
80
|
+
placeholder={texts.emailPlaceholder}
|
|
81
|
+
keyboardType='email-address'
|
|
82
|
+
autoCapitalize='none'
|
|
83
|
+
autoCorrect={false}
|
|
84
|
+
className='px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
|
|
85
|
+
placeholderTextColor='#9CA3AF'
|
|
86
|
+
/>
|
|
87
|
+
</View>
|
|
88
|
+
|
|
89
|
+
<View className='gap-2'>
|
|
90
|
+
<Text className='text-sm font-medium text-gray-900 dark:text-white'>
|
|
91
|
+
{texts.password}
|
|
92
|
+
</Text>
|
|
93
|
+
<TextInput
|
|
94
|
+
value={password}
|
|
95
|
+
onChangeText={setPassword}
|
|
96
|
+
placeholder={texts.passwordPlaceholder}
|
|
97
|
+
secureTextEntry
|
|
98
|
+
className='px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
|
|
99
|
+
placeholderTextColor='#9CA3AF'
|
|
100
|
+
/>
|
|
101
|
+
</View>
|
|
102
|
+
|
|
103
|
+
<View className='gap-2'>
|
|
104
|
+
<Text className='text-sm font-medium text-gray-900 dark:text-white'>
|
|
105
|
+
{texts.confirmPassword}
|
|
106
|
+
</Text>
|
|
107
|
+
<TextInput
|
|
108
|
+
value={confirmPassword}
|
|
109
|
+
onChangeText={setConfirmPassword}
|
|
110
|
+
placeholder={texts.confirmPasswordPlaceholder}
|
|
111
|
+
secureTextEntry
|
|
112
|
+
className='px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
|
|
113
|
+
placeholderTextColor='#9CA3AF'
|
|
114
|
+
/>
|
|
115
|
+
</View>
|
|
116
|
+
|
|
117
|
+
{displayError && (
|
|
118
|
+
<Text className='text-sm text-red-600 dark:text-red-400'>
|
|
119
|
+
{displayError}
|
|
120
|
+
</Text>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
<Pressable
|
|
124
|
+
onPress={handleSubmit}
|
|
125
|
+
disabled={loading || !email || !password || !confirmPassword}
|
|
126
|
+
className={cn(
|
|
127
|
+
'py-3 px-4 rounded-lg items-center justify-center',
|
|
128
|
+
'bg-blue-600 active:bg-blue-700',
|
|
129
|
+
(loading || !email || !password || !confirmPassword) && 'opacity-50'
|
|
130
|
+
)}
|
|
131
|
+
accessibilityRole='button'
|
|
132
|
+
accessibilityLabel={texts.signUp}
|
|
133
|
+
>
|
|
134
|
+
<Text className='font-medium text-white'>
|
|
135
|
+
{loading ? texts.loading : texts.signUp}
|
|
136
|
+
</Text>
|
|
137
|
+
</Pressable>
|
|
138
|
+
|
|
139
|
+
<View className='flex-row items-center justify-center gap-1'>
|
|
140
|
+
<Text className='text-sm text-gray-500 dark:text-gray-400'>
|
|
141
|
+
{texts.haveAccount}
|
|
142
|
+
</Text>
|
|
143
|
+
<Pressable
|
|
144
|
+
onPress={() => {
|
|
145
|
+
onTrack?.({
|
|
146
|
+
action: 'switch_mode',
|
|
147
|
+
trackingLabel,
|
|
148
|
+
componentName,
|
|
149
|
+
});
|
|
150
|
+
onSwitchToSignIn();
|
|
151
|
+
}}
|
|
152
|
+
accessibilityRole='button'
|
|
153
|
+
>
|
|
154
|
+
<Text className='text-sm font-medium text-blue-600 dark:text-blue-400'>
|
|
155
|
+
{texts.signIn}
|
|
156
|
+
</Text>
|
|
157
|
+
</Pressable>
|
|
158
|
+
</View>
|
|
159
|
+
</View>
|
|
160
|
+
);
|
|
161
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ForgotPasswordForm - Password reset form for React Native
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { useState } from 'react';
|
|
6
|
+
import { View, Text, TextInput, Pressable } from 'react-native';
|
|
7
|
+
import { cn } from '@sudobility/components-rn';
|
|
8
|
+
import type { ForgotPasswordFormProps } from './types';
|
|
9
|
+
import { useAuthStatus } from './AuthProvider';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Forgot password form component
|
|
13
|
+
*/
|
|
14
|
+
export const ForgotPasswordForm: React.FC<ForgotPasswordFormProps> = ({
|
|
15
|
+
onSwitchToSignIn,
|
|
16
|
+
onTrack,
|
|
17
|
+
trackingLabel,
|
|
18
|
+
componentName = 'ForgotPasswordForm',
|
|
19
|
+
}) => {
|
|
20
|
+
const { texts, resetPassword, loading, error, clearError } = useAuthStatus();
|
|
21
|
+
const [email, setEmail] = useState('');
|
|
22
|
+
const [success, setSuccess] = useState(false);
|
|
23
|
+
|
|
24
|
+
const handleSubmit = async () => {
|
|
25
|
+
clearError();
|
|
26
|
+
setSuccess(false);
|
|
27
|
+
|
|
28
|
+
onTrack?.({
|
|
29
|
+
action: 'form_submit',
|
|
30
|
+
trackingLabel,
|
|
31
|
+
componentName,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await resetPassword(email);
|
|
35
|
+
setSuccess(true);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (success) {
|
|
39
|
+
return (
|
|
40
|
+
<View className='gap-4'>
|
|
41
|
+
<View className='p-4 rounded-lg bg-green-50 dark:bg-green-900/20'>
|
|
42
|
+
<Text className='text-base font-medium text-green-800 dark:text-green-200'>
|
|
43
|
+
{texts.resetEmailSent}
|
|
44
|
+
</Text>
|
|
45
|
+
<Text className='mt-1 text-sm text-green-700 dark:text-green-300'>
|
|
46
|
+
{texts.resetEmailSentDesc.replace('{{email}}', email)}
|
|
47
|
+
</Text>
|
|
48
|
+
</View>
|
|
49
|
+
|
|
50
|
+
<Pressable
|
|
51
|
+
onPress={() => {
|
|
52
|
+
onTrack?.({
|
|
53
|
+
action: 'switch_mode',
|
|
54
|
+
trackingLabel,
|
|
55
|
+
componentName,
|
|
56
|
+
});
|
|
57
|
+
onSwitchToSignIn();
|
|
58
|
+
}}
|
|
59
|
+
className='py-3 px-4 rounded-lg items-center justify-center bg-blue-600 active:bg-blue-700'
|
|
60
|
+
accessibilityRole='button'
|
|
61
|
+
>
|
|
62
|
+
<Text className='font-medium text-white'>{texts.backToSignIn}</Text>
|
|
63
|
+
</Pressable>
|
|
64
|
+
</View>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<View className='gap-4'>
|
|
70
|
+
<Text className='text-sm text-gray-500 dark:text-gray-400'>
|
|
71
|
+
Enter your email address and we'll send you a link to reset your
|
|
72
|
+
password.
|
|
73
|
+
</Text>
|
|
74
|
+
|
|
75
|
+
<View className='gap-2'>
|
|
76
|
+
<Text className='text-sm font-medium text-gray-900 dark:text-white'>
|
|
77
|
+
{texts.email}
|
|
78
|
+
</Text>
|
|
79
|
+
<TextInput
|
|
80
|
+
value={email}
|
|
81
|
+
onChangeText={setEmail}
|
|
82
|
+
placeholder={texts.emailPlaceholder}
|
|
83
|
+
keyboardType='email-address'
|
|
84
|
+
autoCapitalize='none'
|
|
85
|
+
autoCorrect={false}
|
|
86
|
+
className='px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
|
|
87
|
+
placeholderTextColor='#9CA3AF'
|
|
88
|
+
/>
|
|
89
|
+
</View>
|
|
90
|
+
|
|
91
|
+
{error && (
|
|
92
|
+
<Text className='text-sm text-red-600 dark:text-red-400'>{error}</Text>
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
<Pressable
|
|
96
|
+
onPress={handleSubmit}
|
|
97
|
+
disabled={loading || !email}
|
|
98
|
+
className={cn(
|
|
99
|
+
'py-3 px-4 rounded-lg items-center justify-center',
|
|
100
|
+
'bg-blue-600 active:bg-blue-700',
|
|
101
|
+
(loading || !email) && 'opacity-50'
|
|
102
|
+
)}
|
|
103
|
+
accessibilityRole='button'
|
|
104
|
+
accessibilityLabel={texts.sendResetLink}
|
|
105
|
+
>
|
|
106
|
+
<Text className='font-medium text-white'>
|
|
107
|
+
{loading ? texts.loading : texts.sendResetLink}
|
|
108
|
+
</Text>
|
|
109
|
+
</Pressable>
|
|
110
|
+
|
|
111
|
+
<Pressable
|
|
112
|
+
onPress={() => {
|
|
113
|
+
onTrack?.({
|
|
114
|
+
action: 'switch_mode',
|
|
115
|
+
trackingLabel,
|
|
116
|
+
componentName,
|
|
117
|
+
});
|
|
118
|
+
onSwitchToSignIn();
|
|
119
|
+
}}
|
|
120
|
+
className='items-center py-2'
|
|
121
|
+
accessibilityRole='button'
|
|
122
|
+
>
|
|
123
|
+
<Text className='text-sm text-blue-600 dark:text-blue-400'>
|
|
124
|
+
{texts.backToSignIn}
|
|
125
|
+
</Text>
|
|
126
|
+
</Pressable>
|
|
127
|
+
</View>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProviderButtons - OAuth provider buttons for React Native
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { View, Text, Pressable } from 'react-native';
|
|
7
|
+
import { cn } from '@sudobility/components-rn';
|
|
8
|
+
import type { ProviderButtonsProps, AuthProviderType } from './types';
|
|
9
|
+
import { useAuthStatus } from './AuthProvider';
|
|
10
|
+
|
|
11
|
+
interface ProviderButtonProps {
|
|
12
|
+
provider: AuthProviderType;
|
|
13
|
+
label: string;
|
|
14
|
+
onPress: () => void;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ProviderButton: React.FC<ProviderButtonProps> = ({
|
|
19
|
+
provider,
|
|
20
|
+
label,
|
|
21
|
+
onPress,
|
|
22
|
+
className,
|
|
23
|
+
}) => {
|
|
24
|
+
const bgColors: Record<AuthProviderType, string> = {
|
|
25
|
+
google: 'bg-white border border-gray-300',
|
|
26
|
+
apple: 'bg-black',
|
|
27
|
+
email: 'bg-blue-600',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const textColors: Record<AuthProviderType, string> = {
|
|
31
|
+
google: 'text-gray-900',
|
|
32
|
+
apple: 'text-white',
|
|
33
|
+
email: 'text-white',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Pressable
|
|
38
|
+
onPress={onPress}
|
|
39
|
+
className={cn(
|
|
40
|
+
'flex-row items-center justify-center py-3 px-4 rounded-lg',
|
|
41
|
+
'active:opacity-80',
|
|
42
|
+
bgColors[provider],
|
|
43
|
+
className
|
|
44
|
+
)}
|
|
45
|
+
accessibilityRole='button'
|
|
46
|
+
accessibilityLabel={label}
|
|
47
|
+
>
|
|
48
|
+
<Text className={cn('font-medium text-base', textColors[provider])}>
|
|
49
|
+
{label}
|
|
50
|
+
</Text>
|
|
51
|
+
</Pressable>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Provider buttons component for OAuth and email sign-in
|
|
57
|
+
*/
|
|
58
|
+
export const ProviderButtons: React.FC<ProviderButtonsProps> = ({
|
|
59
|
+
providers,
|
|
60
|
+
onEmailPress,
|
|
61
|
+
onTrack,
|
|
62
|
+
trackingLabel,
|
|
63
|
+
componentName = 'ProviderButtons',
|
|
64
|
+
}) => {
|
|
65
|
+
const { texts, signInWithGoogle, signInWithApple } = useAuthStatus();
|
|
66
|
+
|
|
67
|
+
const handleProviderPress = async (provider: AuthProviderType) => {
|
|
68
|
+
onTrack?.({
|
|
69
|
+
action: 'provider_press',
|
|
70
|
+
trackingLabel,
|
|
71
|
+
componentName,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (provider === 'google') {
|
|
75
|
+
await signInWithGoogle();
|
|
76
|
+
} else if (provider === 'apple') {
|
|
77
|
+
await signInWithApple();
|
|
78
|
+
} else if (provider === 'email') {
|
|
79
|
+
onEmailPress();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const getLabel = (provider: AuthProviderType): string => {
|
|
84
|
+
switch (provider) {
|
|
85
|
+
case 'google':
|
|
86
|
+
return texts.continueWithGoogle;
|
|
87
|
+
case 'apple':
|
|
88
|
+
return texts.continueWithApple;
|
|
89
|
+
case 'email':
|
|
90
|
+
return texts.continueWithEmail;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<View className='gap-3'>
|
|
96
|
+
{providers.map(provider => (
|
|
97
|
+
<ProviderButton
|
|
98
|
+
key={provider}
|
|
99
|
+
provider={provider}
|
|
100
|
+
label={getLabel(provider)}
|
|
101
|
+
onPress={() => handleProviderPress(provider)}
|
|
102
|
+
/>
|
|
103
|
+
))}
|
|
104
|
+
</View>
|
|
105
|
+
);
|
|
106
|
+
};
|