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 fc653fcc5f..d6a4d9f5c4 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 2a9520454e..e4f6508db4 100644
--- a/app/i18n/i18n-types.ts
+++ b/app/i18n/i18n-types.ts
@@ -7591,6 +7591,10 @@ type RootTranslation = {
* Logout
*/
logout: string
+ /**
+ * Unable to fetch profiles at this time
+ */
+ error: string
}
TotpRegistrationInitiateScreen: {
/**
@@ -16614,6 +16618,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 a1a652b31a..c8f31bc628 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()}
+
+
+
+ )
+ }
+
+ if (loading) {
+ return (
+
+
+
+
+
+ )
+ }
- const data = [
- {
- username: "User 1",
- selected: true,
- },
- {
- username: "User 2",
- selected: false,
- },
- {
- username: "User 3",
- selected: false,
- },
- ]
+ const handleAddNew = () => {
+ navigation.navigate("getStarted")
+ }
return (
- {data.map((profile, index) => {
+ {profiles.map((profile, index) => {
return
})}
- {}} containerStyle={styles.addNewButton}>
+
{LL.ProfileScreen.addNew()}
@@ -40,15 +145,67 @@ export const ProfileScreen: React.FC = () => {
)
}
-const Profile: React.FC<{ username: string; selected?: boolean }> = ({
- username,
- selected,
-}) => {
+const Profile: React.FC = ({ username, token, selected }) => {
const styles = useStyles()
const { LL } = useI18nContext()
+ const navigation = useNavigation>()
+ const { persistentState, updateState } = usePersistentStateContext()
+ const client = useApolloClient()
+ const [userLogoutMutation] = useUserLogoutMutation({
+ fetchPolicy: "no-cache",
+ })
+ const oldToken = persistentState.galoyAuthToken
+
+ const logout = useCallback(async (): Promise => {
+ try {
+ const deviceToken = await messaging().getToken()
+ logLogout()
+ await Promise.race([
+ userLogoutMutation({ variables: { input: { deviceToken } } }),
+ // Create a promise that rejects after 2 seconds
+ // this is handy for the case where the server is down, or in dev mode
+ new Promise((_, reject) => {
+ setTimeout(() => {
+ reject(new Error("Logout mutation timeout"))
+ }, 2000)
+ }),
+ ])
+ } catch (err: unknown) {
+ if (err instanceof Error) {
+ crashlytics().recordError(err)
+ console.debug({ err }, `error logout`)
+ }
+ }
+ }, [userLogoutMutation])
+
+ const handleLogout = async () => {
+ updateState((state) => {
+ if (state) {
+ return {
+ ...state,
+ galoyAllAuthTokens: state.galoyAllAuthTokens.filter((t) => t !== token),
+ }
+ }
+ return state
+ })
+ await logout()
+ }
+
+ const handleProfileSwitch = () => {
+ updateState((state) => {
+ if (state) {
+ return {
+ ...state,
+ galoyAuthToken: token,
+ }
+ }
+ return state
+ })
+ client.clearStore() // clear cache to load fresh data using new token
+ }
return (
-
+
{selected && (
@@ -57,7 +214,7 @@ const Profile: React.FC<{ username: string; selected?: boolean }> = ({
{username}
{!selected && (
- {}}>
+
{LL.ProfileScreen.logout()}
)}
@@ -114,4 +271,22 @@ const useStyles = makeStyles(({ colors }) => ({
fontSize: 20,
fontWeight: "bold",
},
+ errorWrapper: {
+ justifyContent: "center",
+ alignItems: "center",
+ marginTop: "50%",
+ marginBottom: "50%",
+ },
+ errorText: {
+ color: colors.error,
+ fontWeight: "bold",
+ fontSize: 18,
+ marginBottom: 20,
+ },
+ loadingWrapper: {
+ justifyContent: "center",
+ alignItems: "center",
+ marginTop: "50%",
+ marginBottom: "50%",
+ },
}))