@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/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
+ };