https://www.obytes.com/blog/authentication-in-react-native-easy-secure-and-reusable-solution
/// Auth.tsx
import * as React from 'react'
import { getToken, setToken, removeToken } from './utils.tsx'
interface AuthState {
userToken: string | undefined | null
status: 'idle' | 'signOut' | 'signIn'
}
type AuthAction = { type: 'SIGN_IN'; token: string } | { type: 'SIGN_OUT' }
type AuthPayload = string
interface AuthContextActions {
signIn: (data: AuthPayload) => void
signOut: () => void
}
interface AuthContextType extends AuthState, AuthContextActions {}
const AuthContext = React.createContext<AuthContextType>({
status: 'idle',
userToken: null,
signIn: () => {},
signOut: () => {},
})
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const [state, dispatch] = React.useReducer(AuthReducer, {
status: 'idle',
userToken: null,
})
React.useEffect(() => {
const initState = async () => {
try {
const userToken = await getToken()
if (userToken !== null) {
dispatch({ type: 'SIGN_IN', token: userToken })
} else {
dispatch({ type: 'SIGN_OUT' })
}
} catch (e) {
// catch error here
// Maybe sign_out user!
}
}
initState()
}, [])
const authActions: AuthContextActions = React.useMemo(
() => ({
signIn: async (token: string) => {
dispatch({ type: 'SIGN_IN', token })
await setToken(token)
},
signOut: async () => {
await removeToken() // TODO: use Vars
dispatch({ type: 'SIGN_OUT' })
},
}),
[]
)
return (
<AuthContext.Provider value={{ ...state, ...authActions }}>
{children}
</AuthContext.Provider>
)
}
const AuthReducer = (prevState: AuthState, action: AuthAction): AuthState => {
switch (action.type) {
case 'SIGN_IN':
return {
...prevState,
status: 'signIn',
userToken: action.token,
}
case 'SIGN_OUT':
return {
...prevState,
status: 'signOut',
userToken: null,
}
}
}
export const useAuth = (): AuthContextType => {
const context = React.useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be inside an AuthProvider with a value')
}
/*
you can add more drived state here
const isLoggedIn = context.status ==== 'signIn'
return ({ ...context, isloggedIn})
*/
return context
}
yarn add react-native-sensitive-info@next
//utils.tsx
import SInfo from 'react-native-sensitive-info';
const TOKEN = 'token';
const SHARED_PERFS = 'ObytesSharedPerfs';
const KEYCHAIN_SERVICE = 'ObytesKeychain';
const keyChainOptions = {
sharedPreferencesName: SHARED_PERFS,
keychainService: KEYCHAIN_SERVICE,
};
export async function getItem<T>(key: string): Promise<T | null> {
const value = await SInfo.getItem(key, keyChainOptions);
return value ? JSON.parse(value)?.[key] || null : null;
}
export async function setItem<T>(key: string, value: T): Promise<void> {
SInfo.setItem(key, JSON.stringify({[key]: value}), keyChainOptions);
}
export async function removeItem(key: string): Promise<void> {
SInfo.deleteItem(key, keyChainOptions);
}
export const getToken = () => getItem<string>(TOKEN);
export const removeToken = () => removeItem(TOKEN);
export const setToken = (value: string) => setItem<string>(TOKEN, value);
// Auth.tsx
// In case you want to use Auth functions outside React tree
export const AuthRef = React.createRef<AuthContextActions>();
export const AuthProvider = ({children}: {children: React.ReactNode}) => {
....
// we add all Auth Action to ref
React.useImperativeHandle(AuthRef, () => authActions);
const authActions: AuthContextActions = React.useMemo(
() => ({
signIn: async (token: string) => {
dispatch({type: 'SIGN_IN', token});
await setToken(token);
},
signOut: async () => {
await removeToken(); // TODO: use Vars
dispatch({type: 'SIGN_OUT'});
},
}),
[],
);
return (
<AuthContext.Provider value={{...state, ...authActions}}>
{children}
</AuthContext.Provider>
);
};
/*
you can eaisly import AuthRef and start using Auth actions
AuthRef.current.signOut()
*/
// App.tsx
import * as React from 'react'
import { Text, View, StyleSheet, Button } from 'react-native'
import { AuthProvider, useAuth, AuthRef } from './Auth'
// you can access to Auth action directly from AuthRef
// AuthRef.current.signOut()
const LogOutButton = () => {
const { signOut } = useAuth()
return <Button title="log Out" onPress={signOut} />
}
const LogInButton = () => {
const { signIn } = useAuth()
return <Button title="log IN" onPress={() => signIn('my_token')} />
}
const Main = () => {
const { status, userToken } = useAuth()
return (
<View style={styles.container}>
<Text style={styles.text}>status : {status}</Text>
<Text style={styles.text}>
userToken : {userToken ? userToken : 'null'}
</Text>
<View style={styles.actions}>
<LogInButton />
<LogOutButton />
</View>
</View>
)
}
export default function App() {
return (
<AuthProvider>
<Main />
</AuthProvider>
)
}
https://reactnavigation.org/docs/auth-flow/
isSignedIn ? (
<>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
<Stack.Screen name="Settings" component={SettingsScreen} />
</>
) : (
<>
<Stack.Screen name="SignIn" component={SignInScreen} />
<Stack.Screen name="SignUp" component={SignUpScreen} />
</>
)
state.userToken == null ? (
<>
<Stack.Screen name="SignIn" component={SignInScreen} />
<Stack.Screen name="SignUp" component={SignUpScreen} />
<Stack.Screen name="ResetPassword" component={ResetPassword} />
</>
) : (
<>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</>
);
React.useReducer
, React.useContext
활용 가능import * as React from 'react';
const AuthContext = React.createContext();
import * as React from 'react';
import * as SecureStore from 'expo-secure-store';
export default function App({ navigation }) {
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
userToken: action.token,
};
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
userToken: null,
};
}
},
{
isLoading: true,
isSignout: false,
userToken: null,
}
);
React.useEffect(() => {
// Fetch the token from storage then navigate to our appropriate place
const bootstrapAsync = async () => {
let userToken;
try {
userToken = await SecureStore.getItemAsync('userToken');
} 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 });
};
bootstrapAsync();
}, []);
const authContext = React.useMemo(
() => ({
signIn: async data => {
// 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 => {
// 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' });
},
}),
[]
);
return (
<AuthContext.Provider value={authContext}>
<Stack.Navigator>
{state.userToken == null ? (
<Stack.Screen name="SignIn" component={SignInScreen} />
) : (
<Stack.Screen name="Home" component={HomeScreen} />
)}
</Stack.Navigator>
</AuthContext.Provider>
);
}
function SignInScreen() {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const { signIn } = React.useContext(AuthContext);
return (
<View>
<TextInput
placeholder="Username"
value={username}
onChangeText={setUsername}
/>
<TextInput
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<Button title="Sign in" onPress={() => signIn({ username, password })} />
</View>
);
}