diff --git a/code/01-starting-code/App.js b/code/01-starting-code/App.js
new file mode 100644
index 00000000..3a3203b0
--- /dev/null
+++ b/code/01-starting-code/App.js
@@ -0,0 +1,57 @@
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { StatusBar } from 'expo-status-bar';
+
+import LoginScreen from './screens/LoginScreen';
+import SignupScreen from './screens/SignupScreen';
+import WelcomeScreen from './screens/WelcomeScreen';
+import { Colors } from './constants/styles';
+
+const Stack = createNativeStackNavigator();
+
+function AuthStack() {
+ return (
+
+
+
+
+ );
+}
+
+function AuthenticatedStack() {
+ return (
+
+
+
+ );
+}
+
+function Navigation() {
+ return (
+
+
+
+ );
+}
+
+export default function App() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/code/01-starting-code/app.json b/code/01-starting-code/app.json
new file mode 100644
index 00000000..9a1223e7
--- /dev/null
+++ b/code/01-starting-code/app.json
@@ -0,0 +1,32 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/code/01-starting-code/assets/adaptive-icon.png b/code/01-starting-code/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/code/01-starting-code/assets/adaptive-icon.png differ
diff --git a/code/01-starting-code/assets/favicon.png b/code/01-starting-code/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/code/01-starting-code/assets/favicon.png differ
diff --git a/code/01-starting-code/assets/icon.png b/code/01-starting-code/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/code/01-starting-code/assets/icon.png differ
diff --git a/code/01-starting-code/assets/splash.png b/code/01-starting-code/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/code/01-starting-code/assets/splash.png differ
diff --git a/code/01-starting-code/babel.config.js b/code/01-starting-code/babel.config.js
new file mode 100644
index 00000000..2900afe9
--- /dev/null
+++ b/code/01-starting-code/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/code/01-starting-code/components/Auth/AuthContent.js b/code/01-starting-code/components/Auth/AuthContent.js
new file mode 100644
index 00000000..eae3b56b
--- /dev/null
+++ b/code/01-starting-code/components/Auth/AuthContent.js
@@ -0,0 +1,83 @@
+import { useState } from 'react';
+import { Alert, StyleSheet, View } from 'react-native';
+
+import FlatButton from '../ui/FlatButton';
+import AuthForm from './AuthForm';
+import { Colors } from '../../constants/styles';
+
+function AuthContent({ isLogin, onAuthenticate }) {
+
+ const [credentialsInvalid, setCredentialsInvalid] = useState({
+ email: false,
+ password: false,
+ confirmEmail: false,
+ confirmPassword: false,
+ });
+
+ function switchAuthModeHandler() {
+ // Todo
+ }
+
+ function submitHandler(credentials) {
+ let { email, confirmEmail, password, confirmPassword } = credentials;
+
+ email = email.trim();
+ password = password.trim();
+
+ const emailIsValid = email.includes('@');
+ const passwordIsValid = password.length > 6;
+ const emailsAreEqual = email === confirmEmail;
+ const passwordsAreEqual = password === confirmPassword;
+
+ if (
+ !emailIsValid ||
+ !passwordIsValid ||
+ (!isLogin && (!emailsAreEqual || !passwordsAreEqual))
+ ) {
+ Alert.alert('Invalid input', 'Please check your entered credentials.');
+ setCredentialsInvalid({
+ email: !emailIsValid,
+ confirmEmail: !emailIsValid || !emailsAreEqual,
+ password: !passwordIsValid,
+ confirmPassword: !passwordIsValid || !passwordsAreEqual,
+ });
+ return;
+ }
+ onAuthenticate({ email, password });
+ }
+
+ return (
+
+
+
+
+ {isLogin ? 'Create a new user' : 'Log in instead'}
+
+
+
+ );
+}
+
+export default AuthContent;
+
+const styles = StyleSheet.create({
+ authContent: {
+ marginTop: 64,
+ marginHorizontal: 32,
+ padding: 16,
+ borderRadius: 8,
+ backgroundColor: Colors.primary800,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.35,
+ shadowRadius: 4,
+ },
+ buttons: {
+ marginTop: 8,
+ },
+});
diff --git a/code/01-starting-code/components/Auth/AuthForm.js b/code/01-starting-code/components/Auth/AuthForm.js
new file mode 100644
index 00000000..cf4a7a81
--- /dev/null
+++ b/code/01-starting-code/components/Auth/AuthForm.js
@@ -0,0 +1,100 @@
+import { useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import Button from '../ui/Button';
+import Input from './Input';
+
+function AuthForm({ isLogin, onSubmit, credentialsInvalid }) {
+ const [enteredEmail, setEnteredEmail] = useState('');
+ const [enteredConfirmEmail, setEnteredConfirmEmail] = useState('');
+ const [enteredPassword, setEnteredPassword] = useState('');
+ const [enteredConfirmPassword, setEnteredConfirmPassword] = useState('');
+
+ const {
+ email: emailIsInvalid,
+ confirmEmail: emailsDontMatch,
+ password: passwordIsInvalid,
+ confirmPassword: passwordsDontMatch,
+ } = credentialsInvalid;
+
+ function updateInputValueHandler(inputType, enteredValue) {
+ switch (inputType) {
+ case 'email':
+ setEnteredEmail(enteredValue);
+ break;
+ case 'confirmEmail':
+ setEnteredConfirmEmail(enteredValue);
+ break;
+ case 'password':
+ setEnteredPassword(enteredValue);
+ break;
+ case 'confirmPassword':
+ setEnteredConfirmPassword(enteredValue);
+ break;
+ }
+ }
+
+ function submitHandler() {
+ onSubmit({
+ email: enteredEmail,
+ confirmEmail: enteredConfirmEmail,
+ password: enteredPassword,
+ confirmPassword: enteredConfirmPassword,
+ });
+ }
+
+ return (
+
+
+
+ {!isLogin && (
+
+ )}
+
+ {!isLogin && (
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default AuthForm;
+
+const styles = StyleSheet.create({
+ buttons: {
+ marginTop: 12,
+ },
+});
diff --git a/code/01-starting-code/components/Auth/Input.js b/code/01-starting-code/components/Auth/Input.js
new file mode 100644
index 00000000..3aa0d5dd
--- /dev/null
+++ b/code/01-starting-code/components/Auth/Input.js
@@ -0,0 +1,54 @@
+import { View, Text, TextInput, StyleSheet } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Input({
+ label,
+ keyboardType,
+ secure,
+ onUpdateValue,
+ value,
+ isInvalid,
+}) {
+ return (
+
+
+ {label}
+
+
+
+ );
+}
+
+export default Input;
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ marginVertical: 8,
+ },
+ label: {
+ color: 'white',
+ marginBottom: 4,
+ },
+ labelInvalid: {
+ color: Colors.error500,
+ },
+ input: {
+ paddingVertical: 8,
+ paddingHorizontal: 6,
+ backgroundColor: Colors.primary100,
+ borderRadius: 4,
+ fontSize: 16,
+ },
+ inputInvalid: {
+ backgroundColor: Colors.error100,
+ },
+});
diff --git a/code/01-starting-code/components/ui/Button.js b/code/01-starting-code/components/ui/Button.js
new file mode 100644
index 00000000..9b4b231d
--- /dev/null
+++ b/code/01-starting-code/components/ui/Button.js
@@ -0,0 +1,41 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Button({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default Button;
+
+const styles = StyleSheet.create({
+ button: {
+ borderRadius: 6,
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ backgroundColor: Colors.primary500,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: 'white',
+ fontSize: 16,
+ fontWeight: 'bold'
+ },
+});
diff --git a/code/01-starting-code/components/ui/FlatButton.js b/code/01-starting-code/components/ui/FlatButton.js
new file mode 100644
index 00000000..1b45e9ec
--- /dev/null
+++ b/code/01-starting-code/components/ui/FlatButton.js
@@ -0,0 +1,32 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function FlatButton({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default FlatButton;
+
+const styles = StyleSheet.create({
+ button: {
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: Colors.primary100,
+ },
+});
diff --git a/code/01-starting-code/components/ui/IconButton.js b/code/01-starting-code/components/ui/IconButton.js
new file mode 100644
index 00000000..7f438db4
--- /dev/null
+++ b/code/01-starting-code/components/ui/IconButton.js
@@ -0,0 +1,25 @@
+import { Pressable, StyleSheet } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+
+function IconButton({ icon, color, size, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+
+ );
+}
+
+export default IconButton;
+
+const styles = StyleSheet.create({
+ button: {
+ margin: 8,
+ borderRadius: 20,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+});
diff --git a/code/01-starting-code/components/ui/LoadingOverlay.js b/code/01-starting-code/components/ui/LoadingOverlay.js
new file mode 100644
index 00000000..99ae9528
--- /dev/null
+++ b/code/01-starting-code/components/ui/LoadingOverlay.js
@@ -0,0 +1,25 @@
+import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
+
+function LoadingOverlay({ message }) {
+ return (
+
+ {message}
+
+
+ );
+}
+
+export default LoadingOverlay;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ message: {
+ fontSize: 16,
+ marginBottom: 12,
+ },
+});
diff --git a/code/01-starting-code/constants/styles.js b/code/01-starting-code/constants/styles.js
new file mode 100644
index 00000000..0ae92d20
--- /dev/null
+++ b/code/01-starting-code/constants/styles.js
@@ -0,0 +1,7 @@
+export const Colors = {
+ primary100: '#f9beda',
+ primary500: '#c30b64',
+ primary800: '#610440',
+ error100: '#fcdcbf',
+ error500: '#f37c13',
+}
\ No newline at end of file
diff --git a/code/01-starting-code/package.json b/code/01-starting-code/package.json
new file mode 100644
index 00000000..bb3ea4d1
--- /dev/null
+++ b/code/01-starting-code/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-navigation/native": "^6.0.8",
+ "@react-navigation/native-stack": "^6.5.0",
+ "expo": "~44.0.0",
+ "expo-status-bar": "~1.2.0",
+ "react": "17.0.1",
+ "react-dom": "17.0.1",
+ "react-native": "0.64.3",
+ "react-native-web": "0.17.1"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.12.9"
+ },
+ "private": true
+}
diff --git a/code/01-starting-code/screens/LoginScreen.js b/code/01-starting-code/screens/LoginScreen.js
new file mode 100644
index 00000000..9626a64a
--- /dev/null
+++ b/code/01-starting-code/screens/LoginScreen.js
@@ -0,0 +1,7 @@
+import AuthContent from '../components/Auth/AuthContent';
+
+function LoginScreen() {
+ return ;
+}
+
+export default LoginScreen;
diff --git a/code/01-starting-code/screens/SignupScreen.js b/code/01-starting-code/screens/SignupScreen.js
new file mode 100644
index 00000000..12bdd477
--- /dev/null
+++ b/code/01-starting-code/screens/SignupScreen.js
@@ -0,0 +1,7 @@
+import AuthContent from '../components/Auth/AuthContent';
+
+function SignupScreen() {
+ return ;
+}
+
+export default SignupScreen;
diff --git a/code/01-starting-code/screens/WelcomeScreen.js b/code/01-starting-code/screens/WelcomeScreen.js
new file mode 100644
index 00000000..b51d4283
--- /dev/null
+++ b/code/01-starting-code/screens/WelcomeScreen.js
@@ -0,0 +1,26 @@
+import { StyleSheet, Text, View } from 'react-native';
+
+function WelcomeScreen() {
+ return (
+
+ Welcome!
+ You authenticated successfully!
+
+ );
+}
+
+export default WelcomeScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ title: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ },
+});
diff --git a/code/02-controlling-signup-login-screens/App.js b/code/02-controlling-signup-login-screens/App.js
new file mode 100644
index 00000000..3a3203b0
--- /dev/null
+++ b/code/02-controlling-signup-login-screens/App.js
@@ -0,0 +1,57 @@
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { StatusBar } from 'expo-status-bar';
+
+import LoginScreen from './screens/LoginScreen';
+import SignupScreen from './screens/SignupScreen';
+import WelcomeScreen from './screens/WelcomeScreen';
+import { Colors } from './constants/styles';
+
+const Stack = createNativeStackNavigator();
+
+function AuthStack() {
+ return (
+
+
+
+
+ );
+}
+
+function AuthenticatedStack() {
+ return (
+
+
+
+ );
+}
+
+function Navigation() {
+ return (
+
+
+
+ );
+}
+
+export default function App() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/code/02-controlling-signup-login-screens/app.json b/code/02-controlling-signup-login-screens/app.json
new file mode 100644
index 00000000..9a1223e7
--- /dev/null
+++ b/code/02-controlling-signup-login-screens/app.json
@@ -0,0 +1,32 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/code/02-controlling-signup-login-screens/assets/adaptive-icon.png b/code/02-controlling-signup-login-screens/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/code/02-controlling-signup-login-screens/assets/adaptive-icon.png differ
diff --git a/code/02-controlling-signup-login-screens/assets/favicon.png b/code/02-controlling-signup-login-screens/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/code/02-controlling-signup-login-screens/assets/favicon.png differ
diff --git a/code/02-controlling-signup-login-screens/assets/icon.png b/code/02-controlling-signup-login-screens/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/code/02-controlling-signup-login-screens/assets/icon.png differ
diff --git a/code/02-controlling-signup-login-screens/assets/splash.png b/code/02-controlling-signup-login-screens/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/code/02-controlling-signup-login-screens/assets/splash.png differ
diff --git a/code/02-controlling-signup-login-screens/babel.config.js b/code/02-controlling-signup-login-screens/babel.config.js
new file mode 100644
index 00000000..2900afe9
--- /dev/null
+++ b/code/02-controlling-signup-login-screens/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/code/02-controlling-signup-login-screens/components/Auth/AuthContent.js b/code/02-controlling-signup-login-screens/components/Auth/AuthContent.js
new file mode 100644
index 00000000..8eb04b4f
--- /dev/null
+++ b/code/02-controlling-signup-login-screens/components/Auth/AuthContent.js
@@ -0,0 +1,89 @@
+import { useState } from 'react';
+import { Alert, StyleSheet, View } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+
+import FlatButton from '../ui/FlatButton';
+import AuthForm from './AuthForm';
+import { Colors } from '../../constants/styles';
+
+function AuthContent({ isLogin, onAuthenticate }) {
+ const navigation = useNavigation();
+
+ const [credentialsInvalid, setCredentialsInvalid] = useState({
+ email: false,
+ password: false,
+ confirmEmail: false,
+ confirmPassword: false,
+ });
+
+ function switchAuthModeHandler() {
+ if (isLogin) {
+ navigation.replace('Signup');
+ } else {
+ navigation.replace('Login');
+ }
+ }
+
+ function submitHandler(credentials) {
+ let { email, confirmEmail, password, confirmPassword } = credentials;
+
+ email = email.trim();
+ password = password.trim();
+
+ const emailIsValid = email.includes('@');
+ const passwordIsValid = password.length > 6;
+ const emailsAreEqual = email === confirmEmail;
+ const passwordsAreEqual = password === confirmPassword;
+
+ if (
+ !emailIsValid ||
+ !passwordIsValid ||
+ (!isLogin && (!emailsAreEqual || !passwordsAreEqual))
+ ) {
+ Alert.alert('Invalid input', 'Please check your entered credentials.');
+ setCredentialsInvalid({
+ email: !emailIsValid,
+ confirmEmail: !emailIsValid || !emailsAreEqual,
+ password: !passwordIsValid,
+ confirmPassword: !passwordIsValid || !passwordsAreEqual,
+ });
+ return;
+ }
+ onAuthenticate({ email, password });
+ }
+
+ return (
+
+
+
+
+ {isLogin ? 'Create a new user' : 'Log in instead'}
+
+
+
+ );
+}
+
+export default AuthContent;
+
+const styles = StyleSheet.create({
+ authContent: {
+ marginTop: 64,
+ marginHorizontal: 32,
+ padding: 16,
+ borderRadius: 8,
+ backgroundColor: Colors.primary800,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.35,
+ shadowRadius: 4,
+ },
+ buttons: {
+ marginTop: 8,
+ },
+});
diff --git a/code/02-controlling-signup-login-screens/components/Auth/AuthForm.js b/code/02-controlling-signup-login-screens/components/Auth/AuthForm.js
new file mode 100644
index 00000000..cf4a7a81
--- /dev/null
+++ b/code/02-controlling-signup-login-screens/components/Auth/AuthForm.js
@@ -0,0 +1,100 @@
+import { useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import Button from '../ui/Button';
+import Input from './Input';
+
+function AuthForm({ isLogin, onSubmit, credentialsInvalid }) {
+ const [enteredEmail, setEnteredEmail] = useState('');
+ const [enteredConfirmEmail, setEnteredConfirmEmail] = useState('');
+ const [enteredPassword, setEnteredPassword] = useState('');
+ const [enteredConfirmPassword, setEnteredConfirmPassword] = useState('');
+
+ const {
+ email: emailIsInvalid,
+ confirmEmail: emailsDontMatch,
+ password: passwordIsInvalid,
+ confirmPassword: passwordsDontMatch,
+ } = credentialsInvalid;
+
+ function updateInputValueHandler(inputType, enteredValue) {
+ switch (inputType) {
+ case 'email':
+ setEnteredEmail(enteredValue);
+ break;
+ case 'confirmEmail':
+ setEnteredConfirmEmail(enteredValue);
+ break;
+ case 'password':
+ setEnteredPassword(enteredValue);
+ break;
+ case 'confirmPassword':
+ setEnteredConfirmPassword(enteredValue);
+ break;
+ }
+ }
+
+ function submitHandler() {
+ onSubmit({
+ email: enteredEmail,
+ confirmEmail: enteredConfirmEmail,
+ password: enteredPassword,
+ confirmPassword: enteredConfirmPassword,
+ });
+ }
+
+ return (
+
+
+
+ {!isLogin && (
+
+ )}
+
+ {!isLogin && (
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default AuthForm;
+
+const styles = StyleSheet.create({
+ buttons: {
+ marginTop: 12,
+ },
+});
diff --git a/code/02-controlling-signup-login-screens/components/Auth/Input.js b/code/02-controlling-signup-login-screens/components/Auth/Input.js
new file mode 100644
index 00000000..3aa0d5dd
--- /dev/null
+++ b/code/02-controlling-signup-login-screens/components/Auth/Input.js
@@ -0,0 +1,54 @@
+import { View, Text, TextInput, StyleSheet } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Input({
+ label,
+ keyboardType,
+ secure,
+ onUpdateValue,
+ value,
+ isInvalid,
+}) {
+ return (
+
+
+ {label}
+
+
+
+ );
+}
+
+export default Input;
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ marginVertical: 8,
+ },
+ label: {
+ color: 'white',
+ marginBottom: 4,
+ },
+ labelInvalid: {
+ color: Colors.error500,
+ },
+ input: {
+ paddingVertical: 8,
+ paddingHorizontal: 6,
+ backgroundColor: Colors.primary100,
+ borderRadius: 4,
+ fontSize: 16,
+ },
+ inputInvalid: {
+ backgroundColor: Colors.error100,
+ },
+});
diff --git a/code/02-controlling-signup-login-screens/components/ui/Button.js b/code/02-controlling-signup-login-screens/components/ui/Button.js
new file mode 100644
index 00000000..9b4b231d
--- /dev/null
+++ b/code/02-controlling-signup-login-screens/components/ui/Button.js
@@ -0,0 +1,41 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Button({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default Button;
+
+const styles = StyleSheet.create({
+ button: {
+ borderRadius: 6,
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ backgroundColor: Colors.primary500,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: 'white',
+ fontSize: 16,
+ fontWeight: 'bold'
+ },
+});
diff --git a/code/02-controlling-signup-login-screens/components/ui/FlatButton.js b/code/02-controlling-signup-login-screens/components/ui/FlatButton.js
new file mode 100644
index 00000000..1b45e9ec
--- /dev/null
+++ b/code/02-controlling-signup-login-screens/components/ui/FlatButton.js
@@ -0,0 +1,32 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function FlatButton({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default FlatButton;
+
+const styles = StyleSheet.create({
+ button: {
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: Colors.primary100,
+ },
+});
diff --git a/code/02-controlling-signup-login-screens/components/ui/IconButton.js b/code/02-controlling-signup-login-screens/components/ui/IconButton.js
new file mode 100644
index 00000000..7f438db4
--- /dev/null
+++ b/code/02-controlling-signup-login-screens/components/ui/IconButton.js
@@ -0,0 +1,25 @@
+import { Pressable, StyleSheet } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+
+function IconButton({ icon, color, size, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+
+ );
+}
+
+export default IconButton;
+
+const styles = StyleSheet.create({
+ button: {
+ margin: 8,
+ borderRadius: 20,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+});
diff --git a/code/02-controlling-signup-login-screens/components/ui/LoadingOverlay.js b/code/02-controlling-signup-login-screens/components/ui/LoadingOverlay.js
new file mode 100644
index 00000000..99ae9528
--- /dev/null
+++ b/code/02-controlling-signup-login-screens/components/ui/LoadingOverlay.js
@@ -0,0 +1,25 @@
+import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
+
+function LoadingOverlay({ message }) {
+ return (
+
+ {message}
+
+
+ );
+}
+
+export default LoadingOverlay;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ message: {
+ fontSize: 16,
+ marginBottom: 12,
+ },
+});
diff --git a/code/02-controlling-signup-login-screens/constants/styles.js b/code/02-controlling-signup-login-screens/constants/styles.js
new file mode 100644
index 00000000..0ae92d20
--- /dev/null
+++ b/code/02-controlling-signup-login-screens/constants/styles.js
@@ -0,0 +1,7 @@
+export const Colors = {
+ primary100: '#f9beda',
+ primary500: '#c30b64',
+ primary800: '#610440',
+ error100: '#fcdcbf',
+ error500: '#f37c13',
+}
\ No newline at end of file
diff --git a/code/02-controlling-signup-login-screens/package.json b/code/02-controlling-signup-login-screens/package.json
new file mode 100644
index 00000000..bb3ea4d1
--- /dev/null
+++ b/code/02-controlling-signup-login-screens/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-navigation/native": "^6.0.8",
+ "@react-navigation/native-stack": "^6.5.0",
+ "expo": "~44.0.0",
+ "expo-status-bar": "~1.2.0",
+ "react": "17.0.1",
+ "react-dom": "17.0.1",
+ "react-native": "0.64.3",
+ "react-native-web": "0.17.1"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.12.9"
+ },
+ "private": true
+}
diff --git a/code/02-controlling-signup-login-screens/screens/LoginScreen.js b/code/02-controlling-signup-login-screens/screens/LoginScreen.js
new file mode 100644
index 00000000..9626a64a
--- /dev/null
+++ b/code/02-controlling-signup-login-screens/screens/LoginScreen.js
@@ -0,0 +1,7 @@
+import AuthContent from '../components/Auth/AuthContent';
+
+function LoginScreen() {
+ return ;
+}
+
+export default LoginScreen;
diff --git a/code/02-controlling-signup-login-screens/screens/SignupScreen.js b/code/02-controlling-signup-login-screens/screens/SignupScreen.js
new file mode 100644
index 00000000..12bdd477
--- /dev/null
+++ b/code/02-controlling-signup-login-screens/screens/SignupScreen.js
@@ -0,0 +1,7 @@
+import AuthContent from '../components/Auth/AuthContent';
+
+function SignupScreen() {
+ return ;
+}
+
+export default SignupScreen;
diff --git a/code/02-controlling-signup-login-screens/screens/WelcomeScreen.js b/code/02-controlling-signup-login-screens/screens/WelcomeScreen.js
new file mode 100644
index 00000000..b51d4283
--- /dev/null
+++ b/code/02-controlling-signup-login-screens/screens/WelcomeScreen.js
@@ -0,0 +1,26 @@
+import { StyleSheet, Text, View } from 'react-native';
+
+function WelcomeScreen() {
+ return (
+
+ Welcome!
+ You authenticated successfully!
+
+ );
+}
+
+export default WelcomeScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ title: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ },
+});
diff --git a/code/03-creating-new-users/App.js b/code/03-creating-new-users/App.js
new file mode 100644
index 00000000..3a3203b0
--- /dev/null
+++ b/code/03-creating-new-users/App.js
@@ -0,0 +1,57 @@
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { StatusBar } from 'expo-status-bar';
+
+import LoginScreen from './screens/LoginScreen';
+import SignupScreen from './screens/SignupScreen';
+import WelcomeScreen from './screens/WelcomeScreen';
+import { Colors } from './constants/styles';
+
+const Stack = createNativeStackNavigator();
+
+function AuthStack() {
+ return (
+
+
+
+
+ );
+}
+
+function AuthenticatedStack() {
+ return (
+
+
+
+ );
+}
+
+function Navigation() {
+ return (
+
+
+
+ );
+}
+
+export default function App() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/code/03-creating-new-users/app.json b/code/03-creating-new-users/app.json
new file mode 100644
index 00000000..9a1223e7
--- /dev/null
+++ b/code/03-creating-new-users/app.json
@@ -0,0 +1,32 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/code/03-creating-new-users/assets/adaptive-icon.png b/code/03-creating-new-users/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/code/03-creating-new-users/assets/adaptive-icon.png differ
diff --git a/code/03-creating-new-users/assets/favicon.png b/code/03-creating-new-users/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/code/03-creating-new-users/assets/favicon.png differ
diff --git a/code/03-creating-new-users/assets/icon.png b/code/03-creating-new-users/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/code/03-creating-new-users/assets/icon.png differ
diff --git a/code/03-creating-new-users/assets/splash.png b/code/03-creating-new-users/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/code/03-creating-new-users/assets/splash.png differ
diff --git a/code/03-creating-new-users/babel.config.js b/code/03-creating-new-users/babel.config.js
new file mode 100644
index 00000000..2900afe9
--- /dev/null
+++ b/code/03-creating-new-users/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/code/03-creating-new-users/components/Auth/AuthContent.js b/code/03-creating-new-users/components/Auth/AuthContent.js
new file mode 100644
index 00000000..8eb04b4f
--- /dev/null
+++ b/code/03-creating-new-users/components/Auth/AuthContent.js
@@ -0,0 +1,89 @@
+import { useState } from 'react';
+import { Alert, StyleSheet, View } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+
+import FlatButton from '../ui/FlatButton';
+import AuthForm from './AuthForm';
+import { Colors } from '../../constants/styles';
+
+function AuthContent({ isLogin, onAuthenticate }) {
+ const navigation = useNavigation();
+
+ const [credentialsInvalid, setCredentialsInvalid] = useState({
+ email: false,
+ password: false,
+ confirmEmail: false,
+ confirmPassword: false,
+ });
+
+ function switchAuthModeHandler() {
+ if (isLogin) {
+ navigation.replace('Signup');
+ } else {
+ navigation.replace('Login');
+ }
+ }
+
+ function submitHandler(credentials) {
+ let { email, confirmEmail, password, confirmPassword } = credentials;
+
+ email = email.trim();
+ password = password.trim();
+
+ const emailIsValid = email.includes('@');
+ const passwordIsValid = password.length > 6;
+ const emailsAreEqual = email === confirmEmail;
+ const passwordsAreEqual = password === confirmPassword;
+
+ if (
+ !emailIsValid ||
+ !passwordIsValid ||
+ (!isLogin && (!emailsAreEqual || !passwordsAreEqual))
+ ) {
+ Alert.alert('Invalid input', 'Please check your entered credentials.');
+ setCredentialsInvalid({
+ email: !emailIsValid,
+ confirmEmail: !emailIsValid || !emailsAreEqual,
+ password: !passwordIsValid,
+ confirmPassword: !passwordIsValid || !passwordsAreEqual,
+ });
+ return;
+ }
+ onAuthenticate({ email, password });
+ }
+
+ return (
+
+
+
+
+ {isLogin ? 'Create a new user' : 'Log in instead'}
+
+
+
+ );
+}
+
+export default AuthContent;
+
+const styles = StyleSheet.create({
+ authContent: {
+ marginTop: 64,
+ marginHorizontal: 32,
+ padding: 16,
+ borderRadius: 8,
+ backgroundColor: Colors.primary800,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.35,
+ shadowRadius: 4,
+ },
+ buttons: {
+ marginTop: 8,
+ },
+});
diff --git a/code/03-creating-new-users/components/Auth/AuthForm.js b/code/03-creating-new-users/components/Auth/AuthForm.js
new file mode 100644
index 00000000..cf4a7a81
--- /dev/null
+++ b/code/03-creating-new-users/components/Auth/AuthForm.js
@@ -0,0 +1,100 @@
+import { useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import Button from '../ui/Button';
+import Input from './Input';
+
+function AuthForm({ isLogin, onSubmit, credentialsInvalid }) {
+ const [enteredEmail, setEnteredEmail] = useState('');
+ const [enteredConfirmEmail, setEnteredConfirmEmail] = useState('');
+ const [enteredPassword, setEnteredPassword] = useState('');
+ const [enteredConfirmPassword, setEnteredConfirmPassword] = useState('');
+
+ const {
+ email: emailIsInvalid,
+ confirmEmail: emailsDontMatch,
+ password: passwordIsInvalid,
+ confirmPassword: passwordsDontMatch,
+ } = credentialsInvalid;
+
+ function updateInputValueHandler(inputType, enteredValue) {
+ switch (inputType) {
+ case 'email':
+ setEnteredEmail(enteredValue);
+ break;
+ case 'confirmEmail':
+ setEnteredConfirmEmail(enteredValue);
+ break;
+ case 'password':
+ setEnteredPassword(enteredValue);
+ break;
+ case 'confirmPassword':
+ setEnteredConfirmPassword(enteredValue);
+ break;
+ }
+ }
+
+ function submitHandler() {
+ onSubmit({
+ email: enteredEmail,
+ confirmEmail: enteredConfirmEmail,
+ password: enteredPassword,
+ confirmPassword: enteredConfirmPassword,
+ });
+ }
+
+ return (
+
+
+
+ {!isLogin && (
+
+ )}
+
+ {!isLogin && (
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default AuthForm;
+
+const styles = StyleSheet.create({
+ buttons: {
+ marginTop: 12,
+ },
+});
diff --git a/code/03-creating-new-users/components/Auth/Input.js b/code/03-creating-new-users/components/Auth/Input.js
new file mode 100644
index 00000000..3aa0d5dd
--- /dev/null
+++ b/code/03-creating-new-users/components/Auth/Input.js
@@ -0,0 +1,54 @@
+import { View, Text, TextInput, StyleSheet } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Input({
+ label,
+ keyboardType,
+ secure,
+ onUpdateValue,
+ value,
+ isInvalid,
+}) {
+ return (
+
+
+ {label}
+
+
+
+ );
+}
+
+export default Input;
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ marginVertical: 8,
+ },
+ label: {
+ color: 'white',
+ marginBottom: 4,
+ },
+ labelInvalid: {
+ color: Colors.error500,
+ },
+ input: {
+ paddingVertical: 8,
+ paddingHorizontal: 6,
+ backgroundColor: Colors.primary100,
+ borderRadius: 4,
+ fontSize: 16,
+ },
+ inputInvalid: {
+ backgroundColor: Colors.error100,
+ },
+});
diff --git a/code/03-creating-new-users/components/ui/Button.js b/code/03-creating-new-users/components/ui/Button.js
new file mode 100644
index 00000000..9b4b231d
--- /dev/null
+++ b/code/03-creating-new-users/components/ui/Button.js
@@ -0,0 +1,41 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Button({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default Button;
+
+const styles = StyleSheet.create({
+ button: {
+ borderRadius: 6,
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ backgroundColor: Colors.primary500,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: 'white',
+ fontSize: 16,
+ fontWeight: 'bold'
+ },
+});
diff --git a/code/03-creating-new-users/components/ui/FlatButton.js b/code/03-creating-new-users/components/ui/FlatButton.js
new file mode 100644
index 00000000..1b45e9ec
--- /dev/null
+++ b/code/03-creating-new-users/components/ui/FlatButton.js
@@ -0,0 +1,32 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function FlatButton({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default FlatButton;
+
+const styles = StyleSheet.create({
+ button: {
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: Colors.primary100,
+ },
+});
diff --git a/code/03-creating-new-users/components/ui/IconButton.js b/code/03-creating-new-users/components/ui/IconButton.js
new file mode 100644
index 00000000..7f438db4
--- /dev/null
+++ b/code/03-creating-new-users/components/ui/IconButton.js
@@ -0,0 +1,25 @@
+import { Pressable, StyleSheet } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+
+function IconButton({ icon, color, size, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+
+ );
+}
+
+export default IconButton;
+
+const styles = StyleSheet.create({
+ button: {
+ margin: 8,
+ borderRadius: 20,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+});
diff --git a/code/03-creating-new-users/components/ui/LoadingOverlay.js b/code/03-creating-new-users/components/ui/LoadingOverlay.js
new file mode 100644
index 00000000..99ae9528
--- /dev/null
+++ b/code/03-creating-new-users/components/ui/LoadingOverlay.js
@@ -0,0 +1,25 @@
+import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
+
+function LoadingOverlay({ message }) {
+ return (
+
+ {message}
+
+
+ );
+}
+
+export default LoadingOverlay;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ message: {
+ fontSize: 16,
+ marginBottom: 12,
+ },
+});
diff --git a/code/03-creating-new-users/constants/styles.js b/code/03-creating-new-users/constants/styles.js
new file mode 100644
index 00000000..0ae92d20
--- /dev/null
+++ b/code/03-creating-new-users/constants/styles.js
@@ -0,0 +1,7 @@
+export const Colors = {
+ primary100: '#f9beda',
+ primary500: '#c30b64',
+ primary800: '#610440',
+ error100: '#fcdcbf',
+ error500: '#f37c13',
+}
\ No newline at end of file
diff --git a/code/03-creating-new-users/package.json b/code/03-creating-new-users/package.json
new file mode 100644
index 00000000..85a602c5
--- /dev/null
+++ b/code/03-creating-new-users/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-navigation/native": "^6.0.8",
+ "@react-navigation/native-stack": "^6.5.0",
+ "axios": "^0.26.0",
+ "expo": "~44.0.0",
+ "expo-status-bar": "~1.2.0",
+ "react": "17.0.1",
+ "react-dom": "17.0.1",
+ "react-native": "0.64.3",
+ "react-native-web": "0.17.1"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.12.9"
+ },
+ "private": true
+}
diff --git a/code/03-creating-new-users/screens/LoginScreen.js b/code/03-creating-new-users/screens/LoginScreen.js
new file mode 100644
index 00000000..9626a64a
--- /dev/null
+++ b/code/03-creating-new-users/screens/LoginScreen.js
@@ -0,0 +1,7 @@
+import AuthContent from '../components/Auth/AuthContent';
+
+function LoginScreen() {
+ return ;
+}
+
+export default LoginScreen;
diff --git a/code/03-creating-new-users/screens/SignupScreen.js b/code/03-creating-new-users/screens/SignupScreen.js
new file mode 100644
index 00000000..15d1e542
--- /dev/null
+++ b/code/03-creating-new-users/screens/SignupScreen.js
@@ -0,0 +1,23 @@
+import { useState } from 'react';
+
+import AuthContent from '../components/Auth/AuthContent';
+import LoadingOverlay from '../components/ui/LoadingOverlay';
+import { createUser } from '../util/auth';
+
+function SignupScreen() {
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+
+ async function signupHandler({ email, password }) {
+ setIsAuthenticating(true);
+ await createUser(email, password);
+ setIsAuthenticating(false);
+ }
+
+ if (isAuthenticating) {
+ return ;
+ }
+
+ return ;
+}
+
+export default SignupScreen;
diff --git a/code/03-creating-new-users/screens/WelcomeScreen.js b/code/03-creating-new-users/screens/WelcomeScreen.js
new file mode 100644
index 00000000..b51d4283
--- /dev/null
+++ b/code/03-creating-new-users/screens/WelcomeScreen.js
@@ -0,0 +1,26 @@
+import { StyleSheet, Text, View } from 'react-native';
+
+function WelcomeScreen() {
+ return (
+
+ Welcome!
+ You authenticated successfully!
+
+ );
+}
+
+export default WelcomeScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ title: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ },
+});
diff --git a/code/03-creating-new-users/util/auth.js b/code/03-creating-new-users/util/auth.js
new file mode 100644
index 00000000..41374e69
--- /dev/null
+++ b/code/03-creating-new-users/util/auth.js
@@ -0,0 +1,14 @@
+import axios from 'axios';
+
+const API_KEY = 'AIzaSyDCYasArcOwcALFhIj2szug5aD2PgUQu1E'
+
+export async function createUser(email, password) {
+ const response = await axios.post(
+ 'https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=' + API_KEY,
+ {
+ email: email,
+ password: password,
+ returnSecureToken: true
+ }
+ );
+}
diff --git a/code/04-logging-users-in/App.js b/code/04-logging-users-in/App.js
new file mode 100644
index 00000000..3a3203b0
--- /dev/null
+++ b/code/04-logging-users-in/App.js
@@ -0,0 +1,57 @@
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { StatusBar } from 'expo-status-bar';
+
+import LoginScreen from './screens/LoginScreen';
+import SignupScreen from './screens/SignupScreen';
+import WelcomeScreen from './screens/WelcomeScreen';
+import { Colors } from './constants/styles';
+
+const Stack = createNativeStackNavigator();
+
+function AuthStack() {
+ return (
+
+
+
+
+ );
+}
+
+function AuthenticatedStack() {
+ return (
+
+
+
+ );
+}
+
+function Navigation() {
+ return (
+
+
+
+ );
+}
+
+export default function App() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/code/04-logging-users-in/app.json b/code/04-logging-users-in/app.json
new file mode 100644
index 00000000..9a1223e7
--- /dev/null
+++ b/code/04-logging-users-in/app.json
@@ -0,0 +1,32 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/code/04-logging-users-in/assets/adaptive-icon.png b/code/04-logging-users-in/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/code/04-logging-users-in/assets/adaptive-icon.png differ
diff --git a/code/04-logging-users-in/assets/favicon.png b/code/04-logging-users-in/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/code/04-logging-users-in/assets/favicon.png differ
diff --git a/code/04-logging-users-in/assets/icon.png b/code/04-logging-users-in/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/code/04-logging-users-in/assets/icon.png differ
diff --git a/code/04-logging-users-in/assets/splash.png b/code/04-logging-users-in/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/code/04-logging-users-in/assets/splash.png differ
diff --git a/code/04-logging-users-in/babel.config.js b/code/04-logging-users-in/babel.config.js
new file mode 100644
index 00000000..2900afe9
--- /dev/null
+++ b/code/04-logging-users-in/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/code/04-logging-users-in/components/Auth/AuthContent.js b/code/04-logging-users-in/components/Auth/AuthContent.js
new file mode 100644
index 00000000..8eb04b4f
--- /dev/null
+++ b/code/04-logging-users-in/components/Auth/AuthContent.js
@@ -0,0 +1,89 @@
+import { useState } from 'react';
+import { Alert, StyleSheet, View } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+
+import FlatButton from '../ui/FlatButton';
+import AuthForm from './AuthForm';
+import { Colors } from '../../constants/styles';
+
+function AuthContent({ isLogin, onAuthenticate }) {
+ const navigation = useNavigation();
+
+ const [credentialsInvalid, setCredentialsInvalid] = useState({
+ email: false,
+ password: false,
+ confirmEmail: false,
+ confirmPassword: false,
+ });
+
+ function switchAuthModeHandler() {
+ if (isLogin) {
+ navigation.replace('Signup');
+ } else {
+ navigation.replace('Login');
+ }
+ }
+
+ function submitHandler(credentials) {
+ let { email, confirmEmail, password, confirmPassword } = credentials;
+
+ email = email.trim();
+ password = password.trim();
+
+ const emailIsValid = email.includes('@');
+ const passwordIsValid = password.length > 6;
+ const emailsAreEqual = email === confirmEmail;
+ const passwordsAreEqual = password === confirmPassword;
+
+ if (
+ !emailIsValid ||
+ !passwordIsValid ||
+ (!isLogin && (!emailsAreEqual || !passwordsAreEqual))
+ ) {
+ Alert.alert('Invalid input', 'Please check your entered credentials.');
+ setCredentialsInvalid({
+ email: !emailIsValid,
+ confirmEmail: !emailIsValid || !emailsAreEqual,
+ password: !passwordIsValid,
+ confirmPassword: !passwordIsValid || !passwordsAreEqual,
+ });
+ return;
+ }
+ onAuthenticate({ email, password });
+ }
+
+ return (
+
+
+
+
+ {isLogin ? 'Create a new user' : 'Log in instead'}
+
+
+
+ );
+}
+
+export default AuthContent;
+
+const styles = StyleSheet.create({
+ authContent: {
+ marginTop: 64,
+ marginHorizontal: 32,
+ padding: 16,
+ borderRadius: 8,
+ backgroundColor: Colors.primary800,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.35,
+ shadowRadius: 4,
+ },
+ buttons: {
+ marginTop: 8,
+ },
+});
diff --git a/code/04-logging-users-in/components/Auth/AuthForm.js b/code/04-logging-users-in/components/Auth/AuthForm.js
new file mode 100644
index 00000000..cf4a7a81
--- /dev/null
+++ b/code/04-logging-users-in/components/Auth/AuthForm.js
@@ -0,0 +1,100 @@
+import { useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import Button from '../ui/Button';
+import Input from './Input';
+
+function AuthForm({ isLogin, onSubmit, credentialsInvalid }) {
+ const [enteredEmail, setEnteredEmail] = useState('');
+ const [enteredConfirmEmail, setEnteredConfirmEmail] = useState('');
+ const [enteredPassword, setEnteredPassword] = useState('');
+ const [enteredConfirmPassword, setEnteredConfirmPassword] = useState('');
+
+ const {
+ email: emailIsInvalid,
+ confirmEmail: emailsDontMatch,
+ password: passwordIsInvalid,
+ confirmPassword: passwordsDontMatch,
+ } = credentialsInvalid;
+
+ function updateInputValueHandler(inputType, enteredValue) {
+ switch (inputType) {
+ case 'email':
+ setEnteredEmail(enteredValue);
+ break;
+ case 'confirmEmail':
+ setEnteredConfirmEmail(enteredValue);
+ break;
+ case 'password':
+ setEnteredPassword(enteredValue);
+ break;
+ case 'confirmPassword':
+ setEnteredConfirmPassword(enteredValue);
+ break;
+ }
+ }
+
+ function submitHandler() {
+ onSubmit({
+ email: enteredEmail,
+ confirmEmail: enteredConfirmEmail,
+ password: enteredPassword,
+ confirmPassword: enteredConfirmPassword,
+ });
+ }
+
+ return (
+
+
+
+ {!isLogin && (
+
+ )}
+
+ {!isLogin && (
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default AuthForm;
+
+const styles = StyleSheet.create({
+ buttons: {
+ marginTop: 12,
+ },
+});
diff --git a/code/04-logging-users-in/components/Auth/Input.js b/code/04-logging-users-in/components/Auth/Input.js
new file mode 100644
index 00000000..3aa0d5dd
--- /dev/null
+++ b/code/04-logging-users-in/components/Auth/Input.js
@@ -0,0 +1,54 @@
+import { View, Text, TextInput, StyleSheet } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Input({
+ label,
+ keyboardType,
+ secure,
+ onUpdateValue,
+ value,
+ isInvalid,
+}) {
+ return (
+
+
+ {label}
+
+
+
+ );
+}
+
+export default Input;
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ marginVertical: 8,
+ },
+ label: {
+ color: 'white',
+ marginBottom: 4,
+ },
+ labelInvalid: {
+ color: Colors.error500,
+ },
+ input: {
+ paddingVertical: 8,
+ paddingHorizontal: 6,
+ backgroundColor: Colors.primary100,
+ borderRadius: 4,
+ fontSize: 16,
+ },
+ inputInvalid: {
+ backgroundColor: Colors.error100,
+ },
+});
diff --git a/code/04-logging-users-in/components/ui/Button.js b/code/04-logging-users-in/components/ui/Button.js
new file mode 100644
index 00000000..9b4b231d
--- /dev/null
+++ b/code/04-logging-users-in/components/ui/Button.js
@@ -0,0 +1,41 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Button({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default Button;
+
+const styles = StyleSheet.create({
+ button: {
+ borderRadius: 6,
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ backgroundColor: Colors.primary500,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: 'white',
+ fontSize: 16,
+ fontWeight: 'bold'
+ },
+});
diff --git a/code/04-logging-users-in/components/ui/FlatButton.js b/code/04-logging-users-in/components/ui/FlatButton.js
new file mode 100644
index 00000000..1b45e9ec
--- /dev/null
+++ b/code/04-logging-users-in/components/ui/FlatButton.js
@@ -0,0 +1,32 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function FlatButton({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default FlatButton;
+
+const styles = StyleSheet.create({
+ button: {
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: Colors.primary100,
+ },
+});
diff --git a/code/04-logging-users-in/components/ui/IconButton.js b/code/04-logging-users-in/components/ui/IconButton.js
new file mode 100644
index 00000000..7f438db4
--- /dev/null
+++ b/code/04-logging-users-in/components/ui/IconButton.js
@@ -0,0 +1,25 @@
+import { Pressable, StyleSheet } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+
+function IconButton({ icon, color, size, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+
+ );
+}
+
+export default IconButton;
+
+const styles = StyleSheet.create({
+ button: {
+ margin: 8,
+ borderRadius: 20,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+});
diff --git a/code/04-logging-users-in/components/ui/LoadingOverlay.js b/code/04-logging-users-in/components/ui/LoadingOverlay.js
new file mode 100644
index 00000000..99ae9528
--- /dev/null
+++ b/code/04-logging-users-in/components/ui/LoadingOverlay.js
@@ -0,0 +1,25 @@
+import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
+
+function LoadingOverlay({ message }) {
+ return (
+
+ {message}
+
+
+ );
+}
+
+export default LoadingOverlay;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ message: {
+ fontSize: 16,
+ marginBottom: 12,
+ },
+});
diff --git a/code/04-logging-users-in/constants/styles.js b/code/04-logging-users-in/constants/styles.js
new file mode 100644
index 00000000..0ae92d20
--- /dev/null
+++ b/code/04-logging-users-in/constants/styles.js
@@ -0,0 +1,7 @@
+export const Colors = {
+ primary100: '#f9beda',
+ primary500: '#c30b64',
+ primary800: '#610440',
+ error100: '#fcdcbf',
+ error500: '#f37c13',
+}
\ No newline at end of file
diff --git a/code/04-logging-users-in/package.json b/code/04-logging-users-in/package.json
new file mode 100644
index 00000000..85a602c5
--- /dev/null
+++ b/code/04-logging-users-in/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-navigation/native": "^6.0.8",
+ "@react-navigation/native-stack": "^6.5.0",
+ "axios": "^0.26.0",
+ "expo": "~44.0.0",
+ "expo-status-bar": "~1.2.0",
+ "react": "17.0.1",
+ "react-dom": "17.0.1",
+ "react-native": "0.64.3",
+ "react-native-web": "0.17.1"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.12.9"
+ },
+ "private": true
+}
diff --git a/code/04-logging-users-in/screens/LoginScreen.js b/code/04-logging-users-in/screens/LoginScreen.js
new file mode 100644
index 00000000..aecd267c
--- /dev/null
+++ b/code/04-logging-users-in/screens/LoginScreen.js
@@ -0,0 +1,23 @@
+import { useState } from 'react';
+
+import AuthContent from '../components/Auth/AuthContent';
+import LoadingOverlay from '../components/ui/LoadingOverlay';
+import { login } from '../util/auth';
+
+function LoginScreen() {
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+
+ async function loginHandler({ email, password }) {
+ setIsAuthenticating(true);
+ await login(email, password);
+ setIsAuthenticating(false);
+ }
+
+ if (isAuthenticating) {
+ return ;
+ }
+
+ return ;
+}
+
+export default LoginScreen;
diff --git a/code/04-logging-users-in/screens/SignupScreen.js b/code/04-logging-users-in/screens/SignupScreen.js
new file mode 100644
index 00000000..15d1e542
--- /dev/null
+++ b/code/04-logging-users-in/screens/SignupScreen.js
@@ -0,0 +1,23 @@
+import { useState } from 'react';
+
+import AuthContent from '../components/Auth/AuthContent';
+import LoadingOverlay from '../components/ui/LoadingOverlay';
+import { createUser } from '../util/auth';
+
+function SignupScreen() {
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+
+ async function signupHandler({ email, password }) {
+ setIsAuthenticating(true);
+ await createUser(email, password);
+ setIsAuthenticating(false);
+ }
+
+ if (isAuthenticating) {
+ return ;
+ }
+
+ return ;
+}
+
+export default SignupScreen;
diff --git a/code/04-logging-users-in/screens/WelcomeScreen.js b/code/04-logging-users-in/screens/WelcomeScreen.js
new file mode 100644
index 00000000..b51d4283
--- /dev/null
+++ b/code/04-logging-users-in/screens/WelcomeScreen.js
@@ -0,0 +1,26 @@
+import { StyleSheet, Text, View } from 'react-native';
+
+function WelcomeScreen() {
+ return (
+
+ Welcome!
+ You authenticated successfully!
+
+ );
+}
+
+export default WelcomeScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ title: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ },
+});
diff --git a/code/04-logging-users-in/util/auth.js b/code/04-logging-users-in/util/auth.js
new file mode 100644
index 00000000..eec60719
--- /dev/null
+++ b/code/04-logging-users-in/util/auth.js
@@ -0,0 +1,23 @@
+import axios from 'axios';
+
+const API_KEY = 'AIzaSyDCYasArcOwcALFhIj2szug5aD2PgUQu1E';
+
+async function authenticate(mode, email, password) {
+ const url = `https://identitytoolkit.googleapis.com/v1/accounts:${mode}?key=${API_KEY}`;
+
+ const response = await axios.post(url, {
+ email: email,
+ password: password,
+ returnSecureToken: true,
+ });
+
+ console.log(response.data);
+}
+
+export async function createUser(email, password) {
+ await authenticate('signUp', email, password);
+}
+
+export async function login(email, password) {
+ await authenticate('signInWithPassword', email, password);
+}
\ No newline at end of file
diff --git a/code/05-auth-error-handling/App.js b/code/05-auth-error-handling/App.js
new file mode 100644
index 00000000..3a3203b0
--- /dev/null
+++ b/code/05-auth-error-handling/App.js
@@ -0,0 +1,57 @@
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { StatusBar } from 'expo-status-bar';
+
+import LoginScreen from './screens/LoginScreen';
+import SignupScreen from './screens/SignupScreen';
+import WelcomeScreen from './screens/WelcomeScreen';
+import { Colors } from './constants/styles';
+
+const Stack = createNativeStackNavigator();
+
+function AuthStack() {
+ return (
+
+
+
+
+ );
+}
+
+function AuthenticatedStack() {
+ return (
+
+
+
+ );
+}
+
+function Navigation() {
+ return (
+
+
+
+ );
+}
+
+export default function App() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/code/05-auth-error-handling/app.json b/code/05-auth-error-handling/app.json
new file mode 100644
index 00000000..9a1223e7
--- /dev/null
+++ b/code/05-auth-error-handling/app.json
@@ -0,0 +1,32 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/code/05-auth-error-handling/assets/adaptive-icon.png b/code/05-auth-error-handling/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/code/05-auth-error-handling/assets/adaptive-icon.png differ
diff --git a/code/05-auth-error-handling/assets/favicon.png b/code/05-auth-error-handling/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/code/05-auth-error-handling/assets/favicon.png differ
diff --git a/code/05-auth-error-handling/assets/icon.png b/code/05-auth-error-handling/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/code/05-auth-error-handling/assets/icon.png differ
diff --git a/code/05-auth-error-handling/assets/splash.png b/code/05-auth-error-handling/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/code/05-auth-error-handling/assets/splash.png differ
diff --git a/code/05-auth-error-handling/babel.config.js b/code/05-auth-error-handling/babel.config.js
new file mode 100644
index 00000000..2900afe9
--- /dev/null
+++ b/code/05-auth-error-handling/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/code/05-auth-error-handling/components/Auth/AuthContent.js b/code/05-auth-error-handling/components/Auth/AuthContent.js
new file mode 100644
index 00000000..8eb04b4f
--- /dev/null
+++ b/code/05-auth-error-handling/components/Auth/AuthContent.js
@@ -0,0 +1,89 @@
+import { useState } from 'react';
+import { Alert, StyleSheet, View } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+
+import FlatButton from '../ui/FlatButton';
+import AuthForm from './AuthForm';
+import { Colors } from '../../constants/styles';
+
+function AuthContent({ isLogin, onAuthenticate }) {
+ const navigation = useNavigation();
+
+ const [credentialsInvalid, setCredentialsInvalid] = useState({
+ email: false,
+ password: false,
+ confirmEmail: false,
+ confirmPassword: false,
+ });
+
+ function switchAuthModeHandler() {
+ if (isLogin) {
+ navigation.replace('Signup');
+ } else {
+ navigation.replace('Login');
+ }
+ }
+
+ function submitHandler(credentials) {
+ let { email, confirmEmail, password, confirmPassword } = credentials;
+
+ email = email.trim();
+ password = password.trim();
+
+ const emailIsValid = email.includes('@');
+ const passwordIsValid = password.length > 6;
+ const emailsAreEqual = email === confirmEmail;
+ const passwordsAreEqual = password === confirmPassword;
+
+ if (
+ !emailIsValid ||
+ !passwordIsValid ||
+ (!isLogin && (!emailsAreEqual || !passwordsAreEqual))
+ ) {
+ Alert.alert('Invalid input', 'Please check your entered credentials.');
+ setCredentialsInvalid({
+ email: !emailIsValid,
+ confirmEmail: !emailIsValid || !emailsAreEqual,
+ password: !passwordIsValid,
+ confirmPassword: !passwordIsValid || !passwordsAreEqual,
+ });
+ return;
+ }
+ onAuthenticate({ email, password });
+ }
+
+ return (
+
+
+
+
+ {isLogin ? 'Create a new user' : 'Log in instead'}
+
+
+
+ );
+}
+
+export default AuthContent;
+
+const styles = StyleSheet.create({
+ authContent: {
+ marginTop: 64,
+ marginHorizontal: 32,
+ padding: 16,
+ borderRadius: 8,
+ backgroundColor: Colors.primary800,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.35,
+ shadowRadius: 4,
+ },
+ buttons: {
+ marginTop: 8,
+ },
+});
diff --git a/code/05-auth-error-handling/components/Auth/AuthForm.js b/code/05-auth-error-handling/components/Auth/AuthForm.js
new file mode 100644
index 00000000..cf4a7a81
--- /dev/null
+++ b/code/05-auth-error-handling/components/Auth/AuthForm.js
@@ -0,0 +1,100 @@
+import { useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import Button from '../ui/Button';
+import Input from './Input';
+
+function AuthForm({ isLogin, onSubmit, credentialsInvalid }) {
+ const [enteredEmail, setEnteredEmail] = useState('');
+ const [enteredConfirmEmail, setEnteredConfirmEmail] = useState('');
+ const [enteredPassword, setEnteredPassword] = useState('');
+ const [enteredConfirmPassword, setEnteredConfirmPassword] = useState('');
+
+ const {
+ email: emailIsInvalid,
+ confirmEmail: emailsDontMatch,
+ password: passwordIsInvalid,
+ confirmPassword: passwordsDontMatch,
+ } = credentialsInvalid;
+
+ function updateInputValueHandler(inputType, enteredValue) {
+ switch (inputType) {
+ case 'email':
+ setEnteredEmail(enteredValue);
+ break;
+ case 'confirmEmail':
+ setEnteredConfirmEmail(enteredValue);
+ break;
+ case 'password':
+ setEnteredPassword(enteredValue);
+ break;
+ case 'confirmPassword':
+ setEnteredConfirmPassword(enteredValue);
+ break;
+ }
+ }
+
+ function submitHandler() {
+ onSubmit({
+ email: enteredEmail,
+ confirmEmail: enteredConfirmEmail,
+ password: enteredPassword,
+ confirmPassword: enteredConfirmPassword,
+ });
+ }
+
+ return (
+
+
+
+ {!isLogin && (
+
+ )}
+
+ {!isLogin && (
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default AuthForm;
+
+const styles = StyleSheet.create({
+ buttons: {
+ marginTop: 12,
+ },
+});
diff --git a/code/05-auth-error-handling/components/Auth/Input.js b/code/05-auth-error-handling/components/Auth/Input.js
new file mode 100644
index 00000000..3aa0d5dd
--- /dev/null
+++ b/code/05-auth-error-handling/components/Auth/Input.js
@@ -0,0 +1,54 @@
+import { View, Text, TextInput, StyleSheet } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Input({
+ label,
+ keyboardType,
+ secure,
+ onUpdateValue,
+ value,
+ isInvalid,
+}) {
+ return (
+
+
+ {label}
+
+
+
+ );
+}
+
+export default Input;
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ marginVertical: 8,
+ },
+ label: {
+ color: 'white',
+ marginBottom: 4,
+ },
+ labelInvalid: {
+ color: Colors.error500,
+ },
+ input: {
+ paddingVertical: 8,
+ paddingHorizontal: 6,
+ backgroundColor: Colors.primary100,
+ borderRadius: 4,
+ fontSize: 16,
+ },
+ inputInvalid: {
+ backgroundColor: Colors.error100,
+ },
+});
diff --git a/code/05-auth-error-handling/components/ui/Button.js b/code/05-auth-error-handling/components/ui/Button.js
new file mode 100644
index 00000000..9b4b231d
--- /dev/null
+++ b/code/05-auth-error-handling/components/ui/Button.js
@@ -0,0 +1,41 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Button({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default Button;
+
+const styles = StyleSheet.create({
+ button: {
+ borderRadius: 6,
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ backgroundColor: Colors.primary500,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: 'white',
+ fontSize: 16,
+ fontWeight: 'bold'
+ },
+});
diff --git a/code/05-auth-error-handling/components/ui/FlatButton.js b/code/05-auth-error-handling/components/ui/FlatButton.js
new file mode 100644
index 00000000..1b45e9ec
--- /dev/null
+++ b/code/05-auth-error-handling/components/ui/FlatButton.js
@@ -0,0 +1,32 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function FlatButton({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default FlatButton;
+
+const styles = StyleSheet.create({
+ button: {
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: Colors.primary100,
+ },
+});
diff --git a/code/05-auth-error-handling/components/ui/IconButton.js b/code/05-auth-error-handling/components/ui/IconButton.js
new file mode 100644
index 00000000..7f438db4
--- /dev/null
+++ b/code/05-auth-error-handling/components/ui/IconButton.js
@@ -0,0 +1,25 @@
+import { Pressable, StyleSheet } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+
+function IconButton({ icon, color, size, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+
+ );
+}
+
+export default IconButton;
+
+const styles = StyleSheet.create({
+ button: {
+ margin: 8,
+ borderRadius: 20,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+});
diff --git a/code/05-auth-error-handling/components/ui/LoadingOverlay.js b/code/05-auth-error-handling/components/ui/LoadingOverlay.js
new file mode 100644
index 00000000..99ae9528
--- /dev/null
+++ b/code/05-auth-error-handling/components/ui/LoadingOverlay.js
@@ -0,0 +1,25 @@
+import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
+
+function LoadingOverlay({ message }) {
+ return (
+
+ {message}
+
+
+ );
+}
+
+export default LoadingOverlay;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ message: {
+ fontSize: 16,
+ marginBottom: 12,
+ },
+});
diff --git a/code/05-auth-error-handling/constants/styles.js b/code/05-auth-error-handling/constants/styles.js
new file mode 100644
index 00000000..0ae92d20
--- /dev/null
+++ b/code/05-auth-error-handling/constants/styles.js
@@ -0,0 +1,7 @@
+export const Colors = {
+ primary100: '#f9beda',
+ primary500: '#c30b64',
+ primary800: '#610440',
+ error100: '#fcdcbf',
+ error500: '#f37c13',
+}
\ No newline at end of file
diff --git a/code/05-auth-error-handling/package.json b/code/05-auth-error-handling/package.json
new file mode 100644
index 00000000..85a602c5
--- /dev/null
+++ b/code/05-auth-error-handling/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-navigation/native": "^6.0.8",
+ "@react-navigation/native-stack": "^6.5.0",
+ "axios": "^0.26.0",
+ "expo": "~44.0.0",
+ "expo-status-bar": "~1.2.0",
+ "react": "17.0.1",
+ "react-dom": "17.0.1",
+ "react-native": "0.64.3",
+ "react-native-web": "0.17.1"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.12.9"
+ },
+ "private": true
+}
diff --git a/code/05-auth-error-handling/screens/LoginScreen.js b/code/05-auth-error-handling/screens/LoginScreen.js
new file mode 100644
index 00000000..d6723da9
--- /dev/null
+++ b/code/05-auth-error-handling/screens/LoginScreen.js
@@ -0,0 +1,31 @@
+import { useState } from 'react';
+import { Alert } from 'react-native';
+
+import AuthContent from '../components/Auth/AuthContent';
+import LoadingOverlay from '../components/ui/LoadingOverlay';
+import { login } from '../util/auth';
+
+function LoginScreen() {
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+
+ async function loginHandler({ email, password }) {
+ setIsAuthenticating(true);
+ try {
+ await login(email, password);
+ } catch (error) {
+ Alert.alert(
+ 'Authentication failed!',
+ 'Could not log you in. Please check your credentials or try again later!'
+ );
+ }
+ setIsAuthenticating(false);
+ }
+
+ if (isAuthenticating) {
+ return ;
+ }
+
+ return ;
+}
+
+export default LoginScreen;
diff --git a/code/05-auth-error-handling/screens/SignupScreen.js b/code/05-auth-error-handling/screens/SignupScreen.js
new file mode 100644
index 00000000..e10d16aa
--- /dev/null
+++ b/code/05-auth-error-handling/screens/SignupScreen.js
@@ -0,0 +1,31 @@
+import { useState } from 'react';
+import { Alert } from 'react-native';
+
+import AuthContent from '../components/Auth/AuthContent';
+import LoadingOverlay from '../components/ui/LoadingOverlay';
+import { createUser } from '../util/auth';
+
+function SignupScreen() {
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+
+ async function signupHandler({ email, password }) {
+ setIsAuthenticating(true);
+ try {
+ await createUser(email, password);
+ } catch (error) {
+ Alert.alert(
+ 'Authentication failed',
+ 'Could not create user, please check your input and try again later.'
+ );
+ }
+ setIsAuthenticating(false);
+ }
+
+ if (isAuthenticating) {
+ return ;
+ }
+
+ return ;
+}
+
+export default SignupScreen;
diff --git a/code/05-auth-error-handling/screens/WelcomeScreen.js b/code/05-auth-error-handling/screens/WelcomeScreen.js
new file mode 100644
index 00000000..b51d4283
--- /dev/null
+++ b/code/05-auth-error-handling/screens/WelcomeScreen.js
@@ -0,0 +1,26 @@
+import { StyleSheet, Text, View } from 'react-native';
+
+function WelcomeScreen() {
+ return (
+
+ Welcome!
+ You authenticated successfully!
+
+ );
+}
+
+export default WelcomeScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ title: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ },
+});
diff --git a/code/05-auth-error-handling/util/auth.js b/code/05-auth-error-handling/util/auth.js
new file mode 100644
index 00000000..eec60719
--- /dev/null
+++ b/code/05-auth-error-handling/util/auth.js
@@ -0,0 +1,23 @@
+import axios from 'axios';
+
+const API_KEY = 'AIzaSyDCYasArcOwcALFhIj2szug5aD2PgUQu1E';
+
+async function authenticate(mode, email, password) {
+ const url = `https://identitytoolkit.googleapis.com/v1/accounts:${mode}?key=${API_KEY}`;
+
+ const response = await axios.post(url, {
+ email: email,
+ password: password,
+ returnSecureToken: true,
+ });
+
+ console.log(response.data);
+}
+
+export async function createUser(email, password) {
+ await authenticate('signUp', email, password);
+}
+
+export async function login(email, password) {
+ await authenticate('signInWithPassword', email, password);
+}
\ No newline at end of file
diff --git a/code/06-storing-managing-the-user-auth-state/App.js b/code/06-storing-managing-the-user-auth-state/App.js
new file mode 100644
index 00000000..c0198b03
--- /dev/null
+++ b/code/06-storing-managing-the-user-auth-state/App.js
@@ -0,0 +1,60 @@
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { StatusBar } from 'expo-status-bar';
+
+import LoginScreen from './screens/LoginScreen';
+import SignupScreen from './screens/SignupScreen';
+import WelcomeScreen from './screens/WelcomeScreen';
+import { Colors } from './constants/styles';
+import AuthContextProvider from './store/auth-context';
+
+const Stack = createNativeStackNavigator();
+
+function AuthStack() {
+ return (
+
+
+
+
+ );
+}
+
+function AuthenticatedStack() {
+ return (
+
+
+
+ );
+}
+
+function Navigation() {
+ return (
+
+
+
+
+
+ );
+}
+
+export default function App() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/code/06-storing-managing-the-user-auth-state/app.json b/code/06-storing-managing-the-user-auth-state/app.json
new file mode 100644
index 00000000..9a1223e7
--- /dev/null
+++ b/code/06-storing-managing-the-user-auth-state/app.json
@@ -0,0 +1,32 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/code/06-storing-managing-the-user-auth-state/assets/adaptive-icon.png b/code/06-storing-managing-the-user-auth-state/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/code/06-storing-managing-the-user-auth-state/assets/adaptive-icon.png differ
diff --git a/code/06-storing-managing-the-user-auth-state/assets/favicon.png b/code/06-storing-managing-the-user-auth-state/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/code/06-storing-managing-the-user-auth-state/assets/favicon.png differ
diff --git a/code/06-storing-managing-the-user-auth-state/assets/icon.png b/code/06-storing-managing-the-user-auth-state/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/code/06-storing-managing-the-user-auth-state/assets/icon.png differ
diff --git a/code/06-storing-managing-the-user-auth-state/assets/splash.png b/code/06-storing-managing-the-user-auth-state/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/code/06-storing-managing-the-user-auth-state/assets/splash.png differ
diff --git a/code/06-storing-managing-the-user-auth-state/babel.config.js b/code/06-storing-managing-the-user-auth-state/babel.config.js
new file mode 100644
index 00000000..2900afe9
--- /dev/null
+++ b/code/06-storing-managing-the-user-auth-state/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/code/06-storing-managing-the-user-auth-state/components/Auth/AuthContent.js b/code/06-storing-managing-the-user-auth-state/components/Auth/AuthContent.js
new file mode 100644
index 00000000..8eb04b4f
--- /dev/null
+++ b/code/06-storing-managing-the-user-auth-state/components/Auth/AuthContent.js
@@ -0,0 +1,89 @@
+import { useState } from 'react';
+import { Alert, StyleSheet, View } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+
+import FlatButton from '../ui/FlatButton';
+import AuthForm from './AuthForm';
+import { Colors } from '../../constants/styles';
+
+function AuthContent({ isLogin, onAuthenticate }) {
+ const navigation = useNavigation();
+
+ const [credentialsInvalid, setCredentialsInvalid] = useState({
+ email: false,
+ password: false,
+ confirmEmail: false,
+ confirmPassword: false,
+ });
+
+ function switchAuthModeHandler() {
+ if (isLogin) {
+ navigation.replace('Signup');
+ } else {
+ navigation.replace('Login');
+ }
+ }
+
+ function submitHandler(credentials) {
+ let { email, confirmEmail, password, confirmPassword } = credentials;
+
+ email = email.trim();
+ password = password.trim();
+
+ const emailIsValid = email.includes('@');
+ const passwordIsValid = password.length > 6;
+ const emailsAreEqual = email === confirmEmail;
+ const passwordsAreEqual = password === confirmPassword;
+
+ if (
+ !emailIsValid ||
+ !passwordIsValid ||
+ (!isLogin && (!emailsAreEqual || !passwordsAreEqual))
+ ) {
+ Alert.alert('Invalid input', 'Please check your entered credentials.');
+ setCredentialsInvalid({
+ email: !emailIsValid,
+ confirmEmail: !emailIsValid || !emailsAreEqual,
+ password: !passwordIsValid,
+ confirmPassword: !passwordIsValid || !passwordsAreEqual,
+ });
+ return;
+ }
+ onAuthenticate({ email, password });
+ }
+
+ return (
+
+
+
+
+ {isLogin ? 'Create a new user' : 'Log in instead'}
+
+
+
+ );
+}
+
+export default AuthContent;
+
+const styles = StyleSheet.create({
+ authContent: {
+ marginTop: 64,
+ marginHorizontal: 32,
+ padding: 16,
+ borderRadius: 8,
+ backgroundColor: Colors.primary800,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.35,
+ shadowRadius: 4,
+ },
+ buttons: {
+ marginTop: 8,
+ },
+});
diff --git a/code/06-storing-managing-the-user-auth-state/components/Auth/AuthForm.js b/code/06-storing-managing-the-user-auth-state/components/Auth/AuthForm.js
new file mode 100644
index 00000000..cf4a7a81
--- /dev/null
+++ b/code/06-storing-managing-the-user-auth-state/components/Auth/AuthForm.js
@@ -0,0 +1,100 @@
+import { useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import Button from '../ui/Button';
+import Input from './Input';
+
+function AuthForm({ isLogin, onSubmit, credentialsInvalid }) {
+ const [enteredEmail, setEnteredEmail] = useState('');
+ const [enteredConfirmEmail, setEnteredConfirmEmail] = useState('');
+ const [enteredPassword, setEnteredPassword] = useState('');
+ const [enteredConfirmPassword, setEnteredConfirmPassword] = useState('');
+
+ const {
+ email: emailIsInvalid,
+ confirmEmail: emailsDontMatch,
+ password: passwordIsInvalid,
+ confirmPassword: passwordsDontMatch,
+ } = credentialsInvalid;
+
+ function updateInputValueHandler(inputType, enteredValue) {
+ switch (inputType) {
+ case 'email':
+ setEnteredEmail(enteredValue);
+ break;
+ case 'confirmEmail':
+ setEnteredConfirmEmail(enteredValue);
+ break;
+ case 'password':
+ setEnteredPassword(enteredValue);
+ break;
+ case 'confirmPassword':
+ setEnteredConfirmPassword(enteredValue);
+ break;
+ }
+ }
+
+ function submitHandler() {
+ onSubmit({
+ email: enteredEmail,
+ confirmEmail: enteredConfirmEmail,
+ password: enteredPassword,
+ confirmPassword: enteredConfirmPassword,
+ });
+ }
+
+ return (
+
+
+
+ {!isLogin && (
+
+ )}
+
+ {!isLogin && (
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default AuthForm;
+
+const styles = StyleSheet.create({
+ buttons: {
+ marginTop: 12,
+ },
+});
diff --git a/code/06-storing-managing-the-user-auth-state/components/Auth/Input.js b/code/06-storing-managing-the-user-auth-state/components/Auth/Input.js
new file mode 100644
index 00000000..3aa0d5dd
--- /dev/null
+++ b/code/06-storing-managing-the-user-auth-state/components/Auth/Input.js
@@ -0,0 +1,54 @@
+import { View, Text, TextInput, StyleSheet } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Input({
+ label,
+ keyboardType,
+ secure,
+ onUpdateValue,
+ value,
+ isInvalid,
+}) {
+ return (
+
+
+ {label}
+
+
+
+ );
+}
+
+export default Input;
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ marginVertical: 8,
+ },
+ label: {
+ color: 'white',
+ marginBottom: 4,
+ },
+ labelInvalid: {
+ color: Colors.error500,
+ },
+ input: {
+ paddingVertical: 8,
+ paddingHorizontal: 6,
+ backgroundColor: Colors.primary100,
+ borderRadius: 4,
+ fontSize: 16,
+ },
+ inputInvalid: {
+ backgroundColor: Colors.error100,
+ },
+});
diff --git a/code/06-storing-managing-the-user-auth-state/components/ui/Button.js b/code/06-storing-managing-the-user-auth-state/components/ui/Button.js
new file mode 100644
index 00000000..9b4b231d
--- /dev/null
+++ b/code/06-storing-managing-the-user-auth-state/components/ui/Button.js
@@ -0,0 +1,41 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Button({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default Button;
+
+const styles = StyleSheet.create({
+ button: {
+ borderRadius: 6,
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ backgroundColor: Colors.primary500,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: 'white',
+ fontSize: 16,
+ fontWeight: 'bold'
+ },
+});
diff --git a/code/06-storing-managing-the-user-auth-state/components/ui/FlatButton.js b/code/06-storing-managing-the-user-auth-state/components/ui/FlatButton.js
new file mode 100644
index 00000000..1b45e9ec
--- /dev/null
+++ b/code/06-storing-managing-the-user-auth-state/components/ui/FlatButton.js
@@ -0,0 +1,32 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function FlatButton({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default FlatButton;
+
+const styles = StyleSheet.create({
+ button: {
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: Colors.primary100,
+ },
+});
diff --git a/code/06-storing-managing-the-user-auth-state/components/ui/IconButton.js b/code/06-storing-managing-the-user-auth-state/components/ui/IconButton.js
new file mode 100644
index 00000000..7f438db4
--- /dev/null
+++ b/code/06-storing-managing-the-user-auth-state/components/ui/IconButton.js
@@ -0,0 +1,25 @@
+import { Pressable, StyleSheet } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+
+function IconButton({ icon, color, size, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+
+ );
+}
+
+export default IconButton;
+
+const styles = StyleSheet.create({
+ button: {
+ margin: 8,
+ borderRadius: 20,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+});
diff --git a/code/06-storing-managing-the-user-auth-state/components/ui/LoadingOverlay.js b/code/06-storing-managing-the-user-auth-state/components/ui/LoadingOverlay.js
new file mode 100644
index 00000000..99ae9528
--- /dev/null
+++ b/code/06-storing-managing-the-user-auth-state/components/ui/LoadingOverlay.js
@@ -0,0 +1,25 @@
+import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
+
+function LoadingOverlay({ message }) {
+ return (
+
+ {message}
+
+
+ );
+}
+
+export default LoadingOverlay;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ message: {
+ fontSize: 16,
+ marginBottom: 12,
+ },
+});
diff --git a/code/06-storing-managing-the-user-auth-state/constants/styles.js b/code/06-storing-managing-the-user-auth-state/constants/styles.js
new file mode 100644
index 00000000..0ae92d20
--- /dev/null
+++ b/code/06-storing-managing-the-user-auth-state/constants/styles.js
@@ -0,0 +1,7 @@
+export const Colors = {
+ primary100: '#f9beda',
+ primary500: '#c30b64',
+ primary800: '#610440',
+ error100: '#fcdcbf',
+ error500: '#f37c13',
+}
\ No newline at end of file
diff --git a/code/06-storing-managing-the-user-auth-state/package.json b/code/06-storing-managing-the-user-auth-state/package.json
new file mode 100644
index 00000000..85a602c5
--- /dev/null
+++ b/code/06-storing-managing-the-user-auth-state/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-navigation/native": "^6.0.8",
+ "@react-navigation/native-stack": "^6.5.0",
+ "axios": "^0.26.0",
+ "expo": "~44.0.0",
+ "expo-status-bar": "~1.2.0",
+ "react": "17.0.1",
+ "react-dom": "17.0.1",
+ "react-native": "0.64.3",
+ "react-native-web": "0.17.1"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.12.9"
+ },
+ "private": true
+}
diff --git a/code/06-storing-managing-the-user-auth-state/screens/LoginScreen.js b/code/06-storing-managing-the-user-auth-state/screens/LoginScreen.js
new file mode 100644
index 00000000..d6723da9
--- /dev/null
+++ b/code/06-storing-managing-the-user-auth-state/screens/LoginScreen.js
@@ -0,0 +1,31 @@
+import { useState } from 'react';
+import { Alert } from 'react-native';
+
+import AuthContent from '../components/Auth/AuthContent';
+import LoadingOverlay from '../components/ui/LoadingOverlay';
+import { login } from '../util/auth';
+
+function LoginScreen() {
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+
+ async function loginHandler({ email, password }) {
+ setIsAuthenticating(true);
+ try {
+ await login(email, password);
+ } catch (error) {
+ Alert.alert(
+ 'Authentication failed!',
+ 'Could not log you in. Please check your credentials or try again later!'
+ );
+ }
+ setIsAuthenticating(false);
+ }
+
+ if (isAuthenticating) {
+ return ;
+ }
+
+ return ;
+}
+
+export default LoginScreen;
diff --git a/code/06-storing-managing-the-user-auth-state/screens/SignupScreen.js b/code/06-storing-managing-the-user-auth-state/screens/SignupScreen.js
new file mode 100644
index 00000000..e10d16aa
--- /dev/null
+++ b/code/06-storing-managing-the-user-auth-state/screens/SignupScreen.js
@@ -0,0 +1,31 @@
+import { useState } from 'react';
+import { Alert } from 'react-native';
+
+import AuthContent from '../components/Auth/AuthContent';
+import LoadingOverlay from '../components/ui/LoadingOverlay';
+import { createUser } from '../util/auth';
+
+function SignupScreen() {
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+
+ async function signupHandler({ email, password }) {
+ setIsAuthenticating(true);
+ try {
+ await createUser(email, password);
+ } catch (error) {
+ Alert.alert(
+ 'Authentication failed',
+ 'Could not create user, please check your input and try again later.'
+ );
+ }
+ setIsAuthenticating(false);
+ }
+
+ if (isAuthenticating) {
+ return ;
+ }
+
+ return ;
+}
+
+export default SignupScreen;
diff --git a/code/06-storing-managing-the-user-auth-state/screens/WelcomeScreen.js b/code/06-storing-managing-the-user-auth-state/screens/WelcomeScreen.js
new file mode 100644
index 00000000..b51d4283
--- /dev/null
+++ b/code/06-storing-managing-the-user-auth-state/screens/WelcomeScreen.js
@@ -0,0 +1,26 @@
+import { StyleSheet, Text, View } from 'react-native';
+
+function WelcomeScreen() {
+ return (
+
+ Welcome!
+ You authenticated successfully!
+
+ );
+}
+
+export default WelcomeScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ title: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ },
+});
diff --git a/code/06-storing-managing-the-user-auth-state/store/auth-context.js b/code/06-storing-managing-the-user-auth-state/store/auth-context.js
new file mode 100644
index 00000000..8105cb78
--- /dev/null
+++ b/code/06-storing-managing-the-user-auth-state/store/auth-context.js
@@ -0,0 +1,31 @@
+import { createContext, useState } from 'react';
+
+export const AuthContext = createContext({
+ token: '',
+ isAuthenticated: false,
+ authenticate: (token) => {},
+ logout: () => {},
+});
+
+function AuthContextProvider({ children }) {
+ const [authToken, setAuthToken] = useState();
+
+ function authenticate(token) {
+ setAuthToken(token);
+ }
+
+ function logout() {
+ setAuthToken(null);
+ }
+
+ const value = {
+ token: authToken,
+ isAuthenticated: !!authToken,
+ authenticate: authenticate,
+ logout: logout,
+ };
+
+ return {children};
+}
+
+export default AuthContextProvider;
diff --git a/code/06-storing-managing-the-user-auth-state/util/auth.js b/code/06-storing-managing-the-user-auth-state/util/auth.js
new file mode 100644
index 00000000..eec60719
--- /dev/null
+++ b/code/06-storing-managing-the-user-auth-state/util/auth.js
@@ -0,0 +1,23 @@
+import axios from 'axios';
+
+const API_KEY = 'AIzaSyDCYasArcOwcALFhIj2szug5aD2PgUQu1E';
+
+async function authenticate(mode, email, password) {
+ const url = `https://identitytoolkit.googleapis.com/v1/accounts:${mode}?key=${API_KEY}`;
+
+ const response = await axios.post(url, {
+ email: email,
+ password: password,
+ returnSecureToken: true,
+ });
+
+ console.log(response.data);
+}
+
+export async function createUser(email, password) {
+ await authenticate('signUp', email, password);
+}
+
+export async function login(email, password) {
+ await authenticate('signInWithPassword', email, password);
+}
\ No newline at end of file
diff --git a/code/07-protecting-screens/App.js b/code/07-protecting-screens/App.js
new file mode 100644
index 00000000..b9de2217
--- /dev/null
+++ b/code/07-protecting-screens/App.js
@@ -0,0 +1,63 @@
+import { useContext } from 'react';
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { StatusBar } from 'expo-status-bar';
+
+import LoginScreen from './screens/LoginScreen';
+import SignupScreen from './screens/SignupScreen';
+import WelcomeScreen from './screens/WelcomeScreen';
+import { Colors } from './constants/styles';
+import AuthContextProvider, { AuthContext } from './store/auth-context';
+
+const Stack = createNativeStackNavigator();
+
+function AuthStack() {
+ return (
+
+
+
+
+ );
+}
+
+function AuthenticatedStack() {
+ return (
+
+
+
+ );
+}
+
+function Navigation() {
+ const authCtx = useContext(AuthContext);
+
+ return (
+
+ {!authCtx.isAuthenticated && }
+ {authCtx.isAuthenticated && }
+
+ );
+}
+
+export default function App() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/code/07-protecting-screens/app.json b/code/07-protecting-screens/app.json
new file mode 100644
index 00000000..9a1223e7
--- /dev/null
+++ b/code/07-protecting-screens/app.json
@@ -0,0 +1,32 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/code/07-protecting-screens/assets/adaptive-icon.png b/code/07-protecting-screens/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/code/07-protecting-screens/assets/adaptive-icon.png differ
diff --git a/code/07-protecting-screens/assets/favicon.png b/code/07-protecting-screens/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/code/07-protecting-screens/assets/favicon.png differ
diff --git a/code/07-protecting-screens/assets/icon.png b/code/07-protecting-screens/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/code/07-protecting-screens/assets/icon.png differ
diff --git a/code/07-protecting-screens/assets/splash.png b/code/07-protecting-screens/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/code/07-protecting-screens/assets/splash.png differ
diff --git a/code/07-protecting-screens/babel.config.js b/code/07-protecting-screens/babel.config.js
new file mode 100644
index 00000000..2900afe9
--- /dev/null
+++ b/code/07-protecting-screens/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/code/07-protecting-screens/components/Auth/AuthContent.js b/code/07-protecting-screens/components/Auth/AuthContent.js
new file mode 100644
index 00000000..8eb04b4f
--- /dev/null
+++ b/code/07-protecting-screens/components/Auth/AuthContent.js
@@ -0,0 +1,89 @@
+import { useState } from 'react';
+import { Alert, StyleSheet, View } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+
+import FlatButton from '../ui/FlatButton';
+import AuthForm from './AuthForm';
+import { Colors } from '../../constants/styles';
+
+function AuthContent({ isLogin, onAuthenticate }) {
+ const navigation = useNavigation();
+
+ const [credentialsInvalid, setCredentialsInvalid] = useState({
+ email: false,
+ password: false,
+ confirmEmail: false,
+ confirmPassword: false,
+ });
+
+ function switchAuthModeHandler() {
+ if (isLogin) {
+ navigation.replace('Signup');
+ } else {
+ navigation.replace('Login');
+ }
+ }
+
+ function submitHandler(credentials) {
+ let { email, confirmEmail, password, confirmPassword } = credentials;
+
+ email = email.trim();
+ password = password.trim();
+
+ const emailIsValid = email.includes('@');
+ const passwordIsValid = password.length > 6;
+ const emailsAreEqual = email === confirmEmail;
+ const passwordsAreEqual = password === confirmPassword;
+
+ if (
+ !emailIsValid ||
+ !passwordIsValid ||
+ (!isLogin && (!emailsAreEqual || !passwordsAreEqual))
+ ) {
+ Alert.alert('Invalid input', 'Please check your entered credentials.');
+ setCredentialsInvalid({
+ email: !emailIsValid,
+ confirmEmail: !emailIsValid || !emailsAreEqual,
+ password: !passwordIsValid,
+ confirmPassword: !passwordIsValid || !passwordsAreEqual,
+ });
+ return;
+ }
+ onAuthenticate({ email, password });
+ }
+
+ return (
+
+
+
+
+ {isLogin ? 'Create a new user' : 'Log in instead'}
+
+
+
+ );
+}
+
+export default AuthContent;
+
+const styles = StyleSheet.create({
+ authContent: {
+ marginTop: 64,
+ marginHorizontal: 32,
+ padding: 16,
+ borderRadius: 8,
+ backgroundColor: Colors.primary800,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.35,
+ shadowRadius: 4,
+ },
+ buttons: {
+ marginTop: 8,
+ },
+});
diff --git a/code/07-protecting-screens/components/Auth/AuthForm.js b/code/07-protecting-screens/components/Auth/AuthForm.js
new file mode 100644
index 00000000..cf4a7a81
--- /dev/null
+++ b/code/07-protecting-screens/components/Auth/AuthForm.js
@@ -0,0 +1,100 @@
+import { useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import Button from '../ui/Button';
+import Input from './Input';
+
+function AuthForm({ isLogin, onSubmit, credentialsInvalid }) {
+ const [enteredEmail, setEnteredEmail] = useState('');
+ const [enteredConfirmEmail, setEnteredConfirmEmail] = useState('');
+ const [enteredPassword, setEnteredPassword] = useState('');
+ const [enteredConfirmPassword, setEnteredConfirmPassword] = useState('');
+
+ const {
+ email: emailIsInvalid,
+ confirmEmail: emailsDontMatch,
+ password: passwordIsInvalid,
+ confirmPassword: passwordsDontMatch,
+ } = credentialsInvalid;
+
+ function updateInputValueHandler(inputType, enteredValue) {
+ switch (inputType) {
+ case 'email':
+ setEnteredEmail(enteredValue);
+ break;
+ case 'confirmEmail':
+ setEnteredConfirmEmail(enteredValue);
+ break;
+ case 'password':
+ setEnteredPassword(enteredValue);
+ break;
+ case 'confirmPassword':
+ setEnteredConfirmPassword(enteredValue);
+ break;
+ }
+ }
+
+ function submitHandler() {
+ onSubmit({
+ email: enteredEmail,
+ confirmEmail: enteredConfirmEmail,
+ password: enteredPassword,
+ confirmPassword: enteredConfirmPassword,
+ });
+ }
+
+ return (
+
+
+
+ {!isLogin && (
+
+ )}
+
+ {!isLogin && (
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default AuthForm;
+
+const styles = StyleSheet.create({
+ buttons: {
+ marginTop: 12,
+ },
+});
diff --git a/code/07-protecting-screens/components/Auth/Input.js b/code/07-protecting-screens/components/Auth/Input.js
new file mode 100644
index 00000000..3aa0d5dd
--- /dev/null
+++ b/code/07-protecting-screens/components/Auth/Input.js
@@ -0,0 +1,54 @@
+import { View, Text, TextInput, StyleSheet } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Input({
+ label,
+ keyboardType,
+ secure,
+ onUpdateValue,
+ value,
+ isInvalid,
+}) {
+ return (
+
+
+ {label}
+
+
+
+ );
+}
+
+export default Input;
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ marginVertical: 8,
+ },
+ label: {
+ color: 'white',
+ marginBottom: 4,
+ },
+ labelInvalid: {
+ color: Colors.error500,
+ },
+ input: {
+ paddingVertical: 8,
+ paddingHorizontal: 6,
+ backgroundColor: Colors.primary100,
+ borderRadius: 4,
+ fontSize: 16,
+ },
+ inputInvalid: {
+ backgroundColor: Colors.error100,
+ },
+});
diff --git a/code/07-protecting-screens/components/ui/Button.js b/code/07-protecting-screens/components/ui/Button.js
new file mode 100644
index 00000000..9b4b231d
--- /dev/null
+++ b/code/07-protecting-screens/components/ui/Button.js
@@ -0,0 +1,41 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Button({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default Button;
+
+const styles = StyleSheet.create({
+ button: {
+ borderRadius: 6,
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ backgroundColor: Colors.primary500,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: 'white',
+ fontSize: 16,
+ fontWeight: 'bold'
+ },
+});
diff --git a/code/07-protecting-screens/components/ui/FlatButton.js b/code/07-protecting-screens/components/ui/FlatButton.js
new file mode 100644
index 00000000..1b45e9ec
--- /dev/null
+++ b/code/07-protecting-screens/components/ui/FlatButton.js
@@ -0,0 +1,32 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function FlatButton({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default FlatButton;
+
+const styles = StyleSheet.create({
+ button: {
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: Colors.primary100,
+ },
+});
diff --git a/code/07-protecting-screens/components/ui/IconButton.js b/code/07-protecting-screens/components/ui/IconButton.js
new file mode 100644
index 00000000..7f438db4
--- /dev/null
+++ b/code/07-protecting-screens/components/ui/IconButton.js
@@ -0,0 +1,25 @@
+import { Pressable, StyleSheet } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+
+function IconButton({ icon, color, size, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+
+ );
+}
+
+export default IconButton;
+
+const styles = StyleSheet.create({
+ button: {
+ margin: 8,
+ borderRadius: 20,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+});
diff --git a/code/07-protecting-screens/components/ui/LoadingOverlay.js b/code/07-protecting-screens/components/ui/LoadingOverlay.js
new file mode 100644
index 00000000..99ae9528
--- /dev/null
+++ b/code/07-protecting-screens/components/ui/LoadingOverlay.js
@@ -0,0 +1,25 @@
+import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
+
+function LoadingOverlay({ message }) {
+ return (
+
+ {message}
+
+
+ );
+}
+
+export default LoadingOverlay;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ message: {
+ fontSize: 16,
+ marginBottom: 12,
+ },
+});
diff --git a/code/07-protecting-screens/constants/styles.js b/code/07-protecting-screens/constants/styles.js
new file mode 100644
index 00000000..0ae92d20
--- /dev/null
+++ b/code/07-protecting-screens/constants/styles.js
@@ -0,0 +1,7 @@
+export const Colors = {
+ primary100: '#f9beda',
+ primary500: '#c30b64',
+ primary800: '#610440',
+ error100: '#fcdcbf',
+ error500: '#f37c13',
+}
\ No newline at end of file
diff --git a/code/07-protecting-screens/package.json b/code/07-protecting-screens/package.json
new file mode 100644
index 00000000..85a602c5
--- /dev/null
+++ b/code/07-protecting-screens/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-navigation/native": "^6.0.8",
+ "@react-navigation/native-stack": "^6.5.0",
+ "axios": "^0.26.0",
+ "expo": "~44.0.0",
+ "expo-status-bar": "~1.2.0",
+ "react": "17.0.1",
+ "react-dom": "17.0.1",
+ "react-native": "0.64.3",
+ "react-native-web": "0.17.1"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.12.9"
+ },
+ "private": true
+}
diff --git a/code/07-protecting-screens/screens/LoginScreen.js b/code/07-protecting-screens/screens/LoginScreen.js
new file mode 100644
index 00000000..ff128968
--- /dev/null
+++ b/code/07-protecting-screens/screens/LoginScreen.js
@@ -0,0 +1,35 @@
+import { useContext, useState } from 'react';
+import { Alert } from 'react-native';
+
+import AuthContent from '../components/Auth/AuthContent';
+import LoadingOverlay from '../components/ui/LoadingOverlay';
+import { AuthContext } from '../store/auth-context';
+import { login } from '../util/auth';
+
+function LoginScreen() {
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+
+ const authCtx = useContext(AuthContext);
+
+ async function loginHandler({ email, password }) {
+ setIsAuthenticating(true);
+ try {
+ const token = await login(email, password);
+ authCtx.authenticate(token);
+ } catch (error) {
+ Alert.alert(
+ 'Authentication failed!',
+ 'Could not log you in. Please check your credentials or try again later!'
+ );
+ setIsAuthenticating(false);
+ }
+ }
+
+ if (isAuthenticating) {
+ return ;
+ }
+
+ return ;
+}
+
+export default LoginScreen;
diff --git a/code/07-protecting-screens/screens/SignupScreen.js b/code/07-protecting-screens/screens/SignupScreen.js
new file mode 100644
index 00000000..4d9e7b86
--- /dev/null
+++ b/code/07-protecting-screens/screens/SignupScreen.js
@@ -0,0 +1,35 @@
+import { useContext, useState } from 'react';
+import { Alert } from 'react-native';
+
+import AuthContent from '../components/Auth/AuthContent';
+import LoadingOverlay from '../components/ui/LoadingOverlay';
+import { AuthContext } from '../store/auth-context';
+import { createUser } from '../util/auth';
+
+function SignupScreen() {
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+
+ const authCtx = useContext(AuthContext);
+
+ async function signupHandler({ email, password }) {
+ setIsAuthenticating(true);
+ try {
+ const token = await createUser(email, password);
+ authCtx.authenticate(token);
+ } catch (error) {
+ Alert.alert(
+ 'Authentication failed',
+ 'Could not create user, please check your input and try again later.'
+ );
+ setIsAuthenticating(false);
+ }
+ }
+
+ if (isAuthenticating) {
+ return ;
+ }
+
+ return ;
+}
+
+export default SignupScreen;
diff --git a/code/07-protecting-screens/screens/WelcomeScreen.js b/code/07-protecting-screens/screens/WelcomeScreen.js
new file mode 100644
index 00000000..b51d4283
--- /dev/null
+++ b/code/07-protecting-screens/screens/WelcomeScreen.js
@@ -0,0 +1,26 @@
+import { StyleSheet, Text, View } from 'react-native';
+
+function WelcomeScreen() {
+ return (
+
+ Welcome!
+ You authenticated successfully!
+
+ );
+}
+
+export default WelcomeScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ title: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ },
+});
diff --git a/code/07-protecting-screens/store/auth-context.js b/code/07-protecting-screens/store/auth-context.js
new file mode 100644
index 00000000..8105cb78
--- /dev/null
+++ b/code/07-protecting-screens/store/auth-context.js
@@ -0,0 +1,31 @@
+import { createContext, useState } from 'react';
+
+export const AuthContext = createContext({
+ token: '',
+ isAuthenticated: false,
+ authenticate: (token) => {},
+ logout: () => {},
+});
+
+function AuthContextProvider({ children }) {
+ const [authToken, setAuthToken] = useState();
+
+ function authenticate(token) {
+ setAuthToken(token);
+ }
+
+ function logout() {
+ setAuthToken(null);
+ }
+
+ const value = {
+ token: authToken,
+ isAuthenticated: !!authToken,
+ authenticate: authenticate,
+ logout: logout,
+ };
+
+ return {children};
+}
+
+export default AuthContextProvider;
diff --git a/code/07-protecting-screens/util/auth.js b/code/07-protecting-screens/util/auth.js
new file mode 100644
index 00000000..a21ffc4e
--- /dev/null
+++ b/code/07-protecting-screens/util/auth.js
@@ -0,0 +1,25 @@
+import axios from 'axios';
+
+const API_KEY = 'AIzaSyDCYasArcOwcALFhIj2szug5aD2PgUQu1E';
+
+async function authenticate(mode, email, password) {
+ const url = `https://identitytoolkit.googleapis.com/v1/accounts:${mode}?key=${API_KEY}`;
+
+ const response = await axios.post(url, {
+ email: email,
+ password: password,
+ returnSecureToken: true,
+ });
+
+ const token = response.data.idToken;
+
+ return token;
+}
+
+export function createUser(email, password) {
+ return authenticate('signUp', email, password);
+}
+
+export function login(email, password) {
+ return authenticate('signInWithPassword', email, password);
+}
\ No newline at end of file
diff --git a/code/08-adding-logout/App.js b/code/08-adding-logout/App.js
new file mode 100644
index 00000000..72a18356
--- /dev/null
+++ b/code/08-adding-logout/App.js
@@ -0,0 +1,78 @@
+import { useContext } from 'react';
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { StatusBar } from 'expo-status-bar';
+
+import LoginScreen from './screens/LoginScreen';
+import SignupScreen from './screens/SignupScreen';
+import WelcomeScreen from './screens/WelcomeScreen';
+import { Colors } from './constants/styles';
+import AuthContextProvider, { AuthContext } from './store/auth-context';
+import IconButton from './components/ui/IconButton';
+
+const Stack = createNativeStackNavigator();
+
+function AuthStack() {
+ return (
+
+
+
+
+ );
+}
+
+function AuthenticatedStack() {
+ const authCtx = useContext(AuthContext);
+ return (
+
+ (
+
+ ),
+ }}
+ />
+
+ );
+}
+
+function Navigation() {
+ const authCtx = useContext(AuthContext);
+
+ return (
+
+ {!authCtx.isAuthenticated && }
+ {authCtx.isAuthenticated && }
+
+ );
+}
+
+export default function App() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/code/08-adding-logout/app.json b/code/08-adding-logout/app.json
new file mode 100644
index 00000000..9a1223e7
--- /dev/null
+++ b/code/08-adding-logout/app.json
@@ -0,0 +1,32 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/code/08-adding-logout/assets/adaptive-icon.png b/code/08-adding-logout/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/code/08-adding-logout/assets/adaptive-icon.png differ
diff --git a/code/08-adding-logout/assets/favicon.png b/code/08-adding-logout/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/code/08-adding-logout/assets/favicon.png differ
diff --git a/code/08-adding-logout/assets/icon.png b/code/08-adding-logout/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/code/08-adding-logout/assets/icon.png differ
diff --git a/code/08-adding-logout/assets/splash.png b/code/08-adding-logout/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/code/08-adding-logout/assets/splash.png differ
diff --git a/code/08-adding-logout/babel.config.js b/code/08-adding-logout/babel.config.js
new file mode 100644
index 00000000..2900afe9
--- /dev/null
+++ b/code/08-adding-logout/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/code/08-adding-logout/components/Auth/AuthContent.js b/code/08-adding-logout/components/Auth/AuthContent.js
new file mode 100644
index 00000000..8eb04b4f
--- /dev/null
+++ b/code/08-adding-logout/components/Auth/AuthContent.js
@@ -0,0 +1,89 @@
+import { useState } from 'react';
+import { Alert, StyleSheet, View } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+
+import FlatButton from '../ui/FlatButton';
+import AuthForm from './AuthForm';
+import { Colors } from '../../constants/styles';
+
+function AuthContent({ isLogin, onAuthenticate }) {
+ const navigation = useNavigation();
+
+ const [credentialsInvalid, setCredentialsInvalid] = useState({
+ email: false,
+ password: false,
+ confirmEmail: false,
+ confirmPassword: false,
+ });
+
+ function switchAuthModeHandler() {
+ if (isLogin) {
+ navigation.replace('Signup');
+ } else {
+ navigation.replace('Login');
+ }
+ }
+
+ function submitHandler(credentials) {
+ let { email, confirmEmail, password, confirmPassword } = credentials;
+
+ email = email.trim();
+ password = password.trim();
+
+ const emailIsValid = email.includes('@');
+ const passwordIsValid = password.length > 6;
+ const emailsAreEqual = email === confirmEmail;
+ const passwordsAreEqual = password === confirmPassword;
+
+ if (
+ !emailIsValid ||
+ !passwordIsValid ||
+ (!isLogin && (!emailsAreEqual || !passwordsAreEqual))
+ ) {
+ Alert.alert('Invalid input', 'Please check your entered credentials.');
+ setCredentialsInvalid({
+ email: !emailIsValid,
+ confirmEmail: !emailIsValid || !emailsAreEqual,
+ password: !passwordIsValid,
+ confirmPassword: !passwordIsValid || !passwordsAreEqual,
+ });
+ return;
+ }
+ onAuthenticate({ email, password });
+ }
+
+ return (
+
+
+
+
+ {isLogin ? 'Create a new user' : 'Log in instead'}
+
+
+
+ );
+}
+
+export default AuthContent;
+
+const styles = StyleSheet.create({
+ authContent: {
+ marginTop: 64,
+ marginHorizontal: 32,
+ padding: 16,
+ borderRadius: 8,
+ backgroundColor: Colors.primary800,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.35,
+ shadowRadius: 4,
+ },
+ buttons: {
+ marginTop: 8,
+ },
+});
diff --git a/code/08-adding-logout/components/Auth/AuthForm.js b/code/08-adding-logout/components/Auth/AuthForm.js
new file mode 100644
index 00000000..cf4a7a81
--- /dev/null
+++ b/code/08-adding-logout/components/Auth/AuthForm.js
@@ -0,0 +1,100 @@
+import { useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import Button from '../ui/Button';
+import Input from './Input';
+
+function AuthForm({ isLogin, onSubmit, credentialsInvalid }) {
+ const [enteredEmail, setEnteredEmail] = useState('');
+ const [enteredConfirmEmail, setEnteredConfirmEmail] = useState('');
+ const [enteredPassword, setEnteredPassword] = useState('');
+ const [enteredConfirmPassword, setEnteredConfirmPassword] = useState('');
+
+ const {
+ email: emailIsInvalid,
+ confirmEmail: emailsDontMatch,
+ password: passwordIsInvalid,
+ confirmPassword: passwordsDontMatch,
+ } = credentialsInvalid;
+
+ function updateInputValueHandler(inputType, enteredValue) {
+ switch (inputType) {
+ case 'email':
+ setEnteredEmail(enteredValue);
+ break;
+ case 'confirmEmail':
+ setEnteredConfirmEmail(enteredValue);
+ break;
+ case 'password':
+ setEnteredPassword(enteredValue);
+ break;
+ case 'confirmPassword':
+ setEnteredConfirmPassword(enteredValue);
+ break;
+ }
+ }
+
+ function submitHandler() {
+ onSubmit({
+ email: enteredEmail,
+ confirmEmail: enteredConfirmEmail,
+ password: enteredPassword,
+ confirmPassword: enteredConfirmPassword,
+ });
+ }
+
+ return (
+
+
+
+ {!isLogin && (
+
+ )}
+
+ {!isLogin && (
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default AuthForm;
+
+const styles = StyleSheet.create({
+ buttons: {
+ marginTop: 12,
+ },
+});
diff --git a/code/08-adding-logout/components/Auth/Input.js b/code/08-adding-logout/components/Auth/Input.js
new file mode 100644
index 00000000..3aa0d5dd
--- /dev/null
+++ b/code/08-adding-logout/components/Auth/Input.js
@@ -0,0 +1,54 @@
+import { View, Text, TextInput, StyleSheet } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Input({
+ label,
+ keyboardType,
+ secure,
+ onUpdateValue,
+ value,
+ isInvalid,
+}) {
+ return (
+
+
+ {label}
+
+
+
+ );
+}
+
+export default Input;
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ marginVertical: 8,
+ },
+ label: {
+ color: 'white',
+ marginBottom: 4,
+ },
+ labelInvalid: {
+ color: Colors.error500,
+ },
+ input: {
+ paddingVertical: 8,
+ paddingHorizontal: 6,
+ backgroundColor: Colors.primary100,
+ borderRadius: 4,
+ fontSize: 16,
+ },
+ inputInvalid: {
+ backgroundColor: Colors.error100,
+ },
+});
diff --git a/code/08-adding-logout/components/ui/Button.js b/code/08-adding-logout/components/ui/Button.js
new file mode 100644
index 00000000..9b4b231d
--- /dev/null
+++ b/code/08-adding-logout/components/ui/Button.js
@@ -0,0 +1,41 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Button({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default Button;
+
+const styles = StyleSheet.create({
+ button: {
+ borderRadius: 6,
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ backgroundColor: Colors.primary500,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: 'white',
+ fontSize: 16,
+ fontWeight: 'bold'
+ },
+});
diff --git a/code/08-adding-logout/components/ui/FlatButton.js b/code/08-adding-logout/components/ui/FlatButton.js
new file mode 100644
index 00000000..1b45e9ec
--- /dev/null
+++ b/code/08-adding-logout/components/ui/FlatButton.js
@@ -0,0 +1,32 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function FlatButton({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default FlatButton;
+
+const styles = StyleSheet.create({
+ button: {
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: Colors.primary100,
+ },
+});
diff --git a/code/08-adding-logout/components/ui/IconButton.js b/code/08-adding-logout/components/ui/IconButton.js
new file mode 100644
index 00000000..7f438db4
--- /dev/null
+++ b/code/08-adding-logout/components/ui/IconButton.js
@@ -0,0 +1,25 @@
+import { Pressable, StyleSheet } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+
+function IconButton({ icon, color, size, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+
+ );
+}
+
+export default IconButton;
+
+const styles = StyleSheet.create({
+ button: {
+ margin: 8,
+ borderRadius: 20,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+});
diff --git a/code/08-adding-logout/components/ui/LoadingOverlay.js b/code/08-adding-logout/components/ui/LoadingOverlay.js
new file mode 100644
index 00000000..99ae9528
--- /dev/null
+++ b/code/08-adding-logout/components/ui/LoadingOverlay.js
@@ -0,0 +1,25 @@
+import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
+
+function LoadingOverlay({ message }) {
+ return (
+
+ {message}
+
+
+ );
+}
+
+export default LoadingOverlay;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ message: {
+ fontSize: 16,
+ marginBottom: 12,
+ },
+});
diff --git a/code/08-adding-logout/constants/styles.js b/code/08-adding-logout/constants/styles.js
new file mode 100644
index 00000000..0ae92d20
--- /dev/null
+++ b/code/08-adding-logout/constants/styles.js
@@ -0,0 +1,7 @@
+export const Colors = {
+ primary100: '#f9beda',
+ primary500: '#c30b64',
+ primary800: '#610440',
+ error100: '#fcdcbf',
+ error500: '#f37c13',
+}
\ No newline at end of file
diff --git a/code/08-adding-logout/package.json b/code/08-adding-logout/package.json
new file mode 100644
index 00000000..85a602c5
--- /dev/null
+++ b/code/08-adding-logout/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-navigation/native": "^6.0.8",
+ "@react-navigation/native-stack": "^6.5.0",
+ "axios": "^0.26.0",
+ "expo": "~44.0.0",
+ "expo-status-bar": "~1.2.0",
+ "react": "17.0.1",
+ "react-dom": "17.0.1",
+ "react-native": "0.64.3",
+ "react-native-web": "0.17.1"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.12.9"
+ },
+ "private": true
+}
diff --git a/code/08-adding-logout/screens/LoginScreen.js b/code/08-adding-logout/screens/LoginScreen.js
new file mode 100644
index 00000000..ff128968
--- /dev/null
+++ b/code/08-adding-logout/screens/LoginScreen.js
@@ -0,0 +1,35 @@
+import { useContext, useState } from 'react';
+import { Alert } from 'react-native';
+
+import AuthContent from '../components/Auth/AuthContent';
+import LoadingOverlay from '../components/ui/LoadingOverlay';
+import { AuthContext } from '../store/auth-context';
+import { login } from '../util/auth';
+
+function LoginScreen() {
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+
+ const authCtx = useContext(AuthContext);
+
+ async function loginHandler({ email, password }) {
+ setIsAuthenticating(true);
+ try {
+ const token = await login(email, password);
+ authCtx.authenticate(token);
+ } catch (error) {
+ Alert.alert(
+ 'Authentication failed!',
+ 'Could not log you in. Please check your credentials or try again later!'
+ );
+ setIsAuthenticating(false);
+ }
+ }
+
+ if (isAuthenticating) {
+ return ;
+ }
+
+ return ;
+}
+
+export default LoginScreen;
diff --git a/code/08-adding-logout/screens/SignupScreen.js b/code/08-adding-logout/screens/SignupScreen.js
new file mode 100644
index 00000000..4d9e7b86
--- /dev/null
+++ b/code/08-adding-logout/screens/SignupScreen.js
@@ -0,0 +1,35 @@
+import { useContext, useState } from 'react';
+import { Alert } from 'react-native';
+
+import AuthContent from '../components/Auth/AuthContent';
+import LoadingOverlay from '../components/ui/LoadingOverlay';
+import { AuthContext } from '../store/auth-context';
+import { createUser } from '../util/auth';
+
+function SignupScreen() {
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+
+ const authCtx = useContext(AuthContext);
+
+ async function signupHandler({ email, password }) {
+ setIsAuthenticating(true);
+ try {
+ const token = await createUser(email, password);
+ authCtx.authenticate(token);
+ } catch (error) {
+ Alert.alert(
+ 'Authentication failed',
+ 'Could not create user, please check your input and try again later.'
+ );
+ setIsAuthenticating(false);
+ }
+ }
+
+ if (isAuthenticating) {
+ return ;
+ }
+
+ return ;
+}
+
+export default SignupScreen;
diff --git a/code/08-adding-logout/screens/WelcomeScreen.js b/code/08-adding-logout/screens/WelcomeScreen.js
new file mode 100644
index 00000000..b51d4283
--- /dev/null
+++ b/code/08-adding-logout/screens/WelcomeScreen.js
@@ -0,0 +1,26 @@
+import { StyleSheet, Text, View } from 'react-native';
+
+function WelcomeScreen() {
+ return (
+
+ Welcome!
+ You authenticated successfully!
+
+ );
+}
+
+export default WelcomeScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ title: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ },
+});
diff --git a/code/08-adding-logout/store/auth-context.js b/code/08-adding-logout/store/auth-context.js
new file mode 100644
index 00000000..8105cb78
--- /dev/null
+++ b/code/08-adding-logout/store/auth-context.js
@@ -0,0 +1,31 @@
+import { createContext, useState } from 'react';
+
+export const AuthContext = createContext({
+ token: '',
+ isAuthenticated: false,
+ authenticate: (token) => {},
+ logout: () => {},
+});
+
+function AuthContextProvider({ children }) {
+ const [authToken, setAuthToken] = useState();
+
+ function authenticate(token) {
+ setAuthToken(token);
+ }
+
+ function logout() {
+ setAuthToken(null);
+ }
+
+ const value = {
+ token: authToken,
+ isAuthenticated: !!authToken,
+ authenticate: authenticate,
+ logout: logout,
+ };
+
+ return {children};
+}
+
+export default AuthContextProvider;
diff --git a/code/08-adding-logout/util/auth.js b/code/08-adding-logout/util/auth.js
new file mode 100644
index 00000000..a21ffc4e
--- /dev/null
+++ b/code/08-adding-logout/util/auth.js
@@ -0,0 +1,25 @@
+import axios from 'axios';
+
+const API_KEY = 'AIzaSyDCYasArcOwcALFhIj2szug5aD2PgUQu1E';
+
+async function authenticate(mode, email, password) {
+ const url = `https://identitytoolkit.googleapis.com/v1/accounts:${mode}?key=${API_KEY}`;
+
+ const response = await axios.post(url, {
+ email: email,
+ password: password,
+ returnSecureToken: true,
+ });
+
+ const token = response.data.idToken;
+
+ return token;
+}
+
+export function createUser(email, password) {
+ return authenticate('signUp', email, password);
+}
+
+export function login(email, password) {
+ return authenticate('signInWithPassword', email, password);
+}
\ No newline at end of file
diff --git a/code/09-accessing-protected-resources/App.js b/code/09-accessing-protected-resources/App.js
new file mode 100644
index 00000000..72a18356
--- /dev/null
+++ b/code/09-accessing-protected-resources/App.js
@@ -0,0 +1,78 @@
+import { useContext } from 'react';
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { StatusBar } from 'expo-status-bar';
+
+import LoginScreen from './screens/LoginScreen';
+import SignupScreen from './screens/SignupScreen';
+import WelcomeScreen from './screens/WelcomeScreen';
+import { Colors } from './constants/styles';
+import AuthContextProvider, { AuthContext } from './store/auth-context';
+import IconButton from './components/ui/IconButton';
+
+const Stack = createNativeStackNavigator();
+
+function AuthStack() {
+ return (
+
+
+
+
+ );
+}
+
+function AuthenticatedStack() {
+ const authCtx = useContext(AuthContext);
+ return (
+
+ (
+
+ ),
+ }}
+ />
+
+ );
+}
+
+function Navigation() {
+ const authCtx = useContext(AuthContext);
+
+ return (
+
+ {!authCtx.isAuthenticated && }
+ {authCtx.isAuthenticated && }
+
+ );
+}
+
+export default function App() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/code/09-accessing-protected-resources/app.json b/code/09-accessing-protected-resources/app.json
new file mode 100644
index 00000000..9a1223e7
--- /dev/null
+++ b/code/09-accessing-protected-resources/app.json
@@ -0,0 +1,32 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/code/09-accessing-protected-resources/assets/adaptive-icon.png b/code/09-accessing-protected-resources/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/code/09-accessing-protected-resources/assets/adaptive-icon.png differ
diff --git a/code/09-accessing-protected-resources/assets/favicon.png b/code/09-accessing-protected-resources/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/code/09-accessing-protected-resources/assets/favicon.png differ
diff --git a/code/09-accessing-protected-resources/assets/icon.png b/code/09-accessing-protected-resources/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/code/09-accessing-protected-resources/assets/icon.png differ
diff --git a/code/09-accessing-protected-resources/assets/splash.png b/code/09-accessing-protected-resources/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/code/09-accessing-protected-resources/assets/splash.png differ
diff --git a/code/09-accessing-protected-resources/babel.config.js b/code/09-accessing-protected-resources/babel.config.js
new file mode 100644
index 00000000..2900afe9
--- /dev/null
+++ b/code/09-accessing-protected-resources/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/code/09-accessing-protected-resources/components/Auth/AuthContent.js b/code/09-accessing-protected-resources/components/Auth/AuthContent.js
new file mode 100644
index 00000000..8eb04b4f
--- /dev/null
+++ b/code/09-accessing-protected-resources/components/Auth/AuthContent.js
@@ -0,0 +1,89 @@
+import { useState } from 'react';
+import { Alert, StyleSheet, View } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+
+import FlatButton from '../ui/FlatButton';
+import AuthForm from './AuthForm';
+import { Colors } from '../../constants/styles';
+
+function AuthContent({ isLogin, onAuthenticate }) {
+ const navigation = useNavigation();
+
+ const [credentialsInvalid, setCredentialsInvalid] = useState({
+ email: false,
+ password: false,
+ confirmEmail: false,
+ confirmPassword: false,
+ });
+
+ function switchAuthModeHandler() {
+ if (isLogin) {
+ navigation.replace('Signup');
+ } else {
+ navigation.replace('Login');
+ }
+ }
+
+ function submitHandler(credentials) {
+ let { email, confirmEmail, password, confirmPassword } = credentials;
+
+ email = email.trim();
+ password = password.trim();
+
+ const emailIsValid = email.includes('@');
+ const passwordIsValid = password.length > 6;
+ const emailsAreEqual = email === confirmEmail;
+ const passwordsAreEqual = password === confirmPassword;
+
+ if (
+ !emailIsValid ||
+ !passwordIsValid ||
+ (!isLogin && (!emailsAreEqual || !passwordsAreEqual))
+ ) {
+ Alert.alert('Invalid input', 'Please check your entered credentials.');
+ setCredentialsInvalid({
+ email: !emailIsValid,
+ confirmEmail: !emailIsValid || !emailsAreEqual,
+ password: !passwordIsValid,
+ confirmPassword: !passwordIsValid || !passwordsAreEqual,
+ });
+ return;
+ }
+ onAuthenticate({ email, password });
+ }
+
+ return (
+
+
+
+
+ {isLogin ? 'Create a new user' : 'Log in instead'}
+
+
+
+ );
+}
+
+export default AuthContent;
+
+const styles = StyleSheet.create({
+ authContent: {
+ marginTop: 64,
+ marginHorizontal: 32,
+ padding: 16,
+ borderRadius: 8,
+ backgroundColor: Colors.primary800,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.35,
+ shadowRadius: 4,
+ },
+ buttons: {
+ marginTop: 8,
+ },
+});
diff --git a/code/09-accessing-protected-resources/components/Auth/AuthForm.js b/code/09-accessing-protected-resources/components/Auth/AuthForm.js
new file mode 100644
index 00000000..cf4a7a81
--- /dev/null
+++ b/code/09-accessing-protected-resources/components/Auth/AuthForm.js
@@ -0,0 +1,100 @@
+import { useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import Button from '../ui/Button';
+import Input from './Input';
+
+function AuthForm({ isLogin, onSubmit, credentialsInvalid }) {
+ const [enteredEmail, setEnteredEmail] = useState('');
+ const [enteredConfirmEmail, setEnteredConfirmEmail] = useState('');
+ const [enteredPassword, setEnteredPassword] = useState('');
+ const [enteredConfirmPassword, setEnteredConfirmPassword] = useState('');
+
+ const {
+ email: emailIsInvalid,
+ confirmEmail: emailsDontMatch,
+ password: passwordIsInvalid,
+ confirmPassword: passwordsDontMatch,
+ } = credentialsInvalid;
+
+ function updateInputValueHandler(inputType, enteredValue) {
+ switch (inputType) {
+ case 'email':
+ setEnteredEmail(enteredValue);
+ break;
+ case 'confirmEmail':
+ setEnteredConfirmEmail(enteredValue);
+ break;
+ case 'password':
+ setEnteredPassword(enteredValue);
+ break;
+ case 'confirmPassword':
+ setEnteredConfirmPassword(enteredValue);
+ break;
+ }
+ }
+
+ function submitHandler() {
+ onSubmit({
+ email: enteredEmail,
+ confirmEmail: enteredConfirmEmail,
+ password: enteredPassword,
+ confirmPassword: enteredConfirmPassword,
+ });
+ }
+
+ return (
+
+
+
+ {!isLogin && (
+
+ )}
+
+ {!isLogin && (
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default AuthForm;
+
+const styles = StyleSheet.create({
+ buttons: {
+ marginTop: 12,
+ },
+});
diff --git a/code/09-accessing-protected-resources/components/Auth/Input.js b/code/09-accessing-protected-resources/components/Auth/Input.js
new file mode 100644
index 00000000..3aa0d5dd
--- /dev/null
+++ b/code/09-accessing-protected-resources/components/Auth/Input.js
@@ -0,0 +1,54 @@
+import { View, Text, TextInput, StyleSheet } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Input({
+ label,
+ keyboardType,
+ secure,
+ onUpdateValue,
+ value,
+ isInvalid,
+}) {
+ return (
+
+
+ {label}
+
+
+
+ );
+}
+
+export default Input;
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ marginVertical: 8,
+ },
+ label: {
+ color: 'white',
+ marginBottom: 4,
+ },
+ labelInvalid: {
+ color: Colors.error500,
+ },
+ input: {
+ paddingVertical: 8,
+ paddingHorizontal: 6,
+ backgroundColor: Colors.primary100,
+ borderRadius: 4,
+ fontSize: 16,
+ },
+ inputInvalid: {
+ backgroundColor: Colors.error100,
+ },
+});
diff --git a/code/09-accessing-protected-resources/components/ui/Button.js b/code/09-accessing-protected-resources/components/ui/Button.js
new file mode 100644
index 00000000..9b4b231d
--- /dev/null
+++ b/code/09-accessing-protected-resources/components/ui/Button.js
@@ -0,0 +1,41 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Button({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default Button;
+
+const styles = StyleSheet.create({
+ button: {
+ borderRadius: 6,
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ backgroundColor: Colors.primary500,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: 'white',
+ fontSize: 16,
+ fontWeight: 'bold'
+ },
+});
diff --git a/code/09-accessing-protected-resources/components/ui/FlatButton.js b/code/09-accessing-protected-resources/components/ui/FlatButton.js
new file mode 100644
index 00000000..1b45e9ec
--- /dev/null
+++ b/code/09-accessing-protected-resources/components/ui/FlatButton.js
@@ -0,0 +1,32 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function FlatButton({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default FlatButton;
+
+const styles = StyleSheet.create({
+ button: {
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: Colors.primary100,
+ },
+});
diff --git a/code/09-accessing-protected-resources/components/ui/IconButton.js b/code/09-accessing-protected-resources/components/ui/IconButton.js
new file mode 100644
index 00000000..7f438db4
--- /dev/null
+++ b/code/09-accessing-protected-resources/components/ui/IconButton.js
@@ -0,0 +1,25 @@
+import { Pressable, StyleSheet } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+
+function IconButton({ icon, color, size, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+
+ );
+}
+
+export default IconButton;
+
+const styles = StyleSheet.create({
+ button: {
+ margin: 8,
+ borderRadius: 20,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+});
diff --git a/code/09-accessing-protected-resources/components/ui/LoadingOverlay.js b/code/09-accessing-protected-resources/components/ui/LoadingOverlay.js
new file mode 100644
index 00000000..99ae9528
--- /dev/null
+++ b/code/09-accessing-protected-resources/components/ui/LoadingOverlay.js
@@ -0,0 +1,25 @@
+import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
+
+function LoadingOverlay({ message }) {
+ return (
+
+ {message}
+
+
+ );
+}
+
+export default LoadingOverlay;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ message: {
+ fontSize: 16,
+ marginBottom: 12,
+ },
+});
diff --git a/code/09-accessing-protected-resources/constants/styles.js b/code/09-accessing-protected-resources/constants/styles.js
new file mode 100644
index 00000000..0ae92d20
--- /dev/null
+++ b/code/09-accessing-protected-resources/constants/styles.js
@@ -0,0 +1,7 @@
+export const Colors = {
+ primary100: '#f9beda',
+ primary500: '#c30b64',
+ primary800: '#610440',
+ error100: '#fcdcbf',
+ error500: '#f37c13',
+}
\ No newline at end of file
diff --git a/code/09-accessing-protected-resources/package.json b/code/09-accessing-protected-resources/package.json
new file mode 100644
index 00000000..85a602c5
--- /dev/null
+++ b/code/09-accessing-protected-resources/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-navigation/native": "^6.0.8",
+ "@react-navigation/native-stack": "^6.5.0",
+ "axios": "^0.26.0",
+ "expo": "~44.0.0",
+ "expo-status-bar": "~1.2.0",
+ "react": "17.0.1",
+ "react-dom": "17.0.1",
+ "react-native": "0.64.3",
+ "react-native-web": "0.17.1"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.12.9"
+ },
+ "private": true
+}
diff --git a/code/09-accessing-protected-resources/screens/LoginScreen.js b/code/09-accessing-protected-resources/screens/LoginScreen.js
new file mode 100644
index 00000000..ff128968
--- /dev/null
+++ b/code/09-accessing-protected-resources/screens/LoginScreen.js
@@ -0,0 +1,35 @@
+import { useContext, useState } from 'react';
+import { Alert } from 'react-native';
+
+import AuthContent from '../components/Auth/AuthContent';
+import LoadingOverlay from '../components/ui/LoadingOverlay';
+import { AuthContext } from '../store/auth-context';
+import { login } from '../util/auth';
+
+function LoginScreen() {
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+
+ const authCtx = useContext(AuthContext);
+
+ async function loginHandler({ email, password }) {
+ setIsAuthenticating(true);
+ try {
+ const token = await login(email, password);
+ authCtx.authenticate(token);
+ } catch (error) {
+ Alert.alert(
+ 'Authentication failed!',
+ 'Could not log you in. Please check your credentials or try again later!'
+ );
+ setIsAuthenticating(false);
+ }
+ }
+
+ if (isAuthenticating) {
+ return ;
+ }
+
+ return ;
+}
+
+export default LoginScreen;
diff --git a/code/09-accessing-protected-resources/screens/SignupScreen.js b/code/09-accessing-protected-resources/screens/SignupScreen.js
new file mode 100644
index 00000000..4d9e7b86
--- /dev/null
+++ b/code/09-accessing-protected-resources/screens/SignupScreen.js
@@ -0,0 +1,35 @@
+import { useContext, useState } from 'react';
+import { Alert } from 'react-native';
+
+import AuthContent from '../components/Auth/AuthContent';
+import LoadingOverlay from '../components/ui/LoadingOverlay';
+import { AuthContext } from '../store/auth-context';
+import { createUser } from '../util/auth';
+
+function SignupScreen() {
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+
+ const authCtx = useContext(AuthContext);
+
+ async function signupHandler({ email, password }) {
+ setIsAuthenticating(true);
+ try {
+ const token = await createUser(email, password);
+ authCtx.authenticate(token);
+ } catch (error) {
+ Alert.alert(
+ 'Authentication failed',
+ 'Could not create user, please check your input and try again later.'
+ );
+ setIsAuthenticating(false);
+ }
+ }
+
+ if (isAuthenticating) {
+ return ;
+ }
+
+ return ;
+}
+
+export default SignupScreen;
diff --git a/code/09-accessing-protected-resources/screens/WelcomeScreen.js b/code/09-accessing-protected-resources/screens/WelcomeScreen.js
new file mode 100644
index 00000000..258fb35a
--- /dev/null
+++ b/code/09-accessing-protected-resources/screens/WelcomeScreen.js
@@ -0,0 +1,47 @@
+import axios from 'axios';
+import { useContext, useEffect, useState } from 'react';
+
+import { StyleSheet, Text, View } from 'react-native';
+import { AuthContext } from '../store/auth-context';
+
+function WelcomeScreen() {
+ const [fetchedMessage, setFetchedMesssage] = useState('');
+
+ const authCtx = useContext(AuthContext);
+ const token = authCtx.token;
+
+ useEffect(() => {
+ axios
+ .get(
+ 'https://react-native-course-3cceb-default-rtdb.firebaseio.com/message.json?auth=' +
+ token
+ )
+ .then((response) => {
+ setFetchedMesssage(response.data);
+ });
+ }, [token]);
+
+ return (
+
+ Welcome!
+ You authenticated successfully!
+ {fetchedMessage}
+
+ );
+}
+
+export default WelcomeScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ title: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ },
+});
diff --git a/code/09-accessing-protected-resources/store/auth-context.js b/code/09-accessing-protected-resources/store/auth-context.js
new file mode 100644
index 00000000..8105cb78
--- /dev/null
+++ b/code/09-accessing-protected-resources/store/auth-context.js
@@ -0,0 +1,31 @@
+import { createContext, useState } from 'react';
+
+export const AuthContext = createContext({
+ token: '',
+ isAuthenticated: false,
+ authenticate: (token) => {},
+ logout: () => {},
+});
+
+function AuthContextProvider({ children }) {
+ const [authToken, setAuthToken] = useState();
+
+ function authenticate(token) {
+ setAuthToken(token);
+ }
+
+ function logout() {
+ setAuthToken(null);
+ }
+
+ const value = {
+ token: authToken,
+ isAuthenticated: !!authToken,
+ authenticate: authenticate,
+ logout: logout,
+ };
+
+ return {children};
+}
+
+export default AuthContextProvider;
diff --git a/code/09-accessing-protected-resources/util/auth.js b/code/09-accessing-protected-resources/util/auth.js
new file mode 100644
index 00000000..a21ffc4e
--- /dev/null
+++ b/code/09-accessing-protected-resources/util/auth.js
@@ -0,0 +1,25 @@
+import axios from 'axios';
+
+const API_KEY = 'AIzaSyDCYasArcOwcALFhIj2szug5aD2PgUQu1E';
+
+async function authenticate(mode, email, password) {
+ const url = `https://identitytoolkit.googleapis.com/v1/accounts:${mode}?key=${API_KEY}`;
+
+ const response = await axios.post(url, {
+ email: email,
+ password: password,
+ returnSecureToken: true,
+ });
+
+ const token = response.data.idToken;
+
+ return token;
+}
+
+export function createUser(email, password) {
+ return authenticate('signUp', email, password);
+}
+
+export function login(email, password) {
+ return authenticate('signInWithPassword', email, password);
+}
\ No newline at end of file
diff --git a/code/10-storing-auth-tokens-on-the-device/App.js b/code/10-storing-auth-tokens-on-the-device/App.js
new file mode 100644
index 00000000..c398d132
--- /dev/null
+++ b/code/10-storing-auth-tokens-on-the-device/App.js
@@ -0,0 +1,107 @@
+import { useContext, useEffect, useState } from 'react';
+import { NavigationContainer } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { StatusBar } from 'expo-status-bar';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import AppLoading from 'expo-app-loading';
+
+import LoginScreen from './screens/LoginScreen';
+import SignupScreen from './screens/SignupScreen';
+import WelcomeScreen from './screens/WelcomeScreen';
+import { Colors } from './constants/styles';
+import AuthContextProvider, { AuthContext } from './store/auth-context';
+import IconButton from './components/ui/IconButton';
+
+const Stack = createNativeStackNavigator();
+
+function AuthStack() {
+ return (
+
+
+
+
+ );
+}
+
+function AuthenticatedStack() {
+ const authCtx = useContext(AuthContext);
+ return (
+
+ (
+
+ ),
+ }}
+ />
+
+ );
+}
+
+function Navigation() {
+ const authCtx = useContext(AuthContext);
+
+ return (
+
+ {!authCtx.isAuthenticated && }
+ {authCtx.isAuthenticated && }
+
+ );
+}
+
+function Root() {
+ const [isTryingLogin, setIsTryingLogin] = useState(true);
+
+ const authCtx = useContext(AuthContext);
+
+ useEffect(() => {
+ async function fetchToken() {
+ const storedToken = await AsyncStorage.getItem('token');
+
+ if (storedToken) {
+ authCtx.authenticate(storedToken);
+ }
+
+ setIsTryingLogin(false);
+ }
+
+ fetchToken();
+ }, []);
+
+ if (isTryingLogin) {
+ return ;
+ }
+
+ return ;
+}
+
+export default function App() {
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/code/10-storing-auth-tokens-on-the-device/app.json b/code/10-storing-auth-tokens-on-the-device/app.json
new file mode 100644
index 00000000..9a1223e7
--- /dev/null
+++ b/code/10-storing-auth-tokens-on-the-device/app.json
@@ -0,0 +1,32 @@
+{
+ "expo": {
+ "name": "RNCourse",
+ "slug": "RNCourse",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "updates": {
+ "fallbackToCacheTimeout": 0
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FFFFFF"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/code/10-storing-auth-tokens-on-the-device/assets/adaptive-icon.png b/code/10-storing-auth-tokens-on-the-device/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/code/10-storing-auth-tokens-on-the-device/assets/adaptive-icon.png differ
diff --git a/code/10-storing-auth-tokens-on-the-device/assets/favicon.png b/code/10-storing-auth-tokens-on-the-device/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/code/10-storing-auth-tokens-on-the-device/assets/favicon.png differ
diff --git a/code/10-storing-auth-tokens-on-the-device/assets/icon.png b/code/10-storing-auth-tokens-on-the-device/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/code/10-storing-auth-tokens-on-the-device/assets/icon.png differ
diff --git a/code/10-storing-auth-tokens-on-the-device/assets/splash.png b/code/10-storing-auth-tokens-on-the-device/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/code/10-storing-auth-tokens-on-the-device/assets/splash.png differ
diff --git a/code/10-storing-auth-tokens-on-the-device/babel.config.js b/code/10-storing-auth-tokens-on-the-device/babel.config.js
new file mode 100644
index 00000000..2900afe9
--- /dev/null
+++ b/code/10-storing-auth-tokens-on-the-device/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/code/10-storing-auth-tokens-on-the-device/components/Auth/AuthContent.js b/code/10-storing-auth-tokens-on-the-device/components/Auth/AuthContent.js
new file mode 100644
index 00000000..8eb04b4f
--- /dev/null
+++ b/code/10-storing-auth-tokens-on-the-device/components/Auth/AuthContent.js
@@ -0,0 +1,89 @@
+import { useState } from 'react';
+import { Alert, StyleSheet, View } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+
+import FlatButton from '../ui/FlatButton';
+import AuthForm from './AuthForm';
+import { Colors } from '../../constants/styles';
+
+function AuthContent({ isLogin, onAuthenticate }) {
+ const navigation = useNavigation();
+
+ const [credentialsInvalid, setCredentialsInvalid] = useState({
+ email: false,
+ password: false,
+ confirmEmail: false,
+ confirmPassword: false,
+ });
+
+ function switchAuthModeHandler() {
+ if (isLogin) {
+ navigation.replace('Signup');
+ } else {
+ navigation.replace('Login');
+ }
+ }
+
+ function submitHandler(credentials) {
+ let { email, confirmEmail, password, confirmPassword } = credentials;
+
+ email = email.trim();
+ password = password.trim();
+
+ const emailIsValid = email.includes('@');
+ const passwordIsValid = password.length > 6;
+ const emailsAreEqual = email === confirmEmail;
+ const passwordsAreEqual = password === confirmPassword;
+
+ if (
+ !emailIsValid ||
+ !passwordIsValid ||
+ (!isLogin && (!emailsAreEqual || !passwordsAreEqual))
+ ) {
+ Alert.alert('Invalid input', 'Please check your entered credentials.');
+ setCredentialsInvalid({
+ email: !emailIsValid,
+ confirmEmail: !emailIsValid || !emailsAreEqual,
+ password: !passwordIsValid,
+ confirmPassword: !passwordIsValid || !passwordsAreEqual,
+ });
+ return;
+ }
+ onAuthenticate({ email, password });
+ }
+
+ return (
+
+
+
+
+ {isLogin ? 'Create a new user' : 'Log in instead'}
+
+
+
+ );
+}
+
+export default AuthContent;
+
+const styles = StyleSheet.create({
+ authContent: {
+ marginTop: 64,
+ marginHorizontal: 32,
+ padding: 16,
+ borderRadius: 8,
+ backgroundColor: Colors.primary800,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.35,
+ shadowRadius: 4,
+ },
+ buttons: {
+ marginTop: 8,
+ },
+});
diff --git a/code/10-storing-auth-tokens-on-the-device/components/Auth/AuthForm.js b/code/10-storing-auth-tokens-on-the-device/components/Auth/AuthForm.js
new file mode 100644
index 00000000..cf4a7a81
--- /dev/null
+++ b/code/10-storing-auth-tokens-on-the-device/components/Auth/AuthForm.js
@@ -0,0 +1,100 @@
+import { useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import Button from '../ui/Button';
+import Input from './Input';
+
+function AuthForm({ isLogin, onSubmit, credentialsInvalid }) {
+ const [enteredEmail, setEnteredEmail] = useState('');
+ const [enteredConfirmEmail, setEnteredConfirmEmail] = useState('');
+ const [enteredPassword, setEnteredPassword] = useState('');
+ const [enteredConfirmPassword, setEnteredConfirmPassword] = useState('');
+
+ const {
+ email: emailIsInvalid,
+ confirmEmail: emailsDontMatch,
+ password: passwordIsInvalid,
+ confirmPassword: passwordsDontMatch,
+ } = credentialsInvalid;
+
+ function updateInputValueHandler(inputType, enteredValue) {
+ switch (inputType) {
+ case 'email':
+ setEnteredEmail(enteredValue);
+ break;
+ case 'confirmEmail':
+ setEnteredConfirmEmail(enteredValue);
+ break;
+ case 'password':
+ setEnteredPassword(enteredValue);
+ break;
+ case 'confirmPassword':
+ setEnteredConfirmPassword(enteredValue);
+ break;
+ }
+ }
+
+ function submitHandler() {
+ onSubmit({
+ email: enteredEmail,
+ confirmEmail: enteredConfirmEmail,
+ password: enteredPassword,
+ confirmPassword: enteredConfirmPassword,
+ });
+ }
+
+ return (
+
+
+
+ {!isLogin && (
+
+ )}
+
+ {!isLogin && (
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default AuthForm;
+
+const styles = StyleSheet.create({
+ buttons: {
+ marginTop: 12,
+ },
+});
diff --git a/code/10-storing-auth-tokens-on-the-device/components/Auth/Input.js b/code/10-storing-auth-tokens-on-the-device/components/Auth/Input.js
new file mode 100644
index 00000000..3aa0d5dd
--- /dev/null
+++ b/code/10-storing-auth-tokens-on-the-device/components/Auth/Input.js
@@ -0,0 +1,54 @@
+import { View, Text, TextInput, StyleSheet } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Input({
+ label,
+ keyboardType,
+ secure,
+ onUpdateValue,
+ value,
+ isInvalid,
+}) {
+ return (
+
+
+ {label}
+
+
+
+ );
+}
+
+export default Input;
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ marginVertical: 8,
+ },
+ label: {
+ color: 'white',
+ marginBottom: 4,
+ },
+ labelInvalid: {
+ color: Colors.error500,
+ },
+ input: {
+ paddingVertical: 8,
+ paddingHorizontal: 6,
+ backgroundColor: Colors.primary100,
+ borderRadius: 4,
+ fontSize: 16,
+ },
+ inputInvalid: {
+ backgroundColor: Colors.error100,
+ },
+});
diff --git a/code/10-storing-auth-tokens-on-the-device/components/ui/Button.js b/code/10-storing-auth-tokens-on-the-device/components/ui/Button.js
new file mode 100644
index 00000000..9b4b231d
--- /dev/null
+++ b/code/10-storing-auth-tokens-on-the-device/components/ui/Button.js
@@ -0,0 +1,41 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function Button({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default Button;
+
+const styles = StyleSheet.create({
+ button: {
+ borderRadius: 6,
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ backgroundColor: Colors.primary500,
+ elevation: 2,
+ shadowColor: 'black',
+ shadowOffset: { width: 1, height: 1 },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: 'white',
+ fontSize: 16,
+ fontWeight: 'bold'
+ },
+});
diff --git a/code/10-storing-auth-tokens-on-the-device/components/ui/FlatButton.js b/code/10-storing-auth-tokens-on-the-device/components/ui/FlatButton.js
new file mode 100644
index 00000000..1b45e9ec
--- /dev/null
+++ b/code/10-storing-auth-tokens-on-the-device/components/ui/FlatButton.js
@@ -0,0 +1,32 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+import { Colors } from '../../constants/styles';
+
+function FlatButton({ children, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+ {children}
+
+
+ );
+}
+
+export default FlatButton;
+
+const styles = StyleSheet.create({
+ button: {
+ paddingVertical: 6,
+ paddingHorizontal: 12,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+ buttonText: {
+ textAlign: 'center',
+ color: Colors.primary100,
+ },
+});
diff --git a/code/10-storing-auth-tokens-on-the-device/components/ui/IconButton.js b/code/10-storing-auth-tokens-on-the-device/components/ui/IconButton.js
new file mode 100644
index 00000000..7f438db4
--- /dev/null
+++ b/code/10-storing-auth-tokens-on-the-device/components/ui/IconButton.js
@@ -0,0 +1,25 @@
+import { Pressable, StyleSheet } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+
+function IconButton({ icon, color, size, onPress }) {
+ return (
+ [styles.button, pressed && styles.pressed]}
+ onPress={onPress}
+ >
+
+
+ );
+}
+
+export default IconButton;
+
+const styles = StyleSheet.create({
+ button: {
+ margin: 8,
+ borderRadius: 20,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+});
diff --git a/code/10-storing-auth-tokens-on-the-device/components/ui/LoadingOverlay.js b/code/10-storing-auth-tokens-on-the-device/components/ui/LoadingOverlay.js
new file mode 100644
index 00000000..99ae9528
--- /dev/null
+++ b/code/10-storing-auth-tokens-on-the-device/components/ui/LoadingOverlay.js
@@ -0,0 +1,25 @@
+import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
+
+function LoadingOverlay({ message }) {
+ return (
+
+ {message}
+
+
+ );
+}
+
+export default LoadingOverlay;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ message: {
+ fontSize: 16,
+ marginBottom: 12,
+ },
+});
diff --git a/code/10-storing-auth-tokens-on-the-device/constants/styles.js b/code/10-storing-auth-tokens-on-the-device/constants/styles.js
new file mode 100644
index 00000000..0ae92d20
--- /dev/null
+++ b/code/10-storing-auth-tokens-on-the-device/constants/styles.js
@@ -0,0 +1,7 @@
+export const Colors = {
+ primary100: '#f9beda',
+ primary500: '#c30b64',
+ primary800: '#610440',
+ error100: '#fcdcbf',
+ error500: '#f37c13',
+}
\ No newline at end of file
diff --git a/code/10-storing-auth-tokens-on-the-device/package.json b/code/10-storing-auth-tokens-on-the-device/package.json
new file mode 100644
index 00000000..40f20a47
--- /dev/null
+++ b/code/10-storing-auth-tokens-on-the-device/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "rncourse",
+ "version": "1.0.0",
+ "main": "node_modules/expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "eject": "expo eject"
+ },
+ "dependencies": {
+ "@react-navigation/native": "^6.0.8",
+ "@react-navigation/native-stack": "^6.5.0",
+ "@react-native-async-storage/async-storage": "^1.16.1",
+ "axios": "^0.26.0",
+ "expo": "~44.0.0",
+ "expo-status-bar": "~1.2.0",
+ "react": "17.0.1",
+ "react-dom": "17.0.1",
+ "react-native": "0.64.3",
+ "react-native-web": "0.17.1",
+ "expo-app-loading": "~1.3.0"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.12.9"
+ },
+ "private": true
+}
diff --git a/code/10-storing-auth-tokens-on-the-device/screens/LoginScreen.js b/code/10-storing-auth-tokens-on-the-device/screens/LoginScreen.js
new file mode 100644
index 00000000..ff128968
--- /dev/null
+++ b/code/10-storing-auth-tokens-on-the-device/screens/LoginScreen.js
@@ -0,0 +1,35 @@
+import { useContext, useState } from 'react';
+import { Alert } from 'react-native';
+
+import AuthContent from '../components/Auth/AuthContent';
+import LoadingOverlay from '../components/ui/LoadingOverlay';
+import { AuthContext } from '../store/auth-context';
+import { login } from '../util/auth';
+
+function LoginScreen() {
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+
+ const authCtx = useContext(AuthContext);
+
+ async function loginHandler({ email, password }) {
+ setIsAuthenticating(true);
+ try {
+ const token = await login(email, password);
+ authCtx.authenticate(token);
+ } catch (error) {
+ Alert.alert(
+ 'Authentication failed!',
+ 'Could not log you in. Please check your credentials or try again later!'
+ );
+ setIsAuthenticating(false);
+ }
+ }
+
+ if (isAuthenticating) {
+ return ;
+ }
+
+ return ;
+}
+
+export default LoginScreen;
diff --git a/code/10-storing-auth-tokens-on-the-device/screens/SignupScreen.js b/code/10-storing-auth-tokens-on-the-device/screens/SignupScreen.js
new file mode 100644
index 00000000..4d9e7b86
--- /dev/null
+++ b/code/10-storing-auth-tokens-on-the-device/screens/SignupScreen.js
@@ -0,0 +1,35 @@
+import { useContext, useState } from 'react';
+import { Alert } from 'react-native';
+
+import AuthContent from '../components/Auth/AuthContent';
+import LoadingOverlay from '../components/ui/LoadingOverlay';
+import { AuthContext } from '../store/auth-context';
+import { createUser } from '../util/auth';
+
+function SignupScreen() {
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
+
+ const authCtx = useContext(AuthContext);
+
+ async function signupHandler({ email, password }) {
+ setIsAuthenticating(true);
+ try {
+ const token = await createUser(email, password);
+ authCtx.authenticate(token);
+ } catch (error) {
+ Alert.alert(
+ 'Authentication failed',
+ 'Could not create user, please check your input and try again later.'
+ );
+ setIsAuthenticating(false);
+ }
+ }
+
+ if (isAuthenticating) {
+ return ;
+ }
+
+ return ;
+}
+
+export default SignupScreen;
diff --git a/code/10-storing-auth-tokens-on-the-device/screens/WelcomeScreen.js b/code/10-storing-auth-tokens-on-the-device/screens/WelcomeScreen.js
new file mode 100644
index 00000000..258fb35a
--- /dev/null
+++ b/code/10-storing-auth-tokens-on-the-device/screens/WelcomeScreen.js
@@ -0,0 +1,47 @@
+import axios from 'axios';
+import { useContext, useEffect, useState } from 'react';
+
+import { StyleSheet, Text, View } from 'react-native';
+import { AuthContext } from '../store/auth-context';
+
+function WelcomeScreen() {
+ const [fetchedMessage, setFetchedMesssage] = useState('');
+
+ const authCtx = useContext(AuthContext);
+ const token = authCtx.token;
+
+ useEffect(() => {
+ axios
+ .get(
+ 'https://react-native-course-3cceb-default-rtdb.firebaseio.com/message.json?auth=' +
+ token
+ )
+ .then((response) => {
+ setFetchedMesssage(response.data);
+ });
+ }, [token]);
+
+ return (
+
+ Welcome!
+ You authenticated successfully!
+ {fetchedMessage}
+
+ );
+}
+
+export default WelcomeScreen;
+
+const styles = StyleSheet.create({
+ rootContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 32,
+ },
+ title: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ },
+});
diff --git a/code/10-storing-auth-tokens-on-the-device/store/auth-context.js b/code/10-storing-auth-tokens-on-the-device/store/auth-context.js
new file mode 100644
index 00000000..9a70df11
--- /dev/null
+++ b/code/10-storing-auth-tokens-on-the-device/store/auth-context.js
@@ -0,0 +1,35 @@
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+import { createContext, useEffect, useState } from 'react';
+
+export const AuthContext = createContext({
+ token: '',
+ isAuthenticated: false,
+ authenticate: (token) => {},
+ logout: () => {},
+});
+
+function AuthContextProvider({ children }) {
+ const [authToken, setAuthToken] = useState();
+
+ function authenticate(token) {
+ setAuthToken(token);
+ AsyncStorage.setItem('token', token);
+ }
+
+ function logout() {
+ setAuthToken(null);
+ AsyncStorage.removeItem('token');
+ }
+
+ const value = {
+ token: authToken,
+ isAuthenticated: !!authToken,
+ authenticate: authenticate,
+ logout: logout,
+ };
+
+ return {children};
+}
+
+export default AuthContextProvider;
diff --git a/code/10-storing-auth-tokens-on-the-device/util/auth.js b/code/10-storing-auth-tokens-on-the-device/util/auth.js
new file mode 100644
index 00000000..a21ffc4e
--- /dev/null
+++ b/code/10-storing-auth-tokens-on-the-device/util/auth.js
@@ -0,0 +1,25 @@
+import axios from 'axios';
+
+const API_KEY = 'AIzaSyDCYasArcOwcALFhIj2szug5aD2PgUQu1E';
+
+async function authenticate(mode, email, password) {
+ const url = `https://identitytoolkit.googleapis.com/v1/accounts:${mode}?key=${API_KEY}`;
+
+ const response = await axios.post(url, {
+ email: email,
+ password: password,
+ returnSecureToken: true,
+ });
+
+ const token = response.data.idToken;
+
+ return token;
+}
+
+export function createUser(email, password) {
+ return authenticate('signUp', email, password);
+}
+
+export function login(email, password) {
+ return authenticate('signInWithPassword', email, password);
+}
\ No newline at end of file
diff --git a/slides/slides.pdf b/slides/slides.pdf
new file mode 100644
index 00000000..405729f3
Binary files /dev/null and b/slides/slides.pdf differ