Skip to content

Commit

Permalink
Context
Browse files Browse the repository at this point in the history
  • Loading branch information
ineshbose committed Jan 6, 2022
1 parent 8e89225 commit 6ad7ec2
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 134 deletions.
8 changes: 4 additions & 4 deletions src/frontend/api/auth.ts
@@ -1,13 +1,13 @@
import axios from 'axios';
import { storeObject, getObject } from './store';
import { AuthError, AuthResponse } from './types';
import { AuthError, AuthToken } from './types';
import { CLIENT_ID, CLIENT_SECRET } from 'react-native-dotenv';

const TOKEN_URL = 'http://127.0.0.1:8000/api/auth/o/token/';

export const getToken = async (username: string, password: string) => {
try {
const response = await axios.post<AuthResponse>(TOKEN_URL, {
const response = await axios.post<AuthToken>(TOKEN_URL, {
username,
password,
grant_type: 'password',
Expand All @@ -23,8 +23,8 @@ export const getToken = async (username: string, password: string) => {

export const refreshToken = async () => {
try {
const authToken = (await getObject('auth_token')) as AuthResponse;
const response = await axios.post<AuthResponse>(TOKEN_URL, {
const authToken = (await getObject('auth_token')) as AuthToken;
const response = await axios.post<AuthToken>(TOKEN_URL, {
refresh_token: authToken.refresh_token,
grant_type: 'refresh_token',
client_id: CLIENT_ID,
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/api/index.ts
@@ -1,6 +1,6 @@
import axios from 'axios';
import { getObject } from './store';
import { AuthResponse, PaginationData, TrackItem } from './types';
import { AuthToken, PaginationData, TrackItem } from './types';

const list: TrackItem[] = [
{
Expand Down Expand Up @@ -86,7 +86,7 @@ export const getTrackItems = async () => {
{
headers: {
Authorization: `Bearer ${
((await getObject('auth_token')) as AuthResponse).access_token
((await getObject('auth_token')) as AuthToken).access_token
}`,
},
}
Expand Down
8 changes: 8 additions & 0 deletions src/frontend/api/store.ts
Expand Up @@ -36,3 +36,11 @@ export const getObject = async (key: string) => {
// error reading value
}
};

export const removeItem = async (key: string) => {
try {
await AsyncStorage.removeItem(`@${key}`);
} catch (e) {
// error removing value
}
};
2 changes: 1 addition & 1 deletion src/frontend/api/types.ts
@@ -1,4 +1,4 @@
export type AuthResponse = {
export type AuthToken = {
access_token: string;
expires_in: number;
refresh_token: string;
Expand Down
63 changes: 63 additions & 0 deletions src/frontend/contexts/Auth.tsx
@@ -0,0 +1,63 @@
import * as React from 'react';
import { getToken } from '../api/auth';
import { getObject, removeItem } from '../api/store';
import { AuthToken } from '../api/types';

type AuthContextType = {
authToken?: AuthToken;
loading: boolean;
signIn: (u: string, p: string) => Promise<void>;
signOut: () => void;
};

export const AuthContext = React.createContext<AuthContextType>(
{} as AuthContextType
);

export const AuthProvider = ({ children }: { children: JSX.Element }) => {
const [authToken, setAuthToken] = React.useState<AuthToken>();
const [loading, setLoading] = React.useState<boolean>(true);

React.useEffect(() => {
const loadToken = async () => {
try {
const authData = (await getObject('auth_token')) as AuthToken;
setAuthToken(authData);
} catch (e) {
// handle error
}
};

loadToken().then(() => setLoading(false));
}, []);

const signIn = async (email: string, password: string) => {
try {
const authData = (await getToken(email, password)) as AuthToken;
setAuthToken(authData);
} catch (e) {
// handle error
}
};

const signOut = async () => {
setAuthToken(undefined);
removeItem('auth_token');
};

return (
<AuthContext.Provider value={{ authToken, loading, signIn, signOut }}>
{children}
</AuthContext.Provider>
);
};

export const useAuth = () => {
const context = React.useContext(AuthContext);

if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}

return context;
};
145 changes: 25 additions & 120 deletions src/frontend/navigation/RootNavigator.tsx
@@ -1,131 +1,36 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
createNativeStackNavigator,
NativeStackScreenProps,
} from '@react-navigation/native-stack';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import * as React from 'react';
import { RootLinkParamList } from '../types';
import BottomTabNavigator from './BottomTabNavigator';
import AuthNavigator from './AuthNavigator';
import { getObject } from '../api/store';
import { AuthResponse } from '../api/types';
import { AuthProvider, useAuth } from '../contexts/Auth';

const Root = createNativeStackNavigator<RootLinkParamList>();
const AuthContext = React.createContext<any>(() => {});

type AuthBaseState = {
userToken?: string | null;
isLoading?: boolean;
isSignout?: boolean;
};

type AuthState = AuthBaseState | undefined;

type AuthActionTypes = 'RESTORE_TOKEN' | 'SIGN_IN' | 'SIGN_OUT';

type AuthData = {
username: string;
password: string;
};

type AuthAction = { type: AuthActionTypes; token?: string };

type AuthReducer = (prevState: AuthState, action: AuthAction) => AuthState;

export default function RootNavigator({
navigation,
}: NativeStackScreenProps<RootLinkParamList>) {
const [isAuth, setIsAuth] = React.useState<boolean>();
const [state, dispatch] = React.useReducer<AuthReducer>(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
userToken: action.token,
isSignout: false,
};
case 'SIGN_OUT':
return {
...prevState,
userToken: null,
isSignout: true,
};
}
},
{
isLoading: true,
isSignout: false,
userToken: null,
}
);

React.useEffect(() => {
const bootstrapAsync = async () => {
let userToken: AuthResponse;

try {
userToken = (await getObject('auth_token')) as AuthResponse;
} catch (e) {
// Restoring token failed
}

// After restoring token, we may need to validate it in production apps

// This will switch to the App screen or Auth screen and this loading
// screen will be unmounted and thrown away.
dispatch({ type: 'RESTORE_TOKEN', token: userToken.access_token });
};

bootstrapAsync();
}, []);

const authContext = React.useMemo(
() => ({
signIn: async (data: AuthData) => {
// In a production app, we need to send some data (usually username, password) to server and get a token
// We will also need to handle errors if sign in failed
// After getting token, we need to persist the token using `SecureStore`
// In the example, we'll use a dummy token

dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: async (data: AuthData) => {
// In a production app, we need to send user data to server and get a token
// We will also need to handle errors if sign up failed
// After getting token, we need to persist the token using `SecureStore`
// In the example, we'll use a dummy token

dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);
export default function RootNavigator() {
const { authToken, loading } = useAuth();

return (
<AuthContext.Provider value={authContext}>
<Root.Navigator>
{state?.userToken ? (
<Root.Screen
name="BottomTab"
component={BottomTabNavigator}
options={{ headerShown: false }}
/>
) : (
<Root.Screen
name="Auth"
component={AuthNavigator}
options={{ headerShown: false }}
/>
)}
</Root.Navigator>
</AuthContext.Provider>
<AuthProvider>
{loading ? (
<></>
) : (
<Root.Navigator>
{authToken?.access_token ? (
<Root.Screen
name="BottomTab"
component={BottomTabNavigator}
options={{ headerShown: false }}
/>
) : (
<Root.Screen
name="Auth"
component={AuthNavigator}
options={{ headerShown: false }}
/>
)}
</Root.Navigator>
)}
</AuthProvider>
);
}
10 changes: 3 additions & 7 deletions src/frontend/screens/Auth/LoginForm.tsx
Expand Up @@ -3,13 +3,15 @@ import * as React from 'react';
import { Button, Card, Input } from 'react-native-elements';
import { getToken } from '../../api/auth';
import { AuthError } from '../../api/types';
import { useAuth } from '../../contexts/Auth';
import { RootAuthScreenProps } from '../../types';
import FormStyle from './FormStyle';
import Logo from './Logo';

export default function LoginForm({
navigation,
}: RootAuthScreenProps<'Login'>) {
const auth = useAuth();
const [email, setEmail] = React.useState<string>();
const [password, setPassword] = React.useState<string>();
const [error, setError] = React.useState<AuthError>();
Expand All @@ -33,13 +35,7 @@ export default function LoginForm({
title="log in"
buttonStyle={FormStyle.submit}
onPress={() =>
getToken(email as string, password as string)
.then(() =>
navigation.navigate('Root', {
screen: 'BottomTab',
})
)
.catch(setError)
auth.signIn(email as string, password as string).catch(setError)
}
/>
<Button
Expand Down

0 comments on commit 6ad7ec2

Please sign in to comment.