Skip to content

Commit

Permalink
refactor mobile auth
Browse files Browse the repository at this point in the history
  • Loading branch information
nickcherry committed Jan 31, 2024
1 parent c4516f1 commit bd607c8
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 127 deletions.
157 changes: 58 additions & 99 deletions mobile/src/contexts/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
AuthKitProvider,
StatusAPIResponse,
useSignIn,
} from '@farcaster/auth-kit';
import { AuthKitProvider } from '@farcaster/auth-kit';
import { FullscreenLoader } from '@mobile/components/loader/FullscreenLoader';
import { useFetchProfile } from '@mobile/hooks/data/profile';
import { User } from '@shared/types/models';
Expand All @@ -14,31 +10,15 @@ import {
useContext,
useEffect,
useReducer,
useRef,
} from 'react';
import { Linking } from 'react-native';

const AuthContext = createContext<{
currentUser: User | undefined;
isSignedIn: boolean;
requestSignIn: () => Promise<void>;
signOut: () => Promise<void>;
token: string | undefined;
}>({
currentUser: undefined,
isSignedIn: false,
requestSignIn: async () => {
throw new Error(
'You need to add an AuthProvider to the top of your React tree.',
);
},
signOut: async () => {
throw new Error(
'You need to add an AuthProvider to the top of your React tree.',
);
},
token: undefined,
});
const sessionKey = 'session';

type SignInParams = {
message: string;
nonce: string;
signature: string;
};

type Session = {
fid: string;
Expand All @@ -52,37 +32,42 @@ type State = {
};

type Action =
| { type: 'initWitUser'; session: Session; user: User }
| { type: 'initWithoutUser' }
| { type: 'signIn'; session: Session; user: User }
| { type: 'signOut' };

const AuthContext = createContext<{
currentUser: User | undefined;
isSignedIn: boolean;
signIn: (params: SignInParams) => Promise<void>;
signOut: () => Promise<void>;
}>({
currentUser: undefined,
isSignedIn: false,
signIn: async () => undefined,
signOut: async () => undefined,
});

const initialState: State = {
currentUser: undefined,
isInitialized: false,
session: undefined,
};

function reducer(state: State, action: Action): State {
function reducer(_state: State, action: Action): State {
switch (action.type) {
case 'initWitUser':
case 'signIn':
return {
currentUser: action.user,
isInitialized: true,
currentUser: action.user,
session: action.session,
};
case 'initWithoutUser':
case 'signOut':
return {
currentUser: undefined,
isInitialized: true,
session: undefined,
currentUser: undefined,
};
case 'signIn':
return { ...state, currentUser: action.user, session: action.session };
case 'signOut':
return { ...state, session: undefined, currentUser: undefined };
}
return state;
}

type AuthProviderProps = {
Expand All @@ -92,85 +77,60 @@ type AuthProviderProps = {
function AuthProviderContent({ children }: AuthProviderProps) {
const [state, dispatch] = useReducer(reducer, initialState);
const fetchProfile = useFetchProfile();
const hasConnectedRef = useRef(false);
const hasLinkedUserRef = useRef(false);

const {
connect,
url,
signIn: authKitSignIn,
} = useSignIn({
onSuccess: useCallback(
async (res: StatusAPIResponse) => {

const signIn = useCallback(
async (body: SignInParams) => {
const reject = (message: string) => {
throw new Error(`Sign in failed: ${message}`);
};
try {
const signInResponse = await fetch(
'http://localhost:3000/api/auth/sign-in',
{
method: 'POST',
body: JSON.stringify({
message: res.message,
nonce: res.nonce,
signature: res.signature,
}),
body: JSON.stringify(body),
},
);

if (signInResponse.ok) {
const { token, fid }: { token: string; fid: string } =
await signInResponse.json();

const { profile: user } = await fetchProfile({ fid });
dispatch({ type: 'signIn', session: { token, fid }, user });
} else {
alert('Sign in failed');
if (!signInResponse.ok) {
reject(await signInResponse.text());
}
},
[fetchProfile],
),
});

const requestSignIn = useCallback(async () => {
if (!hasConnectedRef.current) {
hasConnectedRef.current = true;
connect();
}
}, [connect, hasConnectedRef]);

useEffect(() => {
if (url) {
authKitSignIn();
if (!hasLinkedUserRef.current) {
hasLinkedUserRef.current = true;
Linking.openURL(url);
const session: Session = await signInResponse.json();
// await SecureStore.setItemAsync(sessionKey, JSON.stringify(session));
const { profile: user } = await fetchProfile({ fid: session.fid });
dispatch({ type: 'signIn', session, user });
} catch (error) {
reject((error as Error).message);
}
}
}, [authKitSignIn, url]);
},
[fetchProfile],
);

const signOut = useCallback(async () => {
await fetch('http://localhost:3000/api/auth/sign-out', { method: 'POST' });
}, []);
SecureStore.deleteItemAsync(sessionKey);
if (state.session) {
await fetch('http://localhost:3000/api/auth/sign-out', {
method: 'POST',
});
}

dispatch({ type: 'signOut' });
}, [state.session]);

const init = useCallback(async () => {
const persistedSessionJson = await SecureStore.getItemAsync('session');
const persistedSessionJson = await SecureStore.getItemAsync(sessionKey);

if (persistedSessionJson) {
try {
const persistedSession: Session = JSON.parse(persistedSessionJson);
const { profile: user } = await fetchProfile({
fid: persistedSession.fid,
});

return dispatch({
type: 'initWitUser',
session: persistedSession,
user,
});
signIn(JSON.parse(persistedSessionJson));
} catch (error) {
console.error(error);
}
}

dispatch({ type: 'initWithoutUser' });
}, [fetchProfile]);
dispatch({ type: 'signOut' });
}, [signIn]);

useEffect(() => {
init();
Expand All @@ -185,9 +145,8 @@ function AuthProviderContent({ children }: AuthProviderProps) {
value={{
currentUser: state.currentUser,
isSignedIn: !!state.session,
requestSignIn,
signIn,
signOut,
token: state.session?.token,
}}
>
{children}
Expand Down
31 changes: 3 additions & 28 deletions mobile/src/screens/LandingScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,11 @@
import { Loader } from '@mobile/components/loader/Loader';
import { useAuth } from '@mobile/contexts/AuthProvider';
import { Login } from '@mobile/components/auth/Login';
import { buildScreen } from '@mobile/utils/buildScreen';
import { useState } from 'react';
import { Pressable, Text, View } from 'react-native';
import { View } from 'react-native';

export const LandingScreen = buildScreen(() => {
const { requestSignIn } = useAuth();
const [isSigningIn, setIsSigningIn] = useState(false);

return (
<View className="flex-1 items-center justify-center">
<Pressable
onPress={async () => {
setIsSigningIn(true);

try {
await requestSignIn();
} catch (error) {
console.error(error);
} finally {
setIsSigningIn(false);
}
}}
>
<View className="bg-fc-purple min-h-[60px] min-w-[120px] flex-row items-center justify-center rounded-lg p-4">
{isSigningIn ? (
<Loader />
) : (
<Text className="text-lg font-bold text-white">Login</Text>
)}
</View>
</Pressable>
<Login />
</View>
);
});

0 comments on commit bd607c8

Please sign in to comment.