Skip to content

Commit

Permalink
mobile progress
Browse files Browse the repository at this point in the history
  • Loading branch information
nickcherry committed Jan 31, 2024
1 parent 9849a46 commit fdb998a
Show file tree
Hide file tree
Showing 15 changed files with 238 additions and 164 deletions.
10 changes: 9 additions & 1 deletion mobile/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@
},
"web": {
"favicon": "./assets/favicon.png"
}
},
"plugins": [
[
"expo-secure-store",
{
"faceIDPermission": "Allow $(PRODUCT_NAME) to access your Face ID biometric data."
}
]
]
}
}
Binary file modified mobile/bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@tanstack/react-query": "^5.17.19",
"date-fns": "^3.3.1",
"expo": "50.0.3",
"expo-secure-store": "^12.8.1",
"expo-status-bar": "1.11.1",
"nativewind": "^2.0.11",
"react": "18.2.0",
Expand Down
104 changes: 96 additions & 8 deletions mobile/src/contexts/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { AuthKitProvider, useSignIn } from '@farcaster/auth-kit';
import { FullscreenLoader } from '@mobile/components/loader/FullscreenLoader';
import { useFetchProfile } from '@mobile/hooks/data/profile';
import { User } from '@shared/types/models';
import * as SecureStore from 'expo-secure-store';
import {
createContext,
ReactNode,
useCallback,
useContext,
useState,
useEffect,
useReducer,
} from 'react';
import { Linking } from 'react-native';

