rn-swiftauth-sdk 1.0.0
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/README.md +574 -0
- package/dist/components/AuthScreen.d.ts +14 -0
- package/dist/components/AuthScreen.js +75 -0
- package/dist/components/LoginForm.d.ts +7 -0
- package/dist/components/LoginForm.js +180 -0
- package/dist/components/PasswordInput.d.ts +8 -0
- package/dist/components/PasswordInput.js +70 -0
- package/dist/components/SignUpForm.d.ts +8 -0
- package/dist/components/SignUpForm.js +198 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +19 -0
- package/dist/core/AuthContext.d.ts +3 -0
- package/dist/core/AuthContext.js +10 -0
- package/dist/core/AuthProvider.d.ts +8 -0
- package/dist/core/AuthProvider.js +350 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +18 -0
- package/dist/errors/errorMapper.d.ts +2 -0
- package/dist/errors/errorMapper.js +124 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +17 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +17 -0
- package/dist/hooks/useAuth.d.ts +2 -0
- package/dist/hooks/useAuth.js +13 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +27 -0
- package/dist/types/auth.types.d.ts +29 -0
- package/dist/types/auth.types.js +11 -0
- package/dist/types/config.types.d.ts +21 -0
- package/dist/types/config.types.js +12 -0
- package/dist/types/error.types.d.ts +23 -0
- package/dist/types/error.types.js +26 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.js +21 -0
- package/dist/types/ui.types.d.ts +20 -0
- package/dist/types/ui.types.js +2 -0
- package/dist/utils/validation.d.ts +3 -0
- package/dist/utils/validation.js +29 -0
- package/package.json +62 -0
- package/src/components/AuthScreen.tsx +87 -0
- package/src/components/LoginForm.tsx +246 -0
- package/src/components/PasswordInput.tsx +56 -0
- package/src/components/SignUpForm.tsx +293 -0
- package/src/components/index.ts +3 -0
- package/src/core/AuthContext.tsx +6 -0
- package/src/core/AuthProvider.tsx +362 -0
- package/src/core/index.ts +2 -0
- package/src/errors/errorMapper.ts +139 -0
- package/src/errors/index.ts +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useAuth.ts +13 -0
- package/src/index.ts +12 -0
- package/src/types/auth.types.ts +43 -0
- package/src/types/config.types.ts +46 -0
- package/src/types/error.types.ts +31 -0
- package/src/types/index.ts +5 -0
- package/src/types/ui.types.ts +26 -0
- package/src/utils/validation.ts +20 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare enum AuthErrorCode {
|
|
2
|
+
INVALID_CREDENTIALS = "auth/invalid-credentials",
|
|
3
|
+
USER_NOT_FOUND = "auth/user-not-found",
|
|
4
|
+
EMAIL_ALREADY_IN_USE = "auth/email-already-in-use",
|
|
5
|
+
WEAK_PASSWORD = "auth/weak-password",
|
|
6
|
+
TOKEN_EXPIRED = "auth/token-expired",
|
|
7
|
+
NETWORK_ERROR = "auth/network-request-failed",
|
|
8
|
+
UNKNOWN = "auth/unknown",
|
|
9
|
+
CONFIG_ERROR = "auth/configuration-error",
|
|
10
|
+
CANCELLED = "auth/cancelled",
|
|
11
|
+
GOOGLE_SIGN_IN_CANCELLED = "GOOGLE_SIGN_IN_CANCELLED",
|
|
12
|
+
GOOGLE_SIGN_IN_IN_PROGRESS = "GOOGLE_SIGN_IN_IN_PROGRESS",
|
|
13
|
+
GOOGLE_PLAY_SERVICES_NOT_AVAILABLE = "GOOGLE_PLAY_SERVICES_NOT_AVAILABLE",
|
|
14
|
+
GOOGLE_SIGN_IN_FAILED = "GOOGLE_SIGN_IN_FAILED",
|
|
15
|
+
APPLE_SIGN_IN_CANCELLED = "APPLE_SIGN_IN_CANCELLED",
|
|
16
|
+
APPLE_SIGN_IN_FAILED = "APPLE_SIGN_IN_FAILED",
|
|
17
|
+
APPLE_SIGN_IN_NOT_SUPPORTED = "APPLE_SIGN_IN_NOT_SUPPORTED"
|
|
18
|
+
}
|
|
19
|
+
export interface AuthError {
|
|
20
|
+
code: AuthErrorCode;
|
|
21
|
+
message: string;
|
|
22
|
+
originalError?: any;
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/types/error.types.ts
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.AuthErrorCode = void 0;
|
|
5
|
+
var AuthErrorCode;
|
|
6
|
+
(function (AuthErrorCode) {
|
|
7
|
+
AuthErrorCode["INVALID_CREDENTIALS"] = "auth/invalid-credentials";
|
|
8
|
+
AuthErrorCode["USER_NOT_FOUND"] = "auth/user-not-found";
|
|
9
|
+
AuthErrorCode["EMAIL_ALREADY_IN_USE"] = "auth/email-already-in-use";
|
|
10
|
+
AuthErrorCode["WEAK_PASSWORD"] = "auth/weak-password";
|
|
11
|
+
AuthErrorCode["TOKEN_EXPIRED"] = "auth/token-expired";
|
|
12
|
+
AuthErrorCode["NETWORK_ERROR"] = "auth/network-request-failed";
|
|
13
|
+
AuthErrorCode["UNKNOWN"] = "auth/unknown";
|
|
14
|
+
// Specific to SDK flow
|
|
15
|
+
AuthErrorCode["CONFIG_ERROR"] = "auth/configuration-error";
|
|
16
|
+
AuthErrorCode["CANCELLED"] = "auth/cancelled";
|
|
17
|
+
// Google Sign-In Errors
|
|
18
|
+
AuthErrorCode["GOOGLE_SIGN_IN_CANCELLED"] = "GOOGLE_SIGN_IN_CANCELLED";
|
|
19
|
+
AuthErrorCode["GOOGLE_SIGN_IN_IN_PROGRESS"] = "GOOGLE_SIGN_IN_IN_PROGRESS";
|
|
20
|
+
AuthErrorCode["GOOGLE_PLAY_SERVICES_NOT_AVAILABLE"] = "GOOGLE_PLAY_SERVICES_NOT_AVAILABLE";
|
|
21
|
+
AuthErrorCode["GOOGLE_SIGN_IN_FAILED"] = "GOOGLE_SIGN_IN_FAILED";
|
|
22
|
+
// Apple Sign-In Errors
|
|
23
|
+
AuthErrorCode["APPLE_SIGN_IN_CANCELLED"] = "APPLE_SIGN_IN_CANCELLED";
|
|
24
|
+
AuthErrorCode["APPLE_SIGN_IN_FAILED"] = "APPLE_SIGN_IN_FAILED";
|
|
25
|
+
AuthErrorCode["APPLE_SIGN_IN_NOT_SUPPORTED"] = "APPLE_SIGN_IN_NOT_SUPPORTED";
|
|
26
|
+
})(AuthErrorCode || (exports.AuthErrorCode = AuthErrorCode = {}));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
// src/types/index.ts
|
|
18
|
+
__exportStar(require("./auth.types"), exports);
|
|
19
|
+
__exportStar(require("./config.types"), exports);
|
|
20
|
+
__exportStar(require("./error.types"), exports);
|
|
21
|
+
__exportStar(require("./ui.types"), exports);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ViewStyle, TextStyle } from 'react-native';
|
|
2
|
+
export interface AuthScreenStyles {
|
|
3
|
+
container?: ViewStyle;
|
|
4
|
+
header?: ViewStyle;
|
|
5
|
+
footer?: ViewStyle;
|
|
6
|
+
title?: TextStyle;
|
|
7
|
+
subtitle?: TextStyle;
|
|
8
|
+
footerText?: TextStyle;
|
|
9
|
+
linkText?: TextStyle;
|
|
10
|
+
errorText?: TextStyle;
|
|
11
|
+
input?: TextStyle;
|
|
12
|
+
button?: ViewStyle;
|
|
13
|
+
buttonText?: TextStyle;
|
|
14
|
+
loadingIndicatorColor?: string;
|
|
15
|
+
inputContainer?: ViewStyle;
|
|
16
|
+
eyeIcon?: TextStyle;
|
|
17
|
+
hintContainer?: ViewStyle;
|
|
18
|
+
hintText?: TextStyle;
|
|
19
|
+
hintTextMet?: TextStyle;
|
|
20
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/utils/validation.ts
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.validatePasswordSignup = exports.validatePasswordLogin = exports.validateEmail = void 0;
|
|
5
|
+
const validateEmail = (email) => {
|
|
6
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
7
|
+
if (!email)
|
|
8
|
+
return "Email is required.";
|
|
9
|
+
if (!emailRegex.test(email))
|
|
10
|
+
return "Please enter a valid email address.";
|
|
11
|
+
return null;
|
|
12
|
+
};
|
|
13
|
+
exports.validateEmail = validateEmail;
|
|
14
|
+
const validatePasswordLogin = (password) => {
|
|
15
|
+
if (!password)
|
|
16
|
+
return "Password is required.";
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
19
|
+
exports.validatePasswordLogin = validatePasswordLogin;
|
|
20
|
+
const validatePasswordSignup = (password) => {
|
|
21
|
+
if (!password)
|
|
22
|
+
return "Password is required.";
|
|
23
|
+
if (password.length < 6)
|
|
24
|
+
return "Password must be at least 6 characters.";
|
|
25
|
+
if (!/\d/.test(password))
|
|
26
|
+
return "Password must contain at least one number.";
|
|
27
|
+
return null;
|
|
28
|
+
};
|
|
29
|
+
exports.validatePasswordSignup = validatePasswordSignup;
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rn-swiftauth-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"src"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"watch": "tsc -w",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"react-native",
|
|
17
|
+
"firebase",
|
|
18
|
+
"auth",
|
|
19
|
+
"sdk",
|
|
20
|
+
"expo",
|
|
21
|
+
"google-signin",
|
|
22
|
+
"apple-signin",
|
|
23
|
+
"oauth"
|
|
24
|
+
],
|
|
25
|
+
"author": "Mobile Ninjas - HNG Stage 8",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"description": "Modular Authentication SDK with Firebase, Google Sign-In, and Apple Sign-In support",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@react-native-async-storage/async-storage": ">=1.18.0",
|
|
30
|
+
"@react-native-google-signin/google-signin": ">=10.0.0",
|
|
31
|
+
"expo-apple-authentication": ">=6.0.0",
|
|
32
|
+
"expo-crypto": ">=13.0.0",
|
|
33
|
+
"firebase": ">=9.0.0",
|
|
34
|
+
"react": ">=18.0.0",
|
|
35
|
+
"react-native": ">=0.70.0",
|
|
36
|
+
"react-native-safe-area-context": ">=4.0.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"@react-native-google-signin/google-signin": {
|
|
40
|
+
"optional": false
|
|
41
|
+
},
|
|
42
|
+
"expo-apple-authentication": {
|
|
43
|
+
"optional": false
|
|
44
|
+
},
|
|
45
|
+
"expo-crypto": {
|
|
46
|
+
"optional": false
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@react-native-async-storage/async-storage": "^2.1.0",
|
|
51
|
+
"@react-native-google-signin/google-signin": "^13.1.0",
|
|
52
|
+
"@types/react": "^18.0.0",
|
|
53
|
+
"@types/react-native": "^0.72.8",
|
|
54
|
+
"expo-apple-authentication": "^6.4.2",
|
|
55
|
+
"expo-crypto": "^13.0.2",
|
|
56
|
+
"firebase": "^12.6.0",
|
|
57
|
+
"react": "18.3.1",
|
|
58
|
+
"react-native": "0.76.1",
|
|
59
|
+
"react-native-safe-area-context": "^5.6.2",
|
|
60
|
+
"typescript": "^5.0.0"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity, StyleSheet, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
|
|
3
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
4
|
+
import { LoginForm } from './LoginForm';
|
|
5
|
+
import { SignUpForm } from './SignUpForm';
|
|
6
|
+
import { AuthScreenStyles } from '../types';
|
|
7
|
+
|
|
8
|
+
interface AuthScreenProps {
|
|
9
|
+
styles?: AuthScreenStyles;
|
|
10
|
+
titles?: {
|
|
11
|
+
loginTitle?: string;
|
|
12
|
+
loginSubtitle?: string;
|
|
13
|
+
signupTitle?: string;
|
|
14
|
+
signupSubtitle?: string;
|
|
15
|
+
};
|
|
16
|
+
showPasswordHints?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const AuthScreen = ({ styles: userStyles, titles, showPasswordHints = true }: AuthScreenProps) => {
|
|
20
|
+
const [view, setView] = useState<'login' | 'signup'>('login');
|
|
21
|
+
|
|
22
|
+
const isLogin = view === 'login';
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<SafeAreaView style={[defaultStyles.safeArea, userStyles?.container]}>
|
|
26
|
+
<KeyboardAvoidingView
|
|
27
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
28
|
+
style={{ flex: 1 }}
|
|
29
|
+
>
|
|
30
|
+
<ScrollView
|
|
31
|
+
contentContainerStyle={defaultStyles.scrollContainer}
|
|
32
|
+
keyboardShouldPersistTaps="handled"
|
|
33
|
+
>
|
|
34
|
+
<View style={defaultStyles.innerContainer}>
|
|
35
|
+
|
|
36
|
+
{/* Header Section */}
|
|
37
|
+
<View style={[defaultStyles.header, userStyles?.header]}>
|
|
38
|
+
<Text style={[defaultStyles.title, userStyles?.title]}>
|
|
39
|
+
{isLogin
|
|
40
|
+
? (titles?.loginTitle || 'Welcome Back')
|
|
41
|
+
: (titles?.signupTitle || 'Create Account')}
|
|
42
|
+
</Text>
|
|
43
|
+
<Text style={[defaultStyles.subtitle, userStyles?.subtitle]}>
|
|
44
|
+
{isLogin
|
|
45
|
+
? (titles?.loginSubtitle || 'Sign in to continue')
|
|
46
|
+
: (titles?.signupSubtitle || 'Sign up to get started')}
|
|
47
|
+
</Text>
|
|
48
|
+
</View>
|
|
49
|
+
|
|
50
|
+
{/* Form Section */}
|
|
51
|
+
{isLogin
|
|
52
|
+
? <LoginForm styles={userStyles} />
|
|
53
|
+
: <SignUpForm styles={userStyles} showHints={showPasswordHints} />
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
{/* Footer / Toggle Section */}
|
|
57
|
+
<View style={[defaultStyles.footer, userStyles?.footer]}>
|
|
58
|
+
<Text style={[defaultStyles.footerText, userStyles?.footerText]}>
|
|
59
|
+
{isLogin ? "Don't have an account?" : "Already have an account?"}
|
|
60
|
+
</Text>
|
|
61
|
+
<TouchableOpacity onPress={() => setView(isLogin ? 'signup' : 'login')}>
|
|
62
|
+
<Text style={[defaultStyles.linkText, userStyles?.linkText]}>
|
|
63
|
+
{isLogin ? ' Sign Up' : ' Sign In'}
|
|
64
|
+
</Text>
|
|
65
|
+
</TouchableOpacity>
|
|
66
|
+
</View>
|
|
67
|
+
|
|
68
|
+
</View>
|
|
69
|
+
</ScrollView>
|
|
70
|
+
</KeyboardAvoidingView>
|
|
71
|
+
</SafeAreaView>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const defaultStyles = StyleSheet.create({
|
|
76
|
+
safeArea: { flex: 1, backgroundColor: '#fff' },
|
|
77
|
+
scrollContainer: { flexGrow: 1, justifyContent: 'center' },
|
|
78
|
+
innerContainer: { padding: 24, width: '100%', maxWidth: 500, alignSelf: 'center' },
|
|
79
|
+
|
|
80
|
+
header: { marginBottom: 32, alignItems: 'center' },
|
|
81
|
+
title: { fontSize: 28, fontWeight: 'bold', color: '#1a1a1a', marginBottom: 8 },
|
|
82
|
+
subtitle: { fontSize: 16, color: '#666', textAlign: 'center' },
|
|
83
|
+
|
|
84
|
+
footer: { flexDirection: 'row', justifyContent: 'center', marginTop: 24, marginBottom: 20 },
|
|
85
|
+
footerText: { color: '#666', fontSize: 14 },
|
|
86
|
+
linkText: { color: '#007AFF', fontWeight: '600', fontSize: 14, marginLeft: 5 },
|
|
87
|
+
});
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
TextInput,
|
|
5
|
+
Text,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
ActivityIndicator,
|
|
9
|
+
Platform
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
import { useAuth } from '../hooks/useAuth';
|
|
12
|
+
import { AuthScreenStyles } from '../types';
|
|
13
|
+
import { PasswordInput } from './PasswordInput';
|
|
14
|
+
import { validateEmail, validatePasswordLogin } from '../utils/validation';
|
|
15
|
+
|
|
16
|
+
interface LoginFormProps {
|
|
17
|
+
styles?: AuthScreenStyles;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const LoginForm = ({ styles: userStyles }: LoginFormProps) => {
|
|
21
|
+
const {
|
|
22
|
+
signInWithEmail,
|
|
23
|
+
signInWithGoogle,
|
|
24
|
+
signInWithApple,
|
|
25
|
+
isLoading, // ✅ Using the simplified boolean we added to context
|
|
26
|
+
error,
|
|
27
|
+
config // ✅ We need this to check if social buttons are enabled
|
|
28
|
+
} = useAuth();
|
|
29
|
+
|
|
30
|
+
const [email, setEmail] = useState('');
|
|
31
|
+
const [password, setPassword] = useState('');
|
|
32
|
+
|
|
33
|
+
// ✅ Validation Error State
|
|
34
|
+
const [validationErrors, setValidationErrors] = useState<{ email?: string; password?: string }>({});
|
|
35
|
+
|
|
36
|
+
const handleLogin = async () => {
|
|
37
|
+
// 1. Reset previous errors
|
|
38
|
+
setValidationErrors({});
|
|
39
|
+
|
|
40
|
+
// 2. Validate Inputs
|
|
41
|
+
const emailErr = validateEmail(email);
|
|
42
|
+
const passErr = validatePasswordLogin(password);
|
|
43
|
+
|
|
44
|
+
if (emailErr || passErr) {
|
|
45
|
+
setValidationErrors({
|
|
46
|
+
email: emailErr || undefined,
|
|
47
|
+
password: passErr || undefined
|
|
48
|
+
});
|
|
49
|
+
return; // 🛑 Stop if invalid
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 3. Attempt Login
|
|
53
|
+
try {
|
|
54
|
+
await signInWithEmail(email, password);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
// Auth errors handled by global state
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleGoogleSignIn = async () => {
|
|
61
|
+
try {
|
|
62
|
+
await signInWithGoogle();
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error('Google Sign-In Error:', e);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleAppleSignIn = async () => {
|
|
69
|
+
try {
|
|
70
|
+
await signInWithApple();
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.error('Apple Sign-In Error:', e);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<View style={[defaultStyles.container, userStyles?.container]}>
|
|
78
|
+
{/* Global API Error */}
|
|
79
|
+
{error && (
|
|
80
|
+
<Text style={[defaultStyles.errorText, userStyles?.errorText]}>
|
|
81
|
+
{error.message}
|
|
82
|
+
</Text>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
{/* Email Input */}
|
|
86
|
+
<TextInput
|
|
87
|
+
style={[
|
|
88
|
+
defaultStyles.input,
|
|
89
|
+
userStyles?.input,
|
|
90
|
+
validationErrors.email ? { borderColor: 'red' } : {} // Highlight on error
|
|
91
|
+
]}
|
|
92
|
+
placeholder="Email"
|
|
93
|
+
value={email}
|
|
94
|
+
onChangeText={(text) => {
|
|
95
|
+
setEmail(text);
|
|
96
|
+
if (validationErrors.email) setValidationErrors({...validationErrors, email: undefined});
|
|
97
|
+
}}
|
|
98
|
+
autoCapitalize="none"
|
|
99
|
+
keyboardType="email-address"
|
|
100
|
+
placeholderTextColor="#999"
|
|
101
|
+
editable={!isLoading}
|
|
102
|
+
/>
|
|
103
|
+
{/* Validation Message */}
|
|
104
|
+
{validationErrors.email && (
|
|
105
|
+
<Text style={defaultStyles.validationText}>{validationErrors.email}</Text>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{/* Password Input */}
|
|
109
|
+
<PasswordInput
|
|
110
|
+
styles={userStyles}
|
|
111
|
+
placeholder="Password"
|
|
112
|
+
value={password}
|
|
113
|
+
onChangeText={(text) => {
|
|
114
|
+
setPassword(text);
|
|
115
|
+
if (validationErrors.password) setValidationErrors({...validationErrors, password: undefined});
|
|
116
|
+
}}
|
|
117
|
+
editable={!isLoading}
|
|
118
|
+
/>
|
|
119
|
+
{validationErrors.password && (
|
|
120
|
+
<Text style={defaultStyles.validationText}>{validationErrors.password}</Text>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
{/* Sign In Button */}
|
|
124
|
+
<TouchableOpacity
|
|
125
|
+
style={[
|
|
126
|
+
defaultStyles.button,
|
|
127
|
+
isLoading && defaultStyles.buttonDisabled,
|
|
128
|
+
userStyles?.button
|
|
129
|
+
]}
|
|
130
|
+
onPress={handleLogin}
|
|
131
|
+
disabled={isLoading}
|
|
132
|
+
>
|
|
133
|
+
{isLoading ? (
|
|
134
|
+
<ActivityIndicator color={userStyles?.loadingIndicatorColor || "#fff"} />
|
|
135
|
+
) : (
|
|
136
|
+
<Text style={[defaultStyles.buttonText, userStyles?.buttonText]}>
|
|
137
|
+
Sign In
|
|
138
|
+
</Text>
|
|
139
|
+
)}
|
|
140
|
+
</TouchableOpacity>
|
|
141
|
+
|
|
142
|
+
{/* OAuth Section - Conditionally Rendered based on Config */}
|
|
143
|
+
{(config.enableGoogle || config.enableApple) && !isLoading && (
|
|
144
|
+
<>
|
|
145
|
+
<View style={defaultStyles.dividerContainer}>
|
|
146
|
+
<View style={defaultStyles.divider} />
|
|
147
|
+
<Text style={defaultStyles.dividerText}>OR</Text>
|
|
148
|
+
<View style={defaultStyles.divider} />
|
|
149
|
+
</View>
|
|
150
|
+
|
|
151
|
+
{/* Google Button */}
|
|
152
|
+
{config.enableGoogle && (
|
|
153
|
+
<TouchableOpacity
|
|
154
|
+
style={[
|
|
155
|
+
defaultStyles.oauthButton,
|
|
156
|
+
defaultStyles.googleButton,
|
|
157
|
+
]}
|
|
158
|
+
onPress={handleGoogleSignIn}
|
|
159
|
+
>
|
|
160
|
+
<Text style={defaultStyles.googleButtonText}>
|
|
161
|
+
Continue with Google
|
|
162
|
+
</Text>
|
|
163
|
+
</TouchableOpacity>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
{/* Apple Button (iOS Only) */}
|
|
167
|
+
{config.enableApple && Platform.OS === 'ios' && (
|
|
168
|
+
<TouchableOpacity
|
|
169
|
+
style={[
|
|
170
|
+
defaultStyles.oauthButton,
|
|
171
|
+
defaultStyles.appleButton,
|
|
172
|
+
]}
|
|
173
|
+
onPress={handleAppleSignIn}
|
|
174
|
+
>
|
|
175
|
+
<Text style={defaultStyles.appleButtonText}>
|
|
176
|
+
Continue with Apple
|
|
177
|
+
</Text>
|
|
178
|
+
</TouchableOpacity>
|
|
179
|
+
)}
|
|
180
|
+
</>
|
|
181
|
+
)}
|
|
182
|
+
</View>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const defaultStyles = StyleSheet.create({
|
|
187
|
+
container: { width: '100%', marginVertical: 10 },
|
|
188
|
+
|
|
189
|
+
input: {
|
|
190
|
+
backgroundColor: '#f5f5f5',
|
|
191
|
+
padding: 15,
|
|
192
|
+
borderRadius: 8,
|
|
193
|
+
marginBottom: 8, // Reduced slightly to make room for validation text
|
|
194
|
+
borderWidth: 1,
|
|
195
|
+
borderColor: '#e0e0e0',
|
|
196
|
+
fontSize: 16,
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
button: {
|
|
200
|
+
backgroundColor: '#007AFF',
|
|
201
|
+
padding: 15,
|
|
202
|
+
borderRadius: 8,
|
|
203
|
+
alignItems: 'center',
|
|
204
|
+
marginTop: 8,
|
|
205
|
+
},
|
|
206
|
+
buttonDisabled: { backgroundColor: '#a0cfff' },
|
|
207
|
+
buttonText: { color: '#fff', fontWeight: '600', fontSize: 16 },
|
|
208
|
+
|
|
209
|
+
errorText: { color: 'red', marginBottom: 12, fontSize: 14, textAlign: 'center' },
|
|
210
|
+
validationText: { color: 'red', fontSize: 12, marginBottom: 10, marginLeft: 4, marginTop: -4 },
|
|
211
|
+
|
|
212
|
+
dividerContainer: {
|
|
213
|
+
flexDirection: 'row',
|
|
214
|
+
alignItems: 'center',
|
|
215
|
+
marginVertical: 20,
|
|
216
|
+
},
|
|
217
|
+
divider: { flex: 1, height: 1, backgroundColor: '#e0e0e0' },
|
|
218
|
+
dividerText: { marginHorizontal: 16, color: '#666', fontSize: 14, fontWeight: '500' },
|
|
219
|
+
|
|
220
|
+
oauthButton: {
|
|
221
|
+
padding: 15,
|
|
222
|
+
borderRadius: 8,
|
|
223
|
+
alignItems: 'center',
|
|
224
|
+
marginBottom: 10,
|
|
225
|
+
flexDirection: 'row',
|
|
226
|
+
justifyContent: 'center',
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
googleButton: {
|
|
230
|
+
backgroundColor: '#fff',
|
|
231
|
+
borderWidth: 1,
|
|
232
|
+
borderColor: '#e0e0e0',
|
|
233
|
+
},
|
|
234
|
+
googleButtonText: {
|
|
235
|
+
color: '#000',
|
|
236
|
+
fontSize: 16,
|
|
237
|
+
fontWeight: '500',
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
appleButton: { backgroundColor: '#000' },
|
|
241
|
+
appleButtonText: {
|
|
242
|
+
color: '#fff',
|
|
243
|
+
fontSize: 16,
|
|
244
|
+
fontWeight: '600',
|
|
245
|
+
},
|
|
246
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, TextInput, Text, TouchableOpacity, StyleSheet, TextInputProps } from 'react-native';
|
|
3
|
+
import { AuthScreenStyles } from '../types';
|
|
4
|
+
|
|
5
|
+
interface PasswordInputProps extends TextInputProps {
|
|
6
|
+
styles?: AuthScreenStyles;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const PasswordInput = ({ styles, ...props }: PasswordInputProps) => {
|
|
10
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<View style={[defaultStyles.container, styles?.inputContainer]}>
|
|
14
|
+
<TextInput
|
|
15
|
+
{...props}
|
|
16
|
+
style={[defaultStyles.input, styles?.input, { flex: 1, borderWidth: 0, marginBottom: 0 }]}
|
|
17
|
+
secureTextEntry={!isVisible}
|
|
18
|
+
placeholderTextColor="#999"
|
|
19
|
+
/>
|
|
20
|
+
<TouchableOpacity
|
|
21
|
+
onPress={() => setIsVisible(!isVisible)}
|
|
22
|
+
style={defaultStyles.iconContainer}
|
|
23
|
+
>
|
|
24
|
+
{/* Using Unicode characters to avoid icon library dependencies */}
|
|
25
|
+
<Text style={[defaultStyles.iconText, styles?.eyeIcon]}>
|
|
26
|
+
{isVisible ? "👁️🗨️" : "👁️"}
|
|
27
|
+
</Text>
|
|
28
|
+
</TouchableOpacity>
|
|
29
|
+
</View>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const defaultStyles = StyleSheet.create({
|
|
34
|
+
container: {
|
|
35
|
+
flexDirection: 'row',
|
|
36
|
+
alignItems: 'center',
|
|
37
|
+
backgroundColor: '#f5f5f5',
|
|
38
|
+
borderRadius: 8,
|
|
39
|
+
borderWidth: 1,
|
|
40
|
+
borderColor: '#e0e0e0',
|
|
41
|
+
marginBottom: 12,
|
|
42
|
+
overflow: 'hidden',
|
|
43
|
+
},
|
|
44
|
+
input: {
|
|
45
|
+
padding: 15,
|
|
46
|
+
fontSize: 16,
|
|
47
|
+
backgroundColor: 'transparent',
|
|
48
|
+
},
|
|
49
|
+
iconContainer: {
|
|
50
|
+
padding: 15,
|
|
51
|
+
justifyContent: 'center',
|
|
52
|
+
},
|
|
53
|
+
iconText: {
|
|
54
|
+
fontSize: 18,
|
|
55
|
+
}
|
|
56
|
+
});
|