From e1d168344f3ff97315bf468a0719702bad2a8d89 Mon Sep 17 00:00:00 2001 From: Prakhar Agarwal Date: Mon, 29 Jul 2024 12:02:45 +0530 Subject: [PATCH] feat: add switch account functionality (#3308) * feat: create profile screen, add switch button Signed-off-by: Prakhar Agarwal * feat: add profile item Signed-off-by: Prakhar Agarwal * feat: loads data from persistant state, displays it, switch b/w accounts, log out, and add new user Signed-off-by: Prakhar Agarwal * feat: fetch usernames from backend Signed-off-by: Prakhar Agarwal * feat: display profiles(fix #fb24fb) and enable switching Signed-off-by: Prakhar Agarwal * fix: show switch option only when user is authenticated Signed-off-by: Prakhar Agarwal * fix: call logout mutation Signed-off-by: Prakhar Agarwal * fix: navigate to home screen on switching profile Signed-off-by: Prakhar Agarwal * fix: switch icon color Signed-off-by: Prakhar Agarwal * fix: disable back when loading data Signed-off-by: Prakhar Agarwal * fix: go back 1 screen on profile switch Signed-off-by: Prakhar Agarwal --------- Signed-off-by: Prakhar Agarwal --- app/assets/icons/switch.svg | 4 +- app/graphql/client.tsx | 4 +- app/graphql/generated.gql | 7 + app/graphql/generated.ts | 44 ++++ app/i18n/en/index.ts | 1 + app/i18n/i18n-types.ts | 8 + app/i18n/raw-i18n/source/en.json | 3 +- .../settings-screen/account/banner.tsx | 14 +- .../settings-screen/account/profile.tsx | 223 ++++++++++++++++-- 9 files changed, 274 insertions(+), 34 deletions(-) diff --git a/app/assets/icons/switch.svg b/app/assets/icons/switch.svg index 00fb3654d0..fb03478a4b 100644 --- a/app/assets/icons/switch.svg +++ b/app/assets/icons/switch.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/graphql/client.tsx b/app/graphql/client.tsx index e5c740b987..c152c9d981 100644 --- a/app/graphql/client.tsx +++ b/app/graphql/client.tsx @@ -199,15 +199,15 @@ const GaloyClient: React.FC = ({ children }) => { if (token) { authLink = setContext((request, { headers }) => ({ headers: { - ...headers, authorization: getAuthorizationHeader(token), + ...headers, }, })) } else { authLink = setContext((request, { headers }) => ({ headers: { - ...headers, authorization: "", + ...headers, }, })) } diff --git a/app/graphql/generated.gql b/app/graphql/generated.gql index d4fe7b3887..2a7648aca0 100644 --- a/app/graphql/generated.gql +++ b/app/graphql/generated.gql @@ -1644,6 +1644,13 @@ query transactionListForDefaultAccount($first: Int, $after: String, $last: Int, } } +query username { + me { + username + __typename + } +} + query walletOverviewScreen { me { id diff --git a/app/graphql/generated.ts b/app/graphql/generated.ts index 98df38bb12..25c05cbfe6 100644 --- a/app/graphql/generated.ts +++ b/app/graphql/generated.ts @@ -2990,6 +2990,11 @@ export type OnChainUsdPaymentSendAsBtcDenominatedMutationVariables = Exact<{ export type OnChainUsdPaymentSendAsBtcDenominatedMutation = { readonly __typename: 'Mutation', readonly onChainUsdPaymentSendAsBtcDenominated: { readonly __typename: 'PaymentSendPayload', readonly status?: PaymentSendResult | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }> } }; +export type UsernameQueryVariables = Exact<{ [key: string]: never; }>; + + +export type UsernameQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly username?: string | null } | null }; + export type AccountDeleteMutationVariables = Exact<{ [key: string]: never; }>; @@ -6748,6 +6753,45 @@ export function useOnChainUsdPaymentSendAsBtcDenominatedMutation(baseOptions?: A export type OnChainUsdPaymentSendAsBtcDenominatedMutationHookResult = ReturnType; export type OnChainUsdPaymentSendAsBtcDenominatedMutationResult = Apollo.MutationResult; export type OnChainUsdPaymentSendAsBtcDenominatedMutationOptions = Apollo.BaseMutationOptions; +export const UsernameDocument = gql` + query username { + me { + username + } +} + `; + +/** + * __useUsernameQuery__ + * + * To run a query within a React component, call `useUsernameQuery` and pass it any options that fit your needs. + * When your component renders, `useUsernameQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useUsernameQuery({ + * variables: { + * }, + * }); + */ +export function useUsernameQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(UsernameDocument, options); + } +export function useUsernameLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(UsernameDocument, options); + } +export function useUsernameSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(UsernameDocument, options); + } +export type UsernameQueryHookResult = ReturnType; +export type UsernameLazyQueryHookResult = ReturnType; +export type UsernameSuspenseQueryHookResult = ReturnType; +export type UsernameQueryResult = Apollo.QueryResult; export const AccountDeleteDocument = gql` mutation accountDelete { accountDelete { diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index bc2f1fcec7..66dc67630d 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -2427,6 +2427,7 @@ const en: BaseTranslation = { ProfileScreen: { addNew : "Add new", logout: "Logout", + error: "Unable to fetch profiles at this time", }, TotpRegistrationInitiateScreen: { title: "Two-factor authentication", diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index e50e55a8c1..4715d61dc3 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -7590,6 +7590,10 @@ type RootTranslation = { * L​o​g​o​u​t */ logout: string + /** + * U​n​a​b​l​e​ ​t​o​ ​f​e​t​c​h​ ​p​r​o​f​i​l​e​s​ ​a​t​ ​t​h​i​s​ ​t​i​m​e + */ + error: string } TotpRegistrationInitiateScreen: { /** @@ -16613,6 +16617,10 @@ export type TranslationFunctions = { * Logout */ logout: () => LocalizedString + /** + * Unable to fetch profiles at this time + */ + error: () => LocalizedString } TotpRegistrationInitiateScreen: { /** diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index 5eb591c153..c9bdb51be7 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -2345,7 +2345,8 @@ }, "ProfileScreen": { "addNew": "Add new", - "logout": "Logout" + "logout": "Logout", + "error": "Unable to fetch profiles at this time" }, "TotpRegistrationInitiateScreen": { "title": "Two-factor authentication", diff --git a/app/screens/settings-screen/account/banner.tsx b/app/screens/settings-screen/account/banner.tsx index 138dd28ed4..8f4efb045b 100644 --- a/app/screens/settings-screen/account/banner.tsx +++ b/app/screens/settings-screen/account/banner.tsx @@ -53,12 +53,14 @@ export const AccountBanner = () => { {isUserLoggedIn ? usernameTitle : LL.SettingsScreen.logInOrCreateAccount()} - - - - {LL.AccountScreen.switch()} - - + {isUserLoggedIn && ( + + + + {LL.AccountScreen.switch()} + + + )} ) diff --git a/app/screens/settings-screen/account/profile.tsx b/app/screens/settings-screen/account/profile.tsx index 8e1d2d05d0..6c2cbe9ddd 100644 --- a/app/screens/settings-screen/account/profile.tsx +++ b/app/screens/settings-screen/account/profile.tsx @@ -2,36 +2,141 @@ import { ScrollView } from "react-native-gesture-handler" import { Screen } from "@app/components/screen" import { GaloyPrimaryButton } from "@app/components/atomic/galoy-primary-button" import { useI18nContext } from "@app/i18n/i18n-react" -import { TouchableOpacity, View } from "react-native" +import { ActivityIndicator, Button, TouchableOpacity, View } from "react-native" import { GaloyIcon } from "@app/components/atomic/galoy-icon" -import { makeStyles, Text } from "@rneui/themed" +import { makeStyles, Text, useTheme } from "@rneui/themed" +import { usePersistentStateContext } from "@app/store/persistent-state" +import { useNavigation } from "@react-navigation/native" +import { StackNavigationProp } from "@react-navigation/stack" +import { RootStackParamList } from "@app/navigation/stack-param-lists" +import { useApolloClient, gql } from "@apollo/client" +import { useUserLogoutMutation, useUsernameLazyQuery } from "@app/graphql/generated" +import { useCallback, useEffect, useRef, useState } from "react" +import messaging from "@react-native-firebase/messaging" +import crashlytics from "@react-native-firebase/crashlytics" +import { logLogout } from "@app/utils/analytics" +import { PersistentState } from "@app/store/persistent-state/state-migrations" + +gql` + query username { + me { + username + } + } +` + +type ProfileProps = { + username: string + token: string + selected?: boolean +} export const ProfileScreen: React.FC = () => { const styles = useStyles() + const { + theme: { colors }, + } = useTheme() const { LL } = useI18nContext() + const { persistentState } = usePersistentStateContext() + const navigation = useNavigation>() + + const { galoyAuthToken: curToken, galoyAllAuthTokens: allTokens } = persistentState + + const [profiles, setProfiles] = useState([]) + const [fetchUsername, { error, refetch }] = useUsernameLazyQuery({ + fetchPolicy: "no-cache", + }) + const [loading, setLoading] = useState(true) + const prevTokenRef = useRef(persistentState.galoyAuthToken) // Previous token state + + useEffect(() => { + const fetchUsernames = async () => { + setLoading(true) + const profiles: ProfileProps[] = [] + let counter = 1 + for (const token of allTokens) { + try { + const { data } = await fetchUsername({ + context: { + headers: { + authorization: `Bearer ${token}`, + }, + }, + }) + if (data && data.me) { + profiles.push({ + username: data.me.username ? data.me.username : `Account ${counter++}`, + token, + selected: token === curToken, + }) + } + } catch (err) { + console.error(`Failed to fetch username for token ${token}`, err) + } + } + setProfiles(profiles) + setLoading(false) + } + fetchUsernames() + }, [allTokens, fetchUsername, curToken]) + + useEffect(() => { + const unsubscribe = navigation.addListener("beforeRemove", (e) => { + if (loading) { + e.preventDefault() + } + }) + + return unsubscribe + }, [navigation, loading]) + + useEffect(() => { + if (prevTokenRef.current !== persistentState.galoyAuthToken) { + // Navigate back when token is updated and different from the previous token + navigation.goBack() + } + prevTokenRef.current = persistentState.galoyAuthToken // Update previous token + }, [persistentState.galoyAuthToken, navigation]) + + if (error) { + return ( + + + + {LL.ProfileScreen.error()} + +