const AuthContext = createContext<{
currentUser: User | undefined;
isSignedIn: boolean;
signIn: () => Promise<void>;
token: string | undefined;
}>({
currentUser: undefined,
isSignedIn: false,
signIn: async () => {
throw new Error(
Expand All @@ -22,12 +29,50 @@ const AuthContext = createContext<{
token: undefined,
});

type Session = {
fid: string;
token: string;
};

type State = {
currentUser: User | undefined;
isInitialized: boolean;
session: Session | undefined;
};

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

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

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

type AuthProviderProps = {
children: ReactNode;
};

function AuthProviderContent({ children }: AuthProviderProps) {
const [token, setToken] = useState<string>();
const [state, dispatch] = useReducer(reducer, initialState);
const fetchProfile = useFetchProfile();

const {
connect,
Expand All @@ -38,16 +83,59 @@ function AuthProviderContent({ children }: AuthProviderProps) {
// setToken(res);
},
});

const signIn = useCallback(async () => {
// if (url) {
await connect();
await authKitSignIn(); // Starts polling
Linking.openURL(url);
// }
if (!url) {
throw new Error('Expected authkit to provide url');
}

if (url) {
await connect();
await authKitSignIn(); // Starts polling
Linking.openURL(url);
}
}, [authKitSignIn, connect, url]);

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

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

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

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

useEffect(() => {
init();
}, [init]);

if (!state.isInitialized) {
return <FullscreenLoader />;
}

return (
<AuthContext.Provider value={{ isSignedIn: !!token, signIn, token }}>
<AuthContext.Provider
value={{
currentUser: state.currentUser,
isSignedIn: !!state.session,
signIn,
token: state.session?.token,
}}
>
{children}
</AuthContext.Provider>
);
Expand Down
31 changes: 31 additions & 0 deletions mobile/src/hooks/data/feed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { baseApiUrl } from '@mobile/constants/api';
import { FeedApiResponse } from '@shared/types/api';
import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query';

function buildFeedKey({ fid }: { fid: string }) {
return ['feed', fid];
}

function buildFeedFetcher({ fid }: { fid: string }) {
return async (): Promise<FeedApiResponse> => {
const res = await fetch(`${baseApiUrl}/feed/${fid}`);
return res.json();
};
}

export function useFeed({ fid }: { fid: string }) {
return useSuspenseQuery({
queryKey: buildFeedKey({ fid }),
queryFn: buildFeedFetcher({ fid }),
}).data;
}

export function useFetchFeed() {
const queryClient = useQueryClient();

return async ({ fid }: { fid: string }) => {
const data = buildFeedFetcher({ fid })();
queryClient.setQueryData(buildFeedKey({ fid }), data);
return data;
};
}
31 changes: 31 additions & 0 deletions mobile/src/hooks/data/profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { baseApiUrl } from '@mobile/constants/api';
import { ProfileApiResponse } from '@shared/types/api';
import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query';

function buildProfileKey({ fid }: { fid: string }) {
return ['profile', fid];
}

function buildProfileFetcher({ fid }: { fid: string }) {
return async (): Promise<ProfileApiResponse> => {
const res = await fetch(`${baseApiUrl}/profiles/${fid}`);
return res.json();
};
}

export function useProfile({ fid }: { fid: string }) {
return useSuspenseQuery({
queryKey: buildProfileKey({ fid }),
queryFn: buildProfileFetcher({ fid }),
}).data;
}

export function useFetchProfile() {
const queryClient = useQueryClient();

return async ({ fid }: { fid: string }) => {
const data = buildProfileFetcher({ fid })();
queryClient.setQueryData(buildProfileKey({ fid }), data);
return data;
};
}
31 changes: 31 additions & 0 deletions mobile/src/hooks/data/profileCasts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { baseApiUrl } from '@mobile/constants/api';
import { ProfileCastsApiResponse } from '@shared/types/api';
import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query';

function buildProfileCastsKey({ fid }: { fid: string }) {
return ['profileCasts', fid];
}

function buildProfileCastsFetcher({ fid }: { fid: string }) {
return async (): Promise<ProfileCastsApiResponse> => {
const res = await fetch(`${baseApiUrl}/profiles/${fid}/casts`);
return res.json();
};
}

export function useProfileCasts({ fid }: { fid: string }) {
return useSuspenseQuery({
queryKey: buildProfileCastsKey({ fid }),
queryFn: buildProfileCastsFetcher({ fid }),
}).data;
}

export function useFetchProfileCasts() {
const queryClient = useQueryClient();

return async ({ fid }: { fid: string }) => {
const data = buildProfileCastsFetcher({ fid })();
queryClient.setQueryData(buildProfileCastsKey({ fid }), data);
return data;
};
}
24 changes: 5 additions & 19 deletions mobile/src/screens/FeedScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,27 @@
import { Cast } from '@mobile/components/feed/Cast';
import { baseApiUrl } from '@mobile/constants/api';
import { useFeed, useFetchFeed } from '@mobile/hooks/data/feed';
import { buildScreen } from '@mobile/utils/buildScreen';
import { FeedApiResponse } from '@shared/types/api';
import { Cast as CastType } from '@shared/types/models';
import { FlashList } from '@shopify/flash-list';
import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
import { useCallback, useState } from 'react';

const feedKey = ['feed'];

async function fetchFeed(): Promise<FeedApiResponse> {
const res = await fetch(`${baseApiUrl}/feed?fid=145`);
return res.json();
}

function renderItem({ item }: { item: CastType }) {
return <Cast cast={item} />;
}

export const FeedScreen = buildScreen(() => {
const queryClient = useQueryClient();

const { feed } = useFeed({ fid });
const fetchFeed = useFetchFeed();
const [isRefreshing, setIsRefreshing] = useState(false);

const { feed } = useSuspenseQuery({
queryKey: feedKey,
queryFn: fetchFeed,
}).data;

const onRefresh = useCallback(async () => {
try {
setIsRefreshing(true);
queryClient.setQueryData(feedKey, await fetchFeed());
await fetchFeed({ fid });
} finally {
setIsRefreshing(false);
}
}, [queryClient]);
}, [fetchFeed]);

return (
<FlashList
Expand Down
48 changes: 21 additions & 27 deletions mobile/src/screens/ProfileScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Avatar } from '@mobile/components/avatar/Avatar';
import { Cast } from '@mobile/components/feed/Cast';
import { baseApiUrl } from '@mobile/constants/api';
import { useProfile } from '@mobile/hooks/data/profile';
import {
useFetchProfileCasts,
useProfileCasts,
} from '@mobile/hooks/data/profileCasts';
import { RootParamList } from '@mobile/types/navigation';
import { buildScreen } from '@mobile/utils/buildScreen';
import { useRoute } from '@react-navigation/native';
import { ProfileApiResponse, ProfileCastsApiResponse } from '@shared/types/api';
import { Cast as CastType } from '@shared/types/models';
import { FlashList } from '@shopify/flash-list';
import { useSuspenseQueries } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useCallback, useState } from 'react';
import { Text, View } from 'react-native';

function renderItem({ item }: { item: CastType }) {
Expand All @@ -18,30 +20,20 @@ function renderItem({ item }: { item: CastType }) {
export const ProfileScreen = buildScreen(() => {
const { fid } = useRoute().params as RootParamList['Profile'];

const profileKey = useMemo(() => ['profile', fid], [fid]);
const profileCastsKey = useMemo(() => ['profileCasts', fid], [fid]);
const { profile } = useProfile({ fid });
const { casts } = useProfileCasts({ fid });
const fetchProfileCasts = useFetchProfileCasts();

const [
{
data: { profile },
},
{
data: { casts },
},
] = useSuspenseQueries({
queries: [
{
queryKey: profileKey,
queryFn: (): Promise<ProfileApiResponse> =>
fetch(`${baseApiUrl}/users/${fid}`).then((res) => res.json()),
},
{
queryKey: profileCastsKey,
queryFn: (): Promise<ProfileCastsApiResponse> =>
fetch(`${baseApiUrl}/users/${fid}/casts`).then((res) => res.json()),
},
],
});
const [isRefreshing, setIsRefreshing] = useState(false);

const onRefresh = useCallback(async () => {
try {
setIsRefreshing(true);
await fetchProfileCasts({ fid });
} finally {
setIsRefreshing(false);
}
}, [fetchProfileCasts, fid]);

return (
<View className="flex-1 flex-col justify-between">
Expand All @@ -68,6 +60,8 @@ export const ProfileScreen = buildScreen(() => {
data={casts}
renderItem={renderItem}
estimatedItemSize={190}
refreshing={isRefreshing}
onRefresh={onRefresh}
/>
</View>
</View>
Expand Down
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit fdb998a

Please sign in to comment.