diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index c1eb042892..5d74847052 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -12,7 +12,6 @@ jobs: e2e-android: name: Android runs-on: self-hosted - timeout-minutes: 45 steps: - uses: actions/checkout@v2 @@ -108,7 +107,6 @@ jobs: e2e-ios: name: iOS runs-on: self-hosted - timeout-minutes: 45 steps: - uses: actions/checkout@v2 diff --git a/.storybook/storybook.requires.js b/.storybook/storybook.requires.js index fcec5fbc18..4d69f5b734 100644 --- a/.storybook/storybook.requires.js +++ b/.storybook/storybook.requires.js @@ -99,6 +99,7 @@ const getStories = () => { "./app/screens/settings-screen/display-currency-screen.stories.tsx": require("../app/screens/settings-screen/display-currency-screen.stories.tsx"), "./app/screens/settings-screen/language-screen.stories.tsx": require("../app/screens/settings-screen/language-screen.stories.tsx"), "./app/screens/settings-screen/settings-screen.stories.tsx": require("../app/screens/settings-screen/settings-screen.stories.tsx"), + "./app/screens/settings-screen/settings.stories.tsx": require("../app/screens/settings-screen/settings.stories.tsx"), "./app/screens/settings-screen/theme-screen.stories.tsx": require("../app/screens/settings-screen/theme-screen.stories.tsx"), "./app/screens/transaction-detail-screen/transaction-detail-screen.stories.tsx": require("../app/screens/transaction-detail-screen/transaction-detail-screen.stories.tsx"), } diff --git a/__tests__/hooks/use-show-warning-secure-account.spec.tsx b/__tests__/hooks/use-show-warning-secure-account.spec.tsx index 405975dc79..721a385d60 100644 --- a/__tests__/hooks/use-show-warning-secure-account.spec.tsx +++ b/__tests__/hooks/use-show-warning-secure-account.spec.tsx @@ -9,7 +9,7 @@ import { WarningSecureAccountDocument, } from "@app/graphql/generated" import { IsAuthedContextProvider } from "@app/graphql/is-authed-context" -import { useShowWarningSecureAccount } from "@app/screens/settings-screen/show-warning-secure-account" +import { useShowWarningSecureAccount } from "@app/screens/settings-screen/account/show-warning-secure-account-hook" import { renderHook } from "@testing-library/react-hooks" // FIXME: the mockPrice doesn't work as expect. diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index 5df48337c4..08adf22557 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -109,7 +109,7 @@ platform :android do file_path: ENV["GRADLE_APK_OUTPUT_PATH"] ) - max_retries = 3 + max_retries = 10 retries = 0 begin diff --git a/android/fastlane/README.md b/android/fastlane/README.md index 876fdb3e82..a7649666d0 100644 --- a/android/fastlane/README.md +++ b/android/fastlane/README.md @@ -15,53 +15,53 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do ## Android -### android test +### android build ```sh -[bundle exec] fastlane android test +[bundle exec] fastlane android build ``` -Runs all the tests +Build Releasable APK -### android build +### android play_store_upload ```sh -[bundle exec] fastlane android build +[bundle exec] fastlane android play_store_upload ``` -Build a new version of the app +Deploy a new version to the Google Play -### android beta +### android huawei_store_upload ```sh -[bundle exec] fastlane android beta +[bundle exec] fastlane android huawei_store_upload ``` -Deploy a new version to both Play Store and Huawei Store +Deploy the new version to Huawei App Gallery -### android play_store_release +### android promote_to_beta ```sh -[bundle exec] fastlane android play_store_release +[bundle exec] fastlane android promote_to_beta ``` -Deploy a new version to the Google Play +Promote Internal Testing build to Beta -### android huawei_release +### android promote_to_public ```sh -[bundle exec] fastlane android huawei_release +[bundle exec] fastlane android promote_to_public ``` -Deploy a new version to Huawei App Gallery +Promote Internal Testing build to Public -### android browserstack +### android public_phased_percent ```sh -[bundle exec] fastlane android browserstack +[bundle exec] fastlane android public_phased_percent ``` -End to end testing on browserstack +Phased Public Rollout ### android build_e2e @@ -71,6 +71,14 @@ End to end testing on browserstack Build for end to end testing +### android browserstack + +```sh +[bundle exec] fastlane android browserstack +``` + +End to end testing on browserstack + ---- This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. diff --git a/app/assets/icons/refresh.svg b/app/assets/icons/refresh.svg new file mode 100644 index 0000000000..b485ae0921 --- /dev/null +++ b/app/assets/icons/refresh.svg @@ -0,0 +1 @@ + diff --git a/app/components/atomic/galoy-icon-button/galoy-icon-button.tsx b/app/components/atomic/galoy-icon-button/galoy-icon-button.tsx index 5e89fec5fb..7a22cb7e50 100644 --- a/app/components/atomic/galoy-icon-button/galoy-icon-button.tsx +++ b/app/components/atomic/galoy-icon-button/galoy-icon-button.tsx @@ -15,6 +15,8 @@ export type GaloyIconButtonProps = { size: "small" | "medium" | "large" text?: string iconOnly?: boolean + color?: string + backgroundColor?: string } const sizeMapping = { @@ -29,6 +31,8 @@ export const GaloyIconButton = ({ text, iconOnly, disabled, + color, + backgroundColor, ...remainingProps }: GaloyIconButtonProps & PressableProps) => { const { @@ -55,36 +59,36 @@ export const GaloyIconButton = ({ case iconOnly && disabled: return { opacity: 0.7, - color: colors.primary, + color: color || colors.primary, backgroundColor: colors.transparent, } case iconOnly && pressed: return { opacity: 0.7, - color: colors.primary, - backgroundColor: colors.grey4, + color: color || colors.primary, + backgroundColor: backgroundColor || colors.grey4, } case iconOnly && !pressed: return { - color: colors.primary, + color: color || colors.primary, backgroundColor: colors.transparent, } case !iconOnly && disabled: return { opacity: 0.7, - color: colors.primary, - backgroundColor: colors.grey4, + color: color || colors.primary, + backgroundColor: backgroundColor || colors.grey4, } case !iconOnly && pressed: return { opacity: 0.7, - color: colors.primary, - backgroundColor: colors.grey4, + color: color || colors.primary, + backgroundColor: backgroundColor || colors.grey4, } case !iconOnly && !pressed: return { - color: colors.primary, - backgroundColor: colors.grey4, + color: color || colors.primary, + backgroundColor: backgroundColor || colors.grey4, } default: return {} diff --git a/app/components/atomic/galoy-icon/galoy-icon.tsx b/app/components/atomic/galoy-icon/galoy-icon.tsx index da4468bf3d..679baec772 100644 --- a/app/components/atomic/galoy-icon/galoy-icon.tsx +++ b/app/components/atomic/galoy-icon/galoy-icon.tsx @@ -51,6 +51,7 @@ import Warning from "@app/assets/icons-redesign/warning.svg" import Note from "@app/assets/icons/note.svg" import People from "@app/assets/icons/people.svg" import Rank from "@app/assets/icons/rank.svg" +import Refresh from "@app/assets/icons/refresh.svg" import { makeStyles, useTheme } from "@rneui/themed" export const icons = { @@ -104,6 +105,7 @@ export const icons = { "payment-pending": PaymentPending, "payment-error": PaymentError, "bell": Bell, + "refresh": Refresh, } as const export type IconNamesType = keyof typeof icons diff --git a/app/components/contact-modal/contact-modal.tsx b/app/components/contact-modal/contact-modal.tsx index e2ce0a5977..0f5c5c3dcc 100644 --- a/app/components/contact-modal/contact-modal.tsx +++ b/app/components/contact-modal/contact-modal.tsx @@ -123,8 +123,8 @@ const ContactModal: React.FC = ({ return ( @@ -163,7 +163,7 @@ const useStyles = makeStyles(({ colors }) => ({ marginHorizontal: 0, }, listItemContainer: { - backgroundColor: colors.white, + backgroundColor: colors.grey5, }, listItemTitle: { color: colors.black, diff --git a/app/components/custom-modal/custom-modal.tsx b/app/components/custom-modal/custom-modal.tsx index db54263058..fd3e9627ec 100644 --- a/app/components/custom-modal/custom-modal.tsx +++ b/app/components/custom-modal/custom-modal.tsx @@ -71,10 +71,11 @@ const CustomModal: React.FC = ({ return ( {showCloseIconButton && ( @@ -135,7 +136,7 @@ type UseStylesProps = { const useStyles = makeStyles(({ colors }, props: UseStylesProps) => ({ container: { - backgroundColor: colors.white, + backgroundColor: colors.grey5, maxHeight: "95%", minHeight: props.minHeight || "auto", borderRadius: 16, diff --git a/app/components/set-default-account-modal/set-default-account-modal.tsx b/app/components/set-default-account-modal/set-default-account-modal.tsx index c13d089c26..e1bbb09ebf 100644 --- a/app/components/set-default-account-modal/set-default-account-modal.tsx +++ b/app/components/set-default-account-modal/set-default-account-modal.tsx @@ -19,7 +19,7 @@ import { StackNavigationProp } from "@react-navigation/stack" import { makeStyles, Text, useTheme } from "@rneui/themed" import { GaloyCurrencyBubble } from "../atomic/galoy-currency-bubble" -import { GaloyIcon } from "../atomic/galoy-icon" +import { GaloyIconButton } from "../atomic/galoy-icon-button" gql` query setDefaultAccountModal { @@ -148,15 +148,21 @@ export const SetDefaultAccountModalUI: React.FC = return ( - - - + ({ flexDirection: "column", }, container: { - backgroundColor: colors.white, + backgroundColor: colors.grey5, maxHeight: "80%", minHeight: "auto", borderRadius: 16, diff --git a/app/components/set-lightning-address-modal/set-lightning-address-modal.tsx b/app/components/set-lightning-address-modal/set-lightning-address-modal.tsx index 82c40fa94b..634ebc2f99 100644 --- a/app/components/set-lightning-address-modal/set-lightning-address-modal.tsx +++ b/app/components/set-lightning-address-modal/set-lightning-address-modal.tsx @@ -175,11 +175,11 @@ export const SetLightningAddressModalUI = ({ return ( ({ paddingHorizontal: 12, borderRadius: 8, minHeight: 60, - backgroundColor: colors.grey5, + backgroundColor: colors.grey4, alignItems: "center", justifyContent: "space-between", }, diff --git a/app/components/version/version.tsx b/app/components/version/version.tsx index 577d4338fb..e6ef286977 100644 --- a/app/components/version/version.tsx +++ b/app/components/version/version.tsx @@ -13,7 +13,6 @@ import { testProps } from "../../utils/testProps" const useStyles = makeStyles(({ colors }) => ({ version: { color: colors.grey0, - fontSize: 18, marginTop: 18, textAlign: "center", }, diff --git a/app/graphql/generated.gql b/app/graphql/generated.gql index 3e56172354..c3fdede831 100644 --- a/app/graphql/generated.gql +++ b/app/graphql/generated.gql @@ -798,6 +798,45 @@ query ContactsCard { } } +query ExportCsvSetting($walletIds: [WalletId!]!) { + me { + id + defaultAccount { + id + csvTransactions(walletIds: $walletIds) + __typename + } + __typename + } +} + +query SettingsScreen { + me { + id + username + language + defaultAccount { + id + defaultWalletId + wallets { + id + balance + walletCurrency + __typename + } + __typename + } + totpEnabled + phone + email { + address + verified + __typename + } + __typename + } +} + query accountDefaultWallet($walletCurrency: WalletCurrency, $username: Username!) { accountDefaultWallet(walletCurrency: $walletCurrency, username: $username) { id @@ -837,31 +876,6 @@ query accountLimits { } } -query accountScreen { - me { - id - phone - totpEnabled - email { - address - verified - __typename - } - defaultAccount { - id - level - wallets { - id - balance - walletCurrency - __typename - } - __typename - } - __typename - } -} - query addressScreen { me { id @@ -1454,27 +1468,6 @@ query setDefaultWalletScreen { } } -query settingsScreen { - me { - id - phone - username - language - defaultAccount { - id - defaultWalletId - wallets { - id - balance - walletCurrency - __typename - } - __typename - } - __typename - } -} - query supportChat { me { id @@ -1541,18 +1534,6 @@ query transactionListForDefaultAccount($first: Int, $after: String, $last: Int, } } -query walletCSVTransactions($walletIds: [WalletId!]!) { - me { - id - defaultAccount { - id - csvTransactions(walletIds: $walletIds) - __typename - } - __typename - } -} - query walletOverviewScreen { me { id diff --git a/app/graphql/generated.ts b/app/graphql/generated.ts index 95961716b9..75d0863796 100644 --- a/app/graphql/generated.ts +++ b/app/graphql/generated.ts @@ -2808,11 +2808,6 @@ 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 AccountScreenQueryVariables = Exact<{ [key: string]: never; }>; - - -export type AccountScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly phone?: string | null, readonly totpEnabled: boolean, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly level: AccountLevel, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; - export type AccountDeleteMutationVariables = Exact<{ [key: string]: never; }>; @@ -2828,10 +2823,10 @@ export type UserPhoneDeleteMutationVariables = Exact<{ [key: string]: never; }>; export type UserPhoneDeleteMutation = { readonly __typename: 'Mutation', readonly userPhoneDelete: { readonly __typename: 'UserPhoneDeletePayload', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }>, readonly me?: { readonly __typename: 'User', readonly id: string, readonly phone?: string | null, readonly totpEnabled: boolean, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null } | null } }; -export type UserTotpDeleteMutationVariables = Exact<{ [key: string]: never; }>; +export type WarningSecureAccountQueryVariables = Exact<{ [key: string]: never; }>; -export type UserTotpDeleteMutation = { readonly __typename: 'Mutation', readonly userTotpDelete: { readonly __typename: 'UserTotpDeletePayload', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }>, readonly me?: { readonly __typename: 'User', readonly id: string, readonly phone?: string | null, readonly totpEnabled: boolean, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null } | null } }; +export type WarningSecureAccountQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly level: AccountLevel, readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; export type AccountUpdateDefaultWalletIdMutationVariables = Exact<{ input: AccountUpdateDefaultWalletIdInput; @@ -2897,22 +2892,22 @@ export type AccountDisableNotificationCategoryMutationVariables = Exact<{ export type AccountDisableNotificationCategoryMutation = { readonly __typename: 'Mutation', readonly accountDisableNotificationCategory: { readonly __typename: 'AccountUpdateNotificationSettingsPayload', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }>, readonly account?: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly notificationSettings: { readonly __typename: 'NotificationSettings', readonly push: { readonly __typename: 'NotificationChannelSettings', readonly enabled: boolean, readonly disabledCategories: ReadonlyArray } } } | null } }; -export type WalletCsvTransactionsQueryVariables = Exact<{ - walletIds: ReadonlyArray | Scalars['WalletId']['input']; -}>; +export type SettingsScreenQueryVariables = Exact<{ [key: string]: never; }>; -export type WalletCsvTransactionsQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly csvTransactions: string } } | null }; +export type SettingsScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly username?: string | null, readonly language: string, readonly totpEnabled: boolean, readonly phone?: string | null, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> }, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null } | null }; -export type SettingsScreenQueryVariables = Exact<{ [key: string]: never; }>; +export type ExportCsvSettingQueryVariables = Exact<{ + walletIds: ReadonlyArray | Scalars['WalletId']['input']; +}>; -export type SettingsScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly phone?: string | null, readonly username?: string | null, readonly language: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; +export type ExportCsvSettingQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly csvTransactions: string } } | null }; -export type WarningSecureAccountQueryVariables = Exact<{ [key: string]: never; }>; +export type UserTotpDeleteMutationVariables = Exact<{ [key: string]: never; }>; -export type WarningSecureAccountQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly level: AccountLevel, readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; +export type UserTotpDeleteMutation = { readonly __typename: 'Mutation', readonly userTotpDelete: { readonly __typename: 'UserTotpDeletePayload', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }>, readonly me?: { readonly __typename: 'User', readonly id: string, readonly phone?: string | null, readonly totpEnabled: boolean, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null } | null } }; export type AccountLimitsQueryVariables = Exact<{ [key: string]: never; }>; @@ -6216,55 +6211,6 @@ export function useOnChainUsdPaymentSendAsBtcDenominatedMutation(baseOptions?: A export type OnChainUsdPaymentSendAsBtcDenominatedMutationHookResult = ReturnType; export type OnChainUsdPaymentSendAsBtcDenominatedMutationResult = Apollo.MutationResult; export type OnChainUsdPaymentSendAsBtcDenominatedMutationOptions = Apollo.BaseMutationOptions; -export const AccountScreenDocument = gql` - query accountScreen { - me { - id - phone - totpEnabled - email { - address - verified - } - defaultAccount { - id - level - wallets { - id - balance - walletCurrency - } - } - } -} - `; - -/** - * __useAccountScreenQuery__ - * - * To run a query within a React component, call `useAccountScreenQuery` and pass it any options that fit your needs. - * When your component renders, `useAccountScreenQuery` 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 } = useAccountScreenQuery({ - * variables: { - * }, - * }); - */ -export function useAccountScreenQuery(baseOptions?: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(AccountScreenDocument, options); - } -export function useAccountScreenLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(AccountScreenDocument, options); - } -export type AccountScreenQueryHookResult = ReturnType; -export type AccountScreenLazyQueryHookResult = ReturnType; -export type AccountScreenQueryResult = Apollo.QueryResult; export const AccountDeleteDocument = gql` mutation accountDelete { accountDelete { @@ -6386,49 +6332,49 @@ export function useUserPhoneDeleteMutation(baseOptions?: Apollo.MutationHookOpti export type UserPhoneDeleteMutationHookResult = ReturnType; export type UserPhoneDeleteMutationResult = Apollo.MutationResult; export type UserPhoneDeleteMutationOptions = Apollo.BaseMutationOptions; -export const UserTotpDeleteDocument = gql` - mutation userTotpDelete { - userTotpDelete { - errors { - message - } - me { +export const WarningSecureAccountDocument = gql` + query warningSecureAccount { + me { + id + defaultAccount { + level id - phone - totpEnabled - email { - address - verified + wallets { + id + balance + walletCurrency } } } } `; -export type UserTotpDeleteMutationFn = Apollo.MutationFunction; /** - * __useUserTotpDeleteMutation__ + * __useWarningSecureAccountQuery__ * - * To run a mutation, you first call `useUserTotpDeleteMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useUserTotpDeleteMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution + * To run a query within a React component, call `useWarningSecureAccountQuery` and pass it any options that fit your needs. + * When your component renders, `useWarningSecureAccountQuery` 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 mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * @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 [userTotpDeleteMutation, { data, loading, error }] = useUserTotpDeleteMutation({ + * const { data, loading, error } = useWarningSecureAccountQuery({ * variables: { * }, * }); */ -export function useUserTotpDeleteMutation(baseOptions?: Apollo.MutationHookOptions) { +export function useWarningSecureAccountQuery(baseOptions?: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(UserTotpDeleteDocument, options); + return Apollo.useQuery(WarningSecureAccountDocument, options); } -export type UserTotpDeleteMutationHookResult = ReturnType; -export type UserTotpDeleteMutationResult = Apollo.MutationResult; -export type UserTotpDeleteMutationOptions = Apollo.BaseMutationOptions; +export function useWarningSecureAccountLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(WarningSecureAccountDocument, options); + } +export type WarningSecureAccountQueryHookResult = ReturnType; +export type WarningSecureAccountLazyQueryHookResult = ReturnType; +export type WarningSecureAccountQueryResult = Apollo.QueryResult; export const AccountUpdateDefaultWalletIdDocument = gql` mutation accountUpdateDefaultWalletId($input: AccountUpdateDefaultWalletIdInput!) { accountUpdateDefaultWalletId(input: $input) { @@ -6843,50 +6789,10 @@ export function useAccountDisableNotificationCategoryMutation(baseOptions?: Apol export type AccountDisableNotificationCategoryMutationHookResult = ReturnType; export type AccountDisableNotificationCategoryMutationResult = Apollo.MutationResult; export type AccountDisableNotificationCategoryMutationOptions = Apollo.BaseMutationOptions; -export const WalletCsvTransactionsDocument = gql` - query walletCSVTransactions($walletIds: [WalletId!]!) { - me { - id - defaultAccount { - id - csvTransactions(walletIds: $walletIds) - } - } -} - `; - -/** - * __useWalletCsvTransactionsQuery__ - * - * To run a query within a React component, call `useWalletCsvTransactionsQuery` and pass it any options that fit your needs. - * When your component renders, `useWalletCsvTransactionsQuery` 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 } = useWalletCsvTransactionsQuery({ - * variables: { - * walletIds: // value for 'walletIds' - * }, - * }); - */ -export function useWalletCsvTransactionsQuery(baseOptions: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(WalletCsvTransactionsDocument, options); - } -export function useWalletCsvTransactionsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(WalletCsvTransactionsDocument, options); - } -export type WalletCsvTransactionsQueryHookResult = ReturnType; -export type WalletCsvTransactionsLazyQueryHookResult = ReturnType; -export type WalletCsvTransactionsQueryResult = Apollo.QueryResult; export const SettingsScreenDocument = gql` - query settingsScreen { + query SettingsScreen { me { id - phone username language defaultAccount { @@ -6898,6 +6804,12 @@ export const SettingsScreenDocument = gql` walletCurrency } } + totpEnabled + phone + email { + address + verified + } } } `; @@ -6928,49 +6840,88 @@ export function useSettingsScreenLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt export type SettingsScreenQueryHookResult = ReturnType; export type SettingsScreenLazyQueryHookResult = ReturnType; export type SettingsScreenQueryResult = Apollo.QueryResult; -export const WarningSecureAccountDocument = gql` - query warningSecureAccount { +export const ExportCsvSettingDocument = gql` + query ExportCsvSetting($walletIds: [WalletId!]!) { me { id defaultAccount { - level id - wallets { - id - balance - walletCurrency - } + csvTransactions(walletIds: $walletIds) } } } `; /** - * __useWarningSecureAccountQuery__ + * __useExportCsvSettingQuery__ * - * To run a query within a React component, call `useWarningSecureAccountQuery` and pass it any options that fit your needs. - * When your component renders, `useWarningSecureAccountQuery` returns an object from Apollo Client that contains loading, error, and data properties + * To run a query within a React component, call `useExportCsvSettingQuery` and pass it any options that fit your needs. + * When your component renders, `useExportCsvSettingQuery` 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 } = useWarningSecureAccountQuery({ + * const { data, loading, error } = useExportCsvSettingQuery({ * variables: { + * walletIds: // value for 'walletIds' * }, * }); */ -export function useWarningSecureAccountQuery(baseOptions?: Apollo.QueryHookOptions) { +export function useExportCsvSettingQuery(baseOptions: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(WarningSecureAccountDocument, options); + return Apollo.useQuery(ExportCsvSettingDocument, options); } -export function useWarningSecureAccountLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { +export function useExportCsvSettingLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(WarningSecureAccountDocument, options); + return Apollo.useLazyQuery(ExportCsvSettingDocument, options); } -export type WarningSecureAccountQueryHookResult = ReturnType; -export type WarningSecureAccountLazyQueryHookResult = ReturnType; -export type WarningSecureAccountQueryResult = Apollo.QueryResult; +export type ExportCsvSettingQueryHookResult = ReturnType; +export type ExportCsvSettingLazyQueryHookResult = ReturnType; +export type ExportCsvSettingQueryResult = Apollo.QueryResult; +export const UserTotpDeleteDocument = gql` + mutation userTotpDelete { + userTotpDelete { + errors { + message + } + me { + id + phone + totpEnabled + email { + address + verified + } + } + } +} + `; +export type UserTotpDeleteMutationFn = Apollo.MutationFunction; + +/** + * __useUserTotpDeleteMutation__ + * + * To run a mutation, you first call `useUserTotpDeleteMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUserTotpDeleteMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [userTotpDeleteMutation, { data, loading, error }] = useUserTotpDeleteMutation({ + * variables: { + * }, + * }); + */ +export function useUserTotpDeleteMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UserTotpDeleteDocument, options); + } +export type UserTotpDeleteMutationHookResult = ReturnType; +export type UserTotpDeleteMutationResult = Apollo.MutationResult; +export type UserTotpDeleteMutationOptions = Apollo.BaseMutationOptions; export const AccountLimitsDocument = gql` query accountLimits { me { diff --git a/app/graphql/network-error-component.tsx b/app/graphql/network-error-component.tsx index ca4aeaa8b4..511c549939 100644 --- a/app/graphql/network-error-component.tsx +++ b/app/graphql/network-error-component.tsx @@ -60,7 +60,7 @@ export const NetworkErrorComponent: React.FC = () => { onPress: () => { setShowedAlert(false) navigation.dispatch(() => { - const routes = [{ name: "Primary" }] + const routes = [{ name: "getStarted" }] return CommonActions.reset({ routes, index: routes.length - 1, diff --git a/app/hooks/use-logout.ts b/app/hooks/use-logout.ts index 9e1a51f448..fb65f065be 100644 --- a/app/hooks/use-logout.ts +++ b/app/hooks/use-logout.ts @@ -36,9 +36,6 @@ const useLogout = () => { await KeyStoreWrapper.removePinAttempts() logLogout() - if (stateToDefault) { - resetState() - } await Promise.race([ userLogoutMutation({ variables: { input: { deviceToken } } }), @@ -55,6 +52,10 @@ const useLogout = () => { crashlytics().recordError(err) console.debug({ err }, `error logout`) } + } finally { + if (stateToDefault) { + resetState() + } } }, [resetState, userLogoutMutation], diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index b4c8e44bc4..72a52a520d 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -6,7 +6,7 @@ const en: BaseTranslation = { GaloyAddressScreen: { title: "Receive payment by using:", buttonTitle: "Set your address", - yourAddress: "Your {bankName: string} address", + yourLightningAddress: "Your Lightning address", notAbleToChange: "You won't be able to change your {bankName: string} address after it's set.", addressNotAvailable: "This {bankName: string} address is already taken.", @@ -14,7 +14,7 @@ const en: BaseTranslation = { merchantTitle: "For merchants", yourCashRegister: "Your Lightning Cash Register", yourPaycode: "Your Paycode", - copiedAddressToClipboard: "Copied {bankName: string} address to clipboard", + copiedLightningAddressToClipboard: "Copied Lightning address to clipboard", copiedPaycodeToClipboard: "Copied Paycode to clipboard", copiedCashRegisterLinkToClipboard: "Copied Cash Register Link to clipboard", howToUseIt: "How to use it?", @@ -2300,6 +2300,10 @@ const en: BaseTranslation = { pendingPayment: "The payment has been sent, but hasn't confirmed yet.\n\nIt's possible the payment will not confirm, in which case the funds will be returned to your account.", }, SettingsScreen: { + setByOs: "Set by OS", + pos: "Point of Sale", + posCopied: "Your point of sale link has been copied", + setYourLightningAddress: "Set Your Lightning Address", activated: "Activated", addressScreen: "Ways to get paid", tapUserName: "Tap to set username", @@ -2350,6 +2354,15 @@ const en: BaseTranslation = { } }, AccountScreen: { + fundsMoreThan5Dollars: "Your account has more than $5", + itsATrialAccount: "Trial accounts have reduced transaction limits and no recovery method. If you lose your phone or uninstall the app, your funds will be unrecoverable.", + accountBeingDeleted: "Your account is being deleted, please wait...", + dangerZone: "Danger Zone", + phoneDeletedSuccessfully: "Phone deleted successfully", + phoneNumber: "Phone Number", + tapToAddPhoneNumber: "Tap to add phone number", + loginMethods: "Login Methods", + level: "Level {level: string}", accountLevel: "Account Level", upgrade: "Upgrade your account", logOutAndDeleteLocalData: "Log out and clear all local data", @@ -2365,6 +2378,11 @@ const en: BaseTranslation = { btcBalanceWarning: "You have a bitcoin balance of {balance: string}.", secureYourAccount: "Register to secure your account", tapToAdd: "Tap to add", + tapToAddEmail: "Tap to add email", + unverifiedEmail: "Email (Unverified)", + email: "Email", + emailDeletedSuccessfully: "Email deleted successfully", + unverifiedEmailAdvice: "Unverified emails can't be used to login. You should re-verify your email address.", deleteEmailPromptTitle: "Delete email", deleteEmailPromptContent: "Are you sure you want to delete your email address? you will only be able to log back in with your phone number.", @@ -2390,6 +2408,7 @@ const en: BaseTranslation = { "Are you sure you want to delete your two-factor authentication?", copiedAccountId: "Copied your account ID to clipboard", yourAccountId: "Your Account ID", + accountId: "Account ID", copy: "Copy" }, TotpRegistrationInitiateScreen: { @@ -2425,6 +2444,8 @@ const en: BaseTranslation = { system: "Use System setting", light: "Use Light Mode", dark: "Use Dark Mode", + setToDark: "Dark Mode", + setToLight: "Light Mode", }, Languages: { DEFAULT: "Default (OS)", @@ -2485,6 +2506,7 @@ const en: BaseTranslation = { }, SetAddressModal: { title: "Set {bankName: string} address", + setLightningAddress: "Set Lightning address", Errors: { tooShort: "Address must be at least 3 characters long", tooLong: "Address must be at most 50 characters long", @@ -2579,7 +2601,15 @@ const en: BaseTranslation = { success: "Email {email: string} confirmed successfully", }, common: { + enabled: "Enabled", + notifications: "Notifications", + preferences: "Preferences", + securityAndPrivacy: "Security and Privacy", + advanced: "Advanced", + community: "Community", account: "Account", + trialAccount: "Trial Account", + blinkUser: "Blink User", transactionLimits: "Transaction Limits", activateWallet: "Activate Wallet", amountRequired: "Amount is required", diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index e7cfb28398..a34546883d 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -46,10 +46,9 @@ type RootTranslation = { */ buttonTitle: string /** - * Y​o​u​r​ ​{​b​a​n​k​N​a​m​e​}​ ​a​d​d​r​e​s​s - * @param {string} bankName + * Y​o​u​r​ ​L​i​g​h​t​n​i​n​g​ ​a​d​d​r​e​s​s */ - yourAddress: RequiredParams<'bankName'> + yourLightningAddress: string /** * Y​o​u​ ​w​o​n​'​t​ ​b​e​ ​a​b​l​e​ ​t​o​ ​c​h​a​n​g​e​ ​y​o​u​r​ ​{​b​a​n​k​N​a​m​e​}​ ​a​d​d​r​e​s​s​ ​a​f​t​e​r​ ​i​t​'​s​ ​s​e​t​. * @param {string} bankName @@ -77,10 +76,9 @@ type RootTranslation = { */ yourPaycode: string /** - * C​o​p​i​e​d​ ​{​b​a​n​k​N​a​m​e​}​ ​a​d​d​r​e​s​s​ ​t​o​ ​c​l​i​p​b​o​a​r​d - * @param {string} bankName + * C​o​p​i​e​d​ ​L​i​g​h​t​n​i​n​g​ ​a​d​d​r​e​s​s​ ​t​o​ ​c​l​i​p​b​o​a​r​d */ - copiedAddressToClipboard: RequiredParams<'bankName'> + copiedLightningAddressToClipboard: string /** * C​o​p​i​e​d​ ​P​a​y​c​o​d​e​ ​t​o​ ​c​l​i​p​b​o​a​r​d */ @@ -7166,6 +7164,22 @@ type RootTranslation = { pendingPayment: string } SettingsScreen: { + /** + * S​e​t​ ​b​y​ ​O​S + */ + setByOs: string + /** + * P​o​i​n​t​ ​o​f​ ​S​a​l​e + */ + pos: string + /** + * Y​o​u​r​ ​p​o​i​n​t​ ​o​f​ ​s​a​l​e​ ​l​i​n​k​ ​h​a​s​ ​b​e​e​n​ ​c​o​p​i​e​d + */ + posCopied: string + /** + * S​e​t​ ​Y​o​u​r​ ​L​i​g​h​t​n​i​n​g​ ​A​d​d​r​e​s​s + */ + setYourLightningAddress: string /** * A​c​t​i​v​a​t​e​d */ @@ -7313,6 +7327,43 @@ type RootTranslation = { } } AccountScreen: { + /** + * Y​o​u​r​ ​a​c​c​o​u​n​t​ ​h​a​s​ ​m​o​r​e​ ​t​h​a​n​ ​$​5 + */ + fundsMoreThan5Dollars: string + /** + * T​r​i​a​l​ ​a​c​c​o​u​n​t​s​ ​h​a​v​e​ ​r​e​d​u​c​e​d​ ​t​r​a​n​s​a​c​t​i​o​n​ ​l​i​m​i​t​s​ ​a​n​d​ ​n​o​ ​r​e​c​o​v​e​r​y​ ​m​e​t​h​o​d​.​ ​I​f​ ​y​o​u​ ​l​o​s​e​ ​y​o​u​r​ ​p​h​o​n​e​ ​o​r​ ​u​n​i​n​s​t​a​l​l​ ​t​h​e​ ​a​p​p​,​ ​y​o​u​r​ ​f​u​n​d​s​ ​w​i​l​l​ ​b​e​ ​u​n​r​e​c​o​v​e​r​a​b​l​e​. + */ + itsATrialAccount: string + /** + * Y​o​u​r​ ​a​c​c​o​u​n​t​ ​i​s​ ​b​e​i​n​g​ ​d​e​l​e​t​e​d​,​ ​p​l​e​a​s​e​ ​w​a​i​t​.​.​. + */ + accountBeingDeleted: string + /** + * D​a​n​g​e​r​ ​Z​o​n​e + */ + dangerZone: string + /** + * P​h​o​n​e​ ​d​e​l​e​t​e​d​ ​s​u​c​c​e​s​s​f​u​l​l​y + */ + phoneDeletedSuccessfully: string + /** + * P​h​o​n​e​ ​N​u​m​b​e​r + */ + phoneNumber: string + /** + * T​a​p​ ​t​o​ ​a​d​d​ ​p​h​o​n​e​ ​n​u​m​b​e​r + */ + tapToAddPhoneNumber: string + /** + * L​o​g​i​n​ ​M​e​t​h​o​d​s + */ + loginMethods: string + /** + * L​e​v​e​l​ ​{​l​e​v​e​l​} + * @param {string} level + */ + level: RequiredParams<'level'> /** * A​c​c​o​u​n​t​ ​L​e​v​e​l */ @@ -7370,6 +7421,26 @@ type RootTranslation = { * T​a​p​ ​t​o​ ​a​d​d */ tapToAdd: string + /** + * T​a​p​ ​t​o​ ​a​d​d​ ​e​m​a​i​l + */ + tapToAddEmail: string + /** + * E​m​a​i​l​ ​(​U​n​v​e​r​i​f​i​e​d​) + */ + unverifiedEmail: string + /** + * E​m​a​i​l + */ + email: string + /** + * E​m​a​i​l​ ​d​e​l​e​t​e​d​ ​s​u​c​c​e​s​s​f​u​l​l​y + */ + emailDeletedSuccessfully: string + /** + * U​n​v​e​r​i​f​i​e​d​ ​e​m​a​i​l​s​ ​c​a​n​'​t​ ​b​e​ ​u​s​e​d​ ​t​o​ ​l​o​g​i​n​.​ ​Y​o​u​ ​s​h​o​u​l​d​ ​r​e​-​v​e​r​i​f​y​ ​y​o​u​r​ ​e​m​a​i​l​ ​a​d​d​r​e​s​s​. + */ + unverifiedEmailAdvice: string /** * D​e​l​e​t​e​ ​e​m​a​i​l */ @@ -7454,6 +7525,10 @@ type RootTranslation = { * Y​o​u​r​ ​A​c​c​o​u​n​t​ ​I​D */ yourAccountId: string + /** + * A​c​c​o​u​n​t​ ​I​D + */ + accountId: string /** * C​o​p​y */ @@ -7542,6 +7617,14 @@ type RootTranslation = { * U​s​e​ ​D​a​r​k​ ​M​o​d​e */ dark: string + /** + * D​a​r​k​ ​M​o​d​e + */ + setToDark: string + /** + * L​i​g​h​t​ ​M​o​d​e + */ + setToLight: string } Languages: { /** @@ -7722,6 +7805,10 @@ type RootTranslation = { * @param {string} bankName */ title: RequiredParams<'bankName'> + /** + * S​e​t​ ​L​i​g​h​t​n​i​n​g​ ​a​d​d​r​e​s​s + */ + setLightningAddress: string Errors: { /** * A​d​d​r​e​s​s​ ​m​u​s​t​ ​b​e​ ​a​t​ ​l​e​a​s​t​ ​3​ ​c​h​a​r​a​c​t​e​r​s​ ​l​o​n​g @@ -7991,10 +8078,42 @@ type RootTranslation = { success: RequiredParams<'email'> } common: { + /** + * E​n​a​b​l​e​d + */ + enabled: string + /** + * N​o​t​i​f​i​c​a​t​i​o​n​s + */ + notifications: string + /** + * P​r​e​f​e​r​e​n​c​e​s + */ + preferences: string + /** + * S​e​c​u​r​i​t​y​ ​a​n​d​ ​P​r​i​v​a​c​y + */ + securityAndPrivacy: string + /** + * A​d​v​a​n​c​e​d + */ + advanced: string + /** + * C​o​m​m​u​n​i​t​y + */ + community: string /** * A​c​c​o​u​n​t */ account: string + /** + * T​r​i​a​l​ ​A​c​c​o​u​n​t + */ + trialAccount: string + /** + * B​l​i​n​k​ ​U​s​e​r + */ + blinkUser: string /** * T​r​a​n​s​a​c​t​i​o​n​ ​L​i​m​i​t​s */ @@ -8956,9 +9075,9 @@ export type TranslationFunctions = { */ buttonTitle: () => LocalizedString /** - * Your {bankName} address + * Your Lightning address */ - yourAddress: (arg: { bankName: string }) => LocalizedString + yourLightningAddress: () => LocalizedString /** * You won't be able to change your {bankName} address after it's set. */ @@ -8984,9 +9103,9 @@ export type TranslationFunctions = { */ yourPaycode: () => LocalizedString /** - * Copied {bankName} address to clipboard + * Copied Lightning address to clipboard */ - copiedAddressToClipboard: (arg: { bankName: string }) => LocalizedString + copiedLightningAddressToClipboard: () => LocalizedString /** * Copied Paycode to clipboard */ @@ -16034,6 +16153,22 @@ export type TranslationFunctions = { pendingPayment: () => LocalizedString } SettingsScreen: { + /** + * Set by OS + */ + setByOs: () => LocalizedString + /** + * Point of Sale + */ + pos: () => LocalizedString + /** + * Your point of sale link has been copied + */ + posCopied: () => LocalizedString + /** + * Set Your Lightning Address + */ + setYourLightningAddress: () => LocalizedString /** * Activated */ @@ -16180,6 +16315,42 @@ export type TranslationFunctions = { } } AccountScreen: { + /** + * Your account has more than $5 + */ + fundsMoreThan5Dollars: () => LocalizedString + /** + * Trial accounts have reduced transaction limits and no recovery method. If you lose your phone or uninstall the app, your funds will be unrecoverable. + */ + itsATrialAccount: () => LocalizedString + /** + * Your account is being deleted, please wait... + */ + accountBeingDeleted: () => LocalizedString + /** + * Danger Zone + */ + dangerZone: () => LocalizedString + /** + * Phone deleted successfully + */ + phoneDeletedSuccessfully: () => LocalizedString + /** + * Phone Number + */ + phoneNumber: () => LocalizedString + /** + * Tap to add phone number + */ + tapToAddPhoneNumber: () => LocalizedString + /** + * Login Methods + */ + loginMethods: () => LocalizedString + /** + * Level {level} + */ + level: (arg: { level: string }) => LocalizedString /** * Account Level */ @@ -16231,6 +16402,26 @@ export type TranslationFunctions = { * Tap to add */ tapToAdd: () => LocalizedString + /** + * Tap to add email + */ + tapToAddEmail: () => LocalizedString + /** + * Email (Unverified) + */ + unverifiedEmail: () => LocalizedString + /** + * Email + */ + email: () => LocalizedString + /** + * Email deleted successfully + */ + emailDeletedSuccessfully: () => LocalizedString + /** + * Unverified emails can't be used to login. You should re-verify your email address. + */ + unverifiedEmailAdvice: () => LocalizedString /** * Delete email */ @@ -16315,6 +16506,10 @@ export type TranslationFunctions = { * Your Account ID */ yourAccountId: () => LocalizedString + /** + * Account ID + */ + accountId: () => LocalizedString /** * Copy */ @@ -16403,6 +16598,14 @@ export type TranslationFunctions = { * Use Dark Mode */ dark: () => LocalizedString + /** + * Dark Mode + */ + setToDark: () => LocalizedString + /** + * Light Mode + */ + setToLight: () => LocalizedString } Languages: { /** @@ -16579,6 +16782,10 @@ export type TranslationFunctions = { * Set {bankName} address */ title: (arg: { bankName: string }) => LocalizedString + /** + * Set Lightning address + */ + setLightningAddress: () => LocalizedString Errors: { /** * Address must be at least 3 characters long @@ -16839,10 +17046,42 @@ export type TranslationFunctions = { success: (arg: { email: string }) => LocalizedString } common: { + /** + * Enabled + */ + enabled: () => LocalizedString + /** + * Notifications + */ + notifications: () => LocalizedString + /** + * Preferences + */ + preferences: () => LocalizedString + /** + * Security and Privacy + */ + securityAndPrivacy: () => LocalizedString + /** + * Advanced + */ + advanced: () => LocalizedString + /** + * Community + */ + community: () => LocalizedString /** * Account */ account: () => LocalizedString + /** + * Trial Account + */ + trialAccount: () => LocalizedString + /** + * Blink User + */ + blinkUser: () => LocalizedString /** * Transaction Limits */ diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index 286d62f1bd..2051d665ca 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -2,14 +2,14 @@ "GaloyAddressScreen": { "title": "Receive payment by using:", "buttonTitle": "Set your address", - "yourAddress": "Your {bankName: string} address", + "yourLightningAddress": "Your Lightning address", "notAbleToChange": "You won't be able to change your {bankName: string} address after it's set.", "addressNotAvailable": "This {bankName: string} address is already taken.", "somethingWentWrong": "Something went wrong. Please try again later.", "merchantTitle": "For merchants", "yourCashRegister": "Your Lightning Cash Register", "yourPaycode": "Your Paycode", - "copiedAddressToClipboard": "Copied {bankName: string} address to clipboard", + "copiedLightningAddressToClipboard": "Copied Lightning address to clipboard", "copiedPaycodeToClipboard": "Copied Paycode to clipboard", "copiedCashRegisterLinkToClipboard": "Copied Cash Register Link to clipboard", "howToUseIt": "How to use it?", @@ -2229,6 +2229,10 @@ "pendingPayment": "The payment has been sent, but hasn't confirmed yet.\n\nIt's possible the payment will not confirm, in which case the funds will be returned to your account." }, "SettingsScreen": { + "setByOs": "Set by OS", + "pos": "Point of Sale", + "posCopied": "Your point of sale link has been copied", + "setYourLightningAddress": "Set Your Lightning Address", "activated": "Activated", "addressScreen": "Ways to get paid", "tapUserName": "Tap to set username", @@ -2276,6 +2280,15 @@ } }, "AccountScreen": { + "fundsMoreThan5Dollars": "Your account has more than $5", + "itsATrialAccount": "Trial accounts have reduced transaction limits and no recovery method. If you lose your phone or uninstall the app, your funds will be unrecoverable.", + "accountBeingDeleted": "Your account is being deleted, please wait...", + "dangerZone": "Danger Zone", + "phoneDeletedSuccessfully": "Phone deleted successfully", + "phoneNumber": "Phone Number", + "tapToAddPhoneNumber": "Tap to add phone number", + "loginMethods": "Login Methods", + "level": "Level {level: string}", "accountLevel": "Account Level", "upgrade": "Upgrade your account", "logOutAndDeleteLocalData": "Log out and clear all local data", @@ -2288,6 +2301,11 @@ "btcBalanceWarning": "You have a bitcoin balance of {balance: string}.", "secureYourAccount": "Register to secure your account", "tapToAdd": "Tap to add", + "tapToAddEmail": "Tap to add email", + "unverifiedEmail": "Email (Unverified)", + "email": "Email", + "emailDeletedSuccessfully": "Email deleted successfully", + "unverifiedEmailAdvice": "Unverified emails can't be used to login. You should re-verify your email address.", "deleteEmailPromptTitle": "Delete email", "deleteEmailPromptContent": "Are you sure you want to delete your email address? you will only be able to log back in with your phone number.", "deletePhonePromptTitle": "Delete phone", @@ -2309,6 +2327,7 @@ "totpDeleteAlertContent": "Are you sure you want to delete your two-factor authentication?", "copiedAccountId": "Copied your account ID to clipboard", "yourAccountId": "Your Account ID", + "accountId": "Account ID", "copy": "Copy" }, "TotpRegistrationInitiateScreen": { @@ -2337,7 +2356,9 @@ "info": "Pick your preferred theme for using Blink, or choose to keep it synced with your system settings.", "system": "Use System setting", "light": "Use Light Mode", - "dark": "Use Dark Mode" + "dark": "Use Dark Mode", + "setToDark": "Dark Mode", + "setToLight": "Light Mode" }, "Languages": { "DEFAULT": "Default (OS)" @@ -2395,6 +2416,7 @@ }, "SetAddressModal": { "title": "Set {bankName: string} address", + "setLightningAddress": "Set Lightning address", "Errors": { "tooShort": "Address must be at least 3 characters long", "tooLong": "Address must be at most 50 characters long", @@ -2476,7 +2498,15 @@ "success": "Email {email: string} confirmed successfully" }, "common": { + "enabled": "Enabled", + "notifications": "Notifications", + "preferences": "Preferences", + "securityAndPrivacy": "Security and Privacy", + "advanced": "Advanced", + "community": "Community", "account": "Account", + "trialAccount": "Trial Account", + "blinkUser": "Blink User", "transactionLimits": "Transaction Limits", "activateWallet": "Activate Wallet", "amountRequired": "Amount is required", diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx index 3ea775ea70..d871638104 100644 --- a/app/navigation/root-navigator.tsx +++ b/app/navigation/root-navigator.tsx @@ -38,7 +38,7 @@ import SendBitcoinCompletedScreen from "@app/screens/send-bitcoin-screen/send-bi import SendBitcoinConfirmationScreen from "@app/screens/send-bitcoin-screen/send-bitcoin-confirmation-screen" import SendBitcoinDestinationScreen from "@app/screens/send-bitcoin-screen/send-bitcoin-destination-screen" import SendBitcoinDetailsScreen from "@app/screens/send-bitcoin-screen/send-bitcoin-details-screen" -import { AccountScreen } from "@app/screens/settings-screen/account-screen" +import { AccountScreen } from "@app/screens/settings-screen/account" import { DefaultWalletScreen } from "@app/screens/settings-screen/default-wallet" import { DisplayCurrencyScreen } from "@app/screens/settings-screen/display-currency-screen" import { NotificationSettingsScreen } from "@app/screens/settings-screen/notifications-screen" diff --git a/app/screens/galoy-address-screen/address-component.tsx b/app/screens/galoy-address-screen/address-component.tsx index 30f821dc7a..46b1933881 100644 --- a/app/screens/galoy-address-screen/address-component.tsx +++ b/app/screens/galoy-address-screen/address-component.tsx @@ -1,7 +1,6 @@ import { Linking, Pressable, Share, View } from "react-native" import { GaloyIcon } from "@app/components/atomic/galoy-icon" -import { useAppConfig } from "@app/hooks" import { useI18nContext } from "@app/i18n/i18n-react" import { toastShow } from "@app/utils/toast" import Clipboard from "@react-native-clipboard/clipboard" @@ -33,8 +32,6 @@ const AddressComponent: React.FC = ({ theme: { colors }, } = useTheme() const styles = useStyles() - const { appConfig } = useAppConfig() - const { name: bankName } = appConfig.galoyInstance const trimmedUrl = address.includes("https://") || address.includes("http://") ? address.replace("https://", "") @@ -46,9 +43,7 @@ const AddressComponent: React.FC = ({ message: (translations) => { switch (addressType) { case addressTypes.lightning: - return translations.GaloyAddressScreen.copiedAddressToClipboard({ - bankName, - }) + return translations.GaloyAddressScreen.copiedLightningAddressToClipboard() case addressTypes.pos: return translations.GaloyAddressScreen.copiedCashRegisterLinkToClipboard() case addressTypes.paycode: diff --git a/app/screens/galoy-address-screen/address-screen.tsx b/app/screens/galoy-address-screen/address-screen.tsx index abf37be13b..9ebd09aae0 100644 --- a/app/screens/galoy-address-screen/address-screen.tsx +++ b/app/screens/galoy-address-screen/address-screen.tsx @@ -54,7 +54,6 @@ export const GaloyAddressScreen = () => { }) const { appConfig } = useAppConfig() - const { name: bankName } = appConfig.galoyInstance const [explainerModalVisible, setExplainerModalVisible] = React.useState(false) const [isPosExplainerModalOpen, setIsPosExplainerModalOpen] = React.useState(false) @@ -94,7 +93,7 @@ export const GaloyAddressScreen = () => { { nextFetchPolicy: "cache-and-network", }) - const loading = loadingAuthed || loadingPrice || loadingUnauthed + // keep settings info cached and ignore network call if it's already cached + const { loading: loadingSettings } = useSettingsScreenQuery({ + skip: !isAuthed, + fetchPolicy: "cache-first", + // this enables offline mode use-case + nextFetchPolicy: "cache-and-network", + }) + + const loading = loadingAuthed || loadingPrice || loadingUnauthed || loadingSettings const refetch = React.useCallback(() => { if (isAuthed) { diff --git a/app/screens/people-screen/tab-icon.tsx b/app/screens/people-screen/tab-icon.tsx index 2a9a105716..29c0ce7d99 100644 --- a/app/screens/people-screen/tab-icon.tsx +++ b/app/screens/people-screen/tab-icon.tsx @@ -30,7 +30,7 @@ export const PeopleTabIcon: React.FC = ({ color, focused }) => { useEffect(() => { const innerCircleCachedValue = cachedData?.innerCircleValue || -1 const innerCircleRealValue = - networkData?.me?.defaultAccount.welcomeProfile?.innerCircleAllTimeCount || -1 + networkData?.me?.defaultAccount?.welcomeProfile?.innerCircleAllTimeCount || -1 if (innerCircleCachedValue === -1 && innerCircleRealValue === -1) { setHidden(false) diff --git a/app/screens/phone-auth-screen/phone-registration-validation.tsx b/app/screens/phone-auth-screen/phone-registration-validation.tsx index 0a37f6fff1..eac227eb9c 100644 --- a/app/screens/phone-auth-screen/phone-registration-validation.tsx +++ b/app/screens/phone-auth-screen/phone-registration-validation.tsx @@ -7,7 +7,6 @@ import { GaloyErrorBox } from "@app/components/atomic/galoy-error-box" import { GaloyInfo } from "@app/components/atomic/galoy-info" import { GaloySecondaryButton } from "@app/components/atomic/galoy-secondary-button" import { - AccountScreenDocument, PhoneCodeChannelType, useUserPhoneRegistrationValidateMutation, } from "@app/graphql/generated" @@ -191,7 +190,6 @@ export const PhoneRegistrationValidateScreen: React.FC< logAddPhoneAttempt() const { data } = await phoneValidate({ variables: { input: { phone, code } }, - refetchQueries: [AccountScreenDocument], }) const errors = data?.userPhoneRegistrationValidate?.errors || [] diff --git a/app/screens/settings-screen/account-screen.stories.tsx b/app/screens/settings-screen/account-screen.stories.tsx index 7445b082f6..53acef1449 100644 --- a/app/screens/settings-screen/account-screen.stories.tsx +++ b/app/screens/settings-screen/account-screen.stories.tsx @@ -5,16 +5,16 @@ import { Meta } from "@storybook/react" import { StoryScreen } from "../../../.storybook/views" import { createCache } from "../../graphql/cache" -import { AccountScreenDocument } from "../../graphql/generated" +import { SettingsScreenDocument } from "../../graphql/generated" import { AccountLevel, LevelContextProvider } from "../../graphql/level-context" import mocks from "../../graphql/mocks" -import { AccountScreen } from "./account-screen" +import { AccountScreen } from "../settings-screen/account/account-screen" const mocksLevelOne = [ ...mocks, { request: { - query: AccountScreenDocument, + query: SettingsScreenDocument, }, result: { data: { @@ -55,7 +55,7 @@ const mocksNoEmail = [ ...mocks, { request: { - query: AccountScreenDocument, + query: SettingsScreenDocument, }, result: { data: { @@ -103,6 +103,7 @@ export const Unauthed = () => ( value={{ isAtLeastLevelZero: false, isAtLeastLevelOne: false, + isAtLeastLevelTwo: true, currentLevel: AccountLevel.NonAuth, }} > @@ -117,6 +118,7 @@ export const AuthedEmailNotSet = () => ( value={{ isAtLeastLevelZero: true, isAtLeastLevelOne: true, + isAtLeastLevelTwo: true, currentLevel: AccountLevel.One, }} > @@ -131,6 +133,7 @@ export const AuthedEmailSet = () => ( value={{ isAtLeastLevelZero: true, isAtLeastLevelOne: true, + isAtLeastLevelTwo: true, currentLevel: AccountLevel.One, }} > diff --git a/app/screens/settings-screen/account-screen.tsx b/app/screens/settings-screen/account-screen.tsx deleted file mode 100644 index 74a1eaa5a4..0000000000 --- a/app/screens/settings-screen/account-screen.tsx +++ /dev/null @@ -1,628 +0,0 @@ -import React from "react" -import { Alert, TextInput, View } from "react-native" -import Modal from "react-native-modal" - -import { gql } from "@apollo/client" -import { GaloyPrimaryButton } from "@app/components/atomic/galoy-primary-button" -import { GaloySecondaryButton } from "@app/components/atomic/galoy-secondary-button" -import { Screen } from "@app/components/screen" -import { UpgradeAccountModal } from "@app/components/upgrade-account-modal" -import { CONTACT_EMAIL_ADDRESS } from "@app/config" -import { - useAccountDeleteMutation, - useAccountScreenQuery, - useBetaQuery, - useUserEmailDeleteMutation, - useUserEmailRegistrationInitiateMutation, - useUserPhoneDeleteMutation, - useUserTotpDeleteMutation, -} from "@app/graphql/generated" -import { AccountLevel, useLevel } from "@app/graphql/level-context" -import { getBtcWallet, getUsdWallet } from "@app/graphql/wallets-utils" -import { useDisplayCurrency } from "@app/hooks/use-display-currency" -import useLogout from "@app/hooks/use-logout" -import { useI18nContext } from "@app/i18n/i18n-react" -import { RootStackParamList } from "@app/navigation/stack-param-lists" -import { toBtcMoneyAmount, toUsdMoneyAmount } from "@app/types/amounts" -import { testProps } from "@app/utils/testProps" -import { useNavigation } from "@react-navigation/native" -import { StackNavigationProp } from "@react-navigation/stack" -import { Text, makeStyles, useTheme } from "@rneui/themed" - -import { AccountId } from "./account-id" -import { SettingsRow } from "./settings-row" -import { useShowWarningSecureAccount } from "./show-warning-secure-account" - -gql` - query accountScreen { - me { - id - phone - totpEnabled - email { - address - verified - } - defaultAccount { - id - level - wallets { - id - balance - walletCurrency - } - } - } - } - - mutation accountDelete { - accountDelete { - errors { - message - } - success - } - } - - mutation userEmailDelete { - userEmailDelete { - errors { - message - } - me { - id - phone - totpEnabled - email { - address - verified - } - } - } - } - - mutation userPhoneDelete { - userPhoneDelete { - errors { - message - } - me { - id - phone - totpEnabled - email { - address - verified - } - } - } - } - - mutation userTotpDelete { - userTotpDelete { - errors { - message - } - me { - id - phone - totpEnabled - email { - address - verified - } - } - } - } -` - -export const AccountScreen = () => { - const navigation = - useNavigation>() - - const { logout } = useLogout() - const { LL } = useI18nContext() - const styles = useStyles() - - const { - theme: { colors }, - } = useTheme() - - const { isAtLeastLevelZero, currentLevel, isAtLeastLevelOne } = useLevel() - - const [deleteAccount] = useAccountDeleteMutation() - const [emailDeleteMutation] = useUserEmailDeleteMutation() - const [phoneDeleteMutation] = useUserPhoneDeleteMutation() - const [totpDeleteMutation] = useUserTotpDeleteMutation() - - const [text, setText] = React.useState("") - const [modalVisible, setModalVisible] = React.useState(false) - const [upgradeAccountModalVisible, setUpgradeAccountModalVisible] = - React.useState(false) - const closeUpgradeAccountModal = () => setUpgradeAccountModalVisible(false) - const openUpgradeAccountModal = () => setUpgradeAccountModalVisible(true) - - const { data } = useAccountScreenQuery({ - fetchPolicy: "cache-and-network", - skip: !isAtLeastLevelZero, - }) - - const email = data?.me?.email?.address - const emailVerified = Boolean(email) && Boolean(data?.me?.email?.verified) - const emailUnverified = Boolean(email) && !data?.me?.email?.verified - const phoneVerified = Boolean(data?.me?.phone) - const phoneAndEmailVerified = phoneVerified && emailVerified - const emailString = String(email) - const totpEnabled = Boolean(data?.me?.totpEnabled) - - const showWarningSecureAccount = useShowWarningSecureAccount() - - const [setEmailMutation] = useUserEmailRegistrationInitiateMutation() - - const btcWallet = getBtcWallet(data?.me?.defaultAccount?.wallets) - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) - - const usdWalletBalance = toUsdMoneyAmount(usdWallet?.balance) - const btcWalletBalance = toBtcMoneyAmount(btcWallet?.balance) - - const { formatMoneyAmount } = useDisplayCurrency() - - let usdBalanceWarning = "" - let btcBalanceWarning = "" - let balancePositive = false - if (usdWalletBalance.amount > 0) { - const balance = - formatMoneyAmount && formatMoneyAmount({ moneyAmount: usdWalletBalance }) - usdBalanceWarning = LL.AccountScreen.usdBalanceWarning({ balance }) - balancePositive = true - } - - if (btcWalletBalance.amount > 0) { - const balance = - formatMoneyAmount && formatMoneyAmount({ moneyAmount: btcWalletBalance }) - btcBalanceWarning = LL.AccountScreen.btcBalanceWarning({ balance }) - balancePositive = true - } - - const dataBeta = useBetaQuery() - const beta = dataBeta.data?.beta ?? false - beta - - const deletePhonePrompt = async () => { - Alert.alert( - LL.AccountScreen.deletePhonePromptTitle(), - LL.AccountScreen.deletePhonePromptContent(), - [ - { text: LL.common.cancel(), onPress: () => {} }, - { - text: LL.common.yes(), - onPress: async () => { - deletePhone() - }, - }, - ], - ) - } - - const deleteEmailPrompt = async () => { - Alert.alert( - LL.AccountScreen.deleteEmailPromptTitle(), - LL.AccountScreen.deleteEmailPromptContent(), - [ - { text: LL.common.cancel(), onPress: () => {} }, - { - text: LL.common.yes(), - onPress: async () => { - deleteEmail() - }, - }, - ], - ) - } - - const deletePhone = async () => { - try { - await phoneDeleteMutation() - } catch (err) { - let message = "" - if (err instanceof Error) { - message = err?.message - } - Alert.alert(LL.common.error(), message) - } - } - - const deleteEmail = async () => { - try { - await emailDeleteMutation() - } catch (err) { - let message = "" - if (err instanceof Error) { - message = err?.message - } - Alert.alert(LL.common.error(), message) - } - } - - const logoutAlert = () => { - const logAlertContent = () => { - const phoneNumber = String(data?.me?.phone) - if (phoneAndEmailVerified) { - return LL.AccountScreen.logoutAlertContentPhoneEmail({ - phoneNumber, - email: emailString, - }) - } else if (emailVerified) { - return LL.AccountScreen.logoutAlertContentEmail({ email: emailString }) - } - // phone verified - return LL.AccountScreen.logoutAlertContentPhone({ phoneNumber }) - } - - Alert.alert(LL.AccountScreen.logoutAlertTitle(), logAlertContent(), [ - { - text: LL.common.cancel(), - onPress: () => console.log("Cancel Pressed"), - style: "cancel", - }, - { - text: LL.AccountScreen.IUnderstand(), - onPress: logoutAction, - }, - ]) - } - - const logoutAction = async () => { - try { - await logout() - Alert.alert(LL.common.loggedOut(), "", [ - { - text: LL.common.ok(), - onPress: () => - navigation.reset({ - index: 0, - routes: [{ name: "getStarted" }], - }), - }, - ]) - } catch (err) { - // TODO: figure out why ListItem onPress is swallowing errors - console.error(err) - } - } - - const deleteAccountAction = async () => { - if (balancePositive) { - const fullMessage = - usdBalanceWarning + - "\n" + - btcBalanceWarning + - "\n" + - LL.support.deleteAccountBalanceWarning() - - Alert.alert(LL.common.warning(), fullMessage, [ - { text: LL.common.cancel(), onPress: () => {} }, - { - text: LL.common.yes(), - onPress: async () => setModalVisible(true), - }, - ]) - } else { - setModalVisible(true) - } - } - - const deleteUserAccount = async () => { - try { - const res = await deleteAccount() - - if (res.data?.accountDelete?.success) { - await logout() - Alert.alert(LL.support.bye(), LL.support.deleteAccountConfirmation(), [ - { - text: LL.common.ok(), - onPress: () => - navigation.reset({ - index: 0, - routes: [{ name: "getStarted" }], - }), - }, - ]) - } else { - Alert.alert( - LL.common.error(), - LL.support.deleteAccountError({ email: CONTACT_EMAIL_ADDRESS }) + - "\n\n" + - res.data?.accountDelete?.errors[0].message, - ) - } - } catch (err) { - console.error(err) - Alert.alert( - LL.common.error(), - LL.support.deleteAccountError({ email: CONTACT_EMAIL_ADDRESS }), - ) - } - } - - const tryConfirmEmailAgain = async (email: string) => { - try { - await emailDeleteMutation({ - // to avoid flacky behavior - // this could lead to inconsistent state if delete works but set fails - fetchPolicy: "no-cache", - }) - - const { data } = await setEmailMutation({ - variables: { input: { email } }, - }) - - const errors = data?.userEmailRegistrationInitiate.errors - if (errors && errors.length > 0) { - Alert.alert(errors[0].message) - } - - const emailRegistrationId = data?.userEmailRegistrationInitiate.emailRegistrationId - - if (emailRegistrationId) { - navigation.navigate("emailRegistrationValidate", { - emailRegistrationId, - email, - }) - } else { - console.warn("no flow returned") - } - } catch (err) { - console.error(err, "error in setEmailMutation") - } finally { - // setLoading(false) - } - } - - const confirmEmailAgain = async () => { - if (email) { - Alert.alert( - LL.AccountScreen.emailUnverified(), - LL.AccountScreen.emailUnverifiedContent(), - [ - { text: LL.common.cancel(), onPress: () => {} }, - { - text: LL.common.ok(), - onPress: () => tryConfirmEmailAgain(email), - }, - ], - ) - } else { - console.error("email not set, wrong flow") - } - } - - const totpDelete = async () => { - Alert.alert( - LL.AccountScreen.totpDeleteAlertTitle(), - LL.AccountScreen.totpDeleteAlertContent(), - [ - { text: LL.common.cancel(), onPress: () => {} }, - { - text: LL.common.ok(), - onPress: async () => { - const res = await totpDeleteMutation() - if (res.data?.userTotpDelete?.me?.totpEnabled === false) { - Alert.alert(LL.AccountScreen.totpDeactivated()) - } else { - console.log(res.data?.userTotpDelete.errors) - Alert.alert(LL.common.error(), res.data?.userTotpDelete?.errors[0]?.message) - } - }, - }, - ], - ) - } - - const accountSettingsList: SettingRow[] = [ - { - category: LL.AccountScreen.accountLevel(), - id: "level", - icon: "flash-outline", - subTitleText: currentLevel, - enabled: false, - greyed: true, - }, - { - category: LL.common.transactionLimits(), - id: "limits", - icon: "custom-info-icon", - action: () => navigation.navigate("transactionLimitsScreen"), - enabled: isAtLeastLevelZero, - greyed: !isAtLeastLevelZero, - styleDivider: true, - }, - - { - category: LL.AccountScreen.upgrade(), - id: "upgrade-to-level-two", - icon: "person-outline", - action: () => navigation.navigate("fullOnboardingFlow"), - enabled: true, - hidden: currentLevel !== AccountLevel.One, - styleDivider: true, - }, - { - category: LL.common.backupAccount(), - id: "upgrade-to-level-one", - icon: "person-outline", - subTitleText: showWarningSecureAccount ? LL.AccountScreen.secureYourAccount() : "", - chevronLogo: showWarningSecureAccount ? "alert-circle-outline" : undefined, - chevronColor: showWarningSecureAccount ? colors.primary : undefined, - chevronSize: showWarningSecureAccount ? 24 : undefined, - action: openUpgradeAccountModal, - enabled: true, - hidden: currentLevel !== AccountLevel.Zero, - styleDivider: true, - }, - - { - category: LL.AccountScreen.phoneNumberAuthentication(), - id: "phone", - icon: "call-outline", - subTitleText: data?.me?.phone, - action: phoneVerified - ? deletePhonePrompt - : () => navigation.navigate("phoneRegistrationInitiate"), - enabled: phoneAndEmailVerified || !phoneVerified, - chevronLogo: phoneAndEmailVerified ? "close-circle-outline" : undefined, - chevronColor: phoneAndEmailVerified ? colors.red : undefined, - chevronSize: phoneAndEmailVerified ? 28 : undefined, - hidden: !isAtLeastLevelOne, - }, - - { - category: LL.AccountScreen.emailAuthentication(), - id: "email", - icon: "mail-outline", - subTitleText: email ?? LL.AccountScreen.tapToAdd(), - action: phoneAndEmailVerified - ? deleteEmailPrompt - : () => navigation.navigate("emailRegistrationInitiate"), - enabled: phoneAndEmailVerified || !emailUnverified, - chevronLogo: phoneAndEmailVerified ? "close-circle-outline" : undefined, - chevronColor: phoneAndEmailVerified ? colors.red : undefined, - chevronSize: phoneAndEmailVerified ? 28 : undefined, - styleDivider: !emailUnverified, - hidden: !isAtLeastLevelOne, - }, - { - category: LL.AccountScreen.unverified(), - id: "confirm-email", - icon: "checkmark-circle-outline", - subTitleText: LL.AccountScreen.unverifiedContent(), - action: confirmEmailAgain, - enabled: Boolean(emailUnverified), - chevron: false, - dangerous: true, - hidden: !emailUnverified, - }, - { - category: LL.AccountScreen.removeEmail(), - id: "remove-email", - icon: "trash-outline", - action: deleteEmailPrompt, - enabled: Boolean(emailUnverified), - chevron: false, - styleDivider: true, - hidden: !emailUnverified, - }, - { - category: LL.AccountScreen.totp(), - id: "totp", - icon: "lock-closed-outline", - action: totpEnabled - ? totpDelete - : () => navigation.navigate("totpRegistrationInitiate"), - enabled: true, - chevronLogo: totpEnabled ? "close-circle-outline" : undefined, - chevronColor: totpEnabled ? colors.red : undefined, - chevronSize: totpEnabled ? 28 : undefined, - styleDivider: true, - }, - ] - - if (isAtLeastLevelOne) { - accountSettingsList.push({ - category: LL.AccountScreen.logOutAndDeleteLocalData(), - id: "logout", - icon: "log-out-outline", - action: logoutAlert, - enabled: true, - greyed: false, - chevron: false, - styleDivider: true, - }) - } - - if (currentLevel !== AccountLevel.NonAuth) { - accountSettingsList.push({ - category: LL.support.deleteAccount(), - id: "deleteAccount", - icon: "trash-outline", - dangerous: true, - action: deleteAccountAction, - chevron: false, - enabled: true, - greyed: false, - styleDivider: true, - }) - } - - const AccountDeletionModal = ( - setModalVisible(false)} - backdropOpacity={0.3} - backdropColor={colors.grey3} - avoidKeyboard={true} - > - - {LL.support.typeDelete({ delete: LL.support.delete() })} - - { - setModalVisible(false) - Alert.alert( - LL.support.finalConfirmationAccountDeletionTitle(), - LL.support.finalConfirmationAccountDeletionMessage(), - [ - { text: LL.common.cancel(), onPress: () => {} }, - { text: LL.common.ok(), onPress: () => deleteUserAccount() }, - ], - ) - }} - containerStyle={styles.mainButton} - /> - setModalVisible(false)} /> - - - ) - - return ( - - - {accountSettingsList.map((setting) => ( - - ))} - {AccountDeletionModal} - - - ) -} - -const useStyles = makeStyles(({ colors }) => ({ - view: { - marginHorizontal: 20, - backgroundColor: colors.white, - padding: 20, - borderRadius: 20, - }, - - textInput: { - height: 40, - borderColor: colors.grey3, - borderWidth: 1, - paddingVertical: 12, - color: colors.black, - }, - - mainButton: { marginVertical: 20 }, -})) diff --git a/app/screens/settings-screen/account/account-delete-context.tsx b/app/screens/settings-screen/account/account-delete-context.tsx new file mode 100644 index 0000000000..9ac4c91013 --- /dev/null +++ b/app/screens/settings-screen/account/account-delete-context.tsx @@ -0,0 +1,54 @@ +import { PropsWithChildren, createContext, useContext, useState } from "react" +import { ActivityIndicator, View } from "react-native" + +import { useI18nContext } from "@app/i18n/i18n-react" +import { Text, makeStyles, useTheme } from "@rneui/themed" + +type AccountDeleteContextType = { + setAccountIsBeingDeleted: React.Dispatch> +} + +const AccountDeleteContext = createContext({ + setAccountIsBeingDeleted: () => {}, +}) + +export const AccountDeleteContextProvider: React.FC = ({ + children, +}) => { + const styles = useStyles() + const { + theme: { colors }, + } = useTheme() + + const { LL } = useI18nContext() + + const [accountIsBeingDeleted, setAccountIsBeingDeleted] = useState(false) + + const Loading = ( + + + + {LL.AccountScreen.accountBeingDeleted()} + + + ) + + return ( + + {accountIsBeingDeleted ? Loading : children} + + ) +} + +export const useAccountDeleteContext = () => useContext(AccountDeleteContext) + +const useStyles = makeStyles(() => ({ + center: { + height: "100%", + display: "flex", + flexDirection: "column", + rowGap: 10, + justifyContent: "center", + alignItems: "center", + }, +})) diff --git a/app/screens/settings-screen/account/account-screen.tsx b/app/screens/settings-screen/account/account-screen.tsx new file mode 100644 index 0000000000..35f1812abc --- /dev/null +++ b/app/screens/settings-screen/account/account-screen.tsx @@ -0,0 +1,59 @@ +import { ScrollView } from "react-native-gesture-handler" + +import { Screen } from "@app/components/screen" +import { useLevel } from "@app/graphql/level-context" +import { useI18nContext } from "@app/i18n/i18n-react" +import { testProps } from "@app/utils/testProps" +import { makeStyles } from "@rneui/themed" + +import { SettingsGroup } from "../group" +import { TotpSetting } from "../totp" +import { AccountDeleteContextProvider } from "./account-delete-context" +import { AccountBanner } from "./banner" +import { AccountId } from "./id" +import { DangerZoneSettings } from "./settings/danger-zone" +import { EmailSetting } from "./settings/email" +import { PhoneSetting } from "./settings/phone" +import { UpgradeAccountLevelOne } from "./settings/upgrade" +import { UpgradeTrialAccount } from "./settings/upgrade-trial-account" + +export const AccountScreen: React.FC = () => { + const styles = useStyles() + const { LL } = useI18nContext() + + const { isAtLeastLevelOne } = useLevel() + + return ( + + + + + + + {isAtLeastLevelOne && ( + + )} + + + + + + ) +} + +const useStyles = makeStyles(() => ({ + outer: { + marginTop: 4, + paddingHorizontal: 12, + paddingBottom: 20, + display: "flex", + flexDirection: "column", + rowGap: 12, + }, +})) diff --git a/app/screens/settings-screen/account/banner.tsx b/app/screens/settings-screen/account/banner.tsx new file mode 100644 index 0000000000..df41712bfb --- /dev/null +++ b/app/screens/settings-screen/account/banner.tsx @@ -0,0 +1,70 @@ +/** + * This component is the top banner on the settings screen + * It shows the user their own username with a people icon + * If the user isn't logged in, it shows Login or Create Account + * Later on, this will support switching between accounts + */ +import { View } from "react-native" +import { TouchableWithoutFeedback } from "react-native-gesture-handler" + +import { GaloyIcon } from "@app/components/atomic/galoy-icon" +import { useSettingsScreenQuery } from "@app/graphql/generated" +import { AccountLevel, useLevel } from "@app/graphql/level-context" +import { useI18nContext } from "@app/i18n/i18n-react" +import { RootStackParamList } from "@app/navigation/stack-param-lists" +import { useNavigation } from "@react-navigation/native" +import { StackNavigationProp } from "@react-navigation/stack" +import { Text, makeStyles, useTheme, Skeleton } from "@rneui/themed" + +export const AccountBanner = () => { + const styles = useStyles() + const { LL } = useI18nContext() + + const navigation = useNavigation>() + + const { currentLevel } = useLevel() + const isUserLoggedIn = currentLevel !== AccountLevel.NonAuth + + const { data, loading } = useSettingsScreenQuery({ fetchPolicy: "cache-first" }) + + const usernameTitle = data?.me?.username || LL.common.blinkUser() + + if (loading) return + + return ( + + !isUserLoggedIn && + navigation.reset({ + index: 0, + routes: [{ name: "getStarted" }], + }) + } + > + + + + {isUserLoggedIn ? usernameTitle : LL.SettingsScreen.logInOrCreateAccount()} + + + + ) +} + +export const AccountIcon: React.FC<{ size: number }> = ({ size }) => { + const { + theme: { colors }, + } = useTheme() + return +} + +const useStyles = makeStyles(() => ({ + outer: { + height: 70, + padding: 4, + display: "flex", + flexDirection: "row", + alignItems: "center", + columnGap: 12, + }, +})) diff --git a/app/screens/settings-screen/account-id.tsx b/app/screens/settings-screen/account/id.tsx similarity index 60% rename from app/screens/settings-screen/account-id.tsx rename to app/screens/settings-screen/account/id.tsx index 0235c2686f..b12a2e1df9 100644 --- a/app/screens/settings-screen/account-id.tsx +++ b/app/screens/settings-screen/account/id.tsx @@ -2,15 +2,14 @@ import { useCallback } from "react" import { View } from "react-native" import { GaloyIconButton } from "@app/components/atomic/galoy-icon-button" -import { useAccountScreenQuery } from "@app/graphql/generated" +import { useSettingsScreenQuery } from "@app/graphql/generated" import { useI18nContext } from "@app/i18n/i18n-react" -import { testProps } from "@app/utils/testProps" import { toastShow } from "@app/utils/toast" import Clipboard from "@react-native-clipboard/clipboard" -import { Text, makeStyles } from "@rneui/themed" +import { Skeleton, Text, makeStyles } from "@rneui/themed" export const AccountId: React.FC = () => { - const { data } = useAccountScreenQuery() + const { data, loading } = useSettingsScreenQuery() const { LL } = useI18nContext() const styles = useStyles() @@ -30,44 +29,49 @@ export const AccountId: React.FC = () => { }, [LL, accountId]) return ( - - - {LL.AccountScreen.yourAccountId()} + + + {LL.AccountScreen.accountId()} - - - - {Array(20) - .fill(null) - .map((_, i) => ( - - ))} + {loading ? ( + + ) : ( + + + + {Array(20) + .fill(null) + .map((_, i) => ( + + ))} + + + {last6digitsOfAccountId} + - - {last6digitsOfAccountId} - + - - + )} ) } const useStyles = makeStyles(({ colors }) => ({ - accountId: { - margin: 20, - }, circle: { height: 4, width: 4, borderRadius: 2, backgroundColor: colors.black, }, + spacing: { + paddingHorizontal: 10, + paddingVertical: 6, + }, wrapper: { marginTop: 5, display: "flex", @@ -76,8 +80,8 @@ const useStyles = makeStyles(({ colors }) => ({ justifyContent: "space-between", backgroundColor: colors.grey5, borderRadius: 10, - paddingHorizontal: 10, - paddingVertical: 6, + marginBottom: 10, + height: 48, }, accIdWrapper: { display: "flex", diff --git a/app/screens/settings-screen/account/index.ts b/app/screens/settings-screen/account/index.ts new file mode 100644 index 0000000000..e0a5266ce9 --- /dev/null +++ b/app/screens/settings-screen/account/index.ts @@ -0,0 +1 @@ +export * from "./account-screen" diff --git a/app/screens/settings-screen/account/login-methods-hook.ts b/app/screens/settings-screen/account/login-methods-hook.ts new file mode 100644 index 0000000000..5627698b0f --- /dev/null +++ b/app/screens/settings-screen/account/login-methods-hook.ts @@ -0,0 +1,22 @@ +import { useSettingsScreenQuery } from "@app/graphql/generated" + +export const useLoginMethods = () => { + const { data } = useSettingsScreenQuery({ fetchPolicy: "cache-and-network" }) + + const email = data?.me?.email?.address || undefined + const emailVerified = Boolean(email && data?.me?.email?.verified) + + const phone = data?.me?.phone + const phoneVerified = Boolean(phone) + + const bothEmailAndPhoneVerified = phoneVerified && emailVerified + + return { + loading: !data, // Data would auto refresh after network call + email, + emailVerified, + phone, + phoneVerified, + bothEmailAndPhoneVerified, + } +} diff --git a/app/screens/settings-screen/account/settings/danger-zone.tsx b/app/screens/settings-screen/account/settings/danger-zone.tsx new file mode 100644 index 0000000000..92ed37617b --- /dev/null +++ b/app/screens/settings-screen/account/settings/danger-zone.tsx @@ -0,0 +1,35 @@ +import { View } from "react-native" + +import { AccountLevel, useLevel } from "@app/graphql/level-context" +import { useI18nContext } from "@app/i18n/i18n-react" +import { Text, makeStyles } from "@rneui/themed" + +import { Delete } from "./delete" +import { LogOut } from "./logout" + +export const DangerZoneSettings: React.FC = () => { + const { LL } = useI18nContext() + const styles = useStyles() + + const { currentLevel, isAtLeastLevelOne, isAtLeastLevelZero } = useLevel() + if (!isAtLeastLevelZero) return <> + + return ( + + + {LL.AccountScreen.dangerZone()} + + {isAtLeastLevelOne && } + {currentLevel !== AccountLevel.NonAuth && } + + ) +} + +const useStyles = makeStyles(() => ({ + verticalSpacing: { + marginTop: 10, + display: "flex", + flexDirection: "column", + rowGap: 10, + }, +})) diff --git a/app/screens/settings-screen/account/settings/delete.tsx b/app/screens/settings-screen/account/settings/delete.tsx new file mode 100644 index 0000000000..afd7dde522 --- /dev/null +++ b/app/screens/settings-screen/account/settings/delete.tsx @@ -0,0 +1,231 @@ +import { useState } from "react" +import { Alert, TextInput, View } from "react-native" +import Modal from "react-native-modal" + +import { gql } from "@apollo/client" +import { GaloyIconButton } from "@app/components/atomic/galoy-icon-button" +import { GaloyPrimaryButton } from "@app/components/atomic/galoy-primary-button" +import { GaloySecondaryButton } from "@app/components/atomic/galoy-secondary-button" +import { CONTACT_EMAIL_ADDRESS } from "@app/config" +import { useAccountDeleteMutation, useSettingsScreenQuery } from "@app/graphql/generated" +import { getBtcWallet, getUsdWallet } from "@app/graphql/wallets-utils" +import { useDisplayCurrency } from "@app/hooks/use-display-currency" +import useLogout from "@app/hooks/use-logout" +import { useI18nContext } from "@app/i18n/i18n-react" +import { RootStackParamList } from "@app/navigation/stack-param-lists" +import { toBtcMoneyAmount, toUsdMoneyAmount } from "@app/types/amounts" +import { useNavigation } from "@react-navigation/native" +import { StackNavigationProp } from "@react-navigation/stack" +import { useTheme, Text, makeStyles } from "@rneui/themed" + +import { SettingsButton } from "../../button" +import { useAccountDeleteContext } from "../account-delete-context" + +gql` + mutation accountDelete { + accountDelete { + errors { + message + } + success + } + } +` + +export const Delete = () => { + const styles = useStyles() + + const navigation = useNavigation>() + const { logout } = useLogout() + + const { LL } = useI18nContext() + + const [text, setText] = useState("") + const [modalVisible, setModalVisible] = useState(false) + const closeModal = () => { + setModalVisible(false) + setText("") + } + + const { + theme: { colors }, + } = useTheme() + + const { setAccountIsBeingDeleted } = useAccountDeleteContext() + + const [deleteAccount] = useAccountDeleteMutation({ fetchPolicy: "no-cache" }) + + const { data, loading } = useSettingsScreenQuery() + const { formatMoneyAmount } = useDisplayCurrency() + + const btcWallet = getBtcWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + + const usdWalletBalance = toUsdMoneyAmount(usdWallet?.balance) + const btcWalletBalance = toBtcMoneyAmount(btcWallet?.balance) + + let usdBalanceWarning = "" + let btcBalanceWarning = "" + let balancePositive = false + if (usdWalletBalance.amount > 0) { + const balance = + formatMoneyAmount && formatMoneyAmount({ moneyAmount: usdWalletBalance }) + usdBalanceWarning = LL.AccountScreen.usdBalanceWarning({ balance }) + balancePositive = true + } + + if (btcWalletBalance.amount > 0) { + const balance = + formatMoneyAmount && formatMoneyAmount({ moneyAmount: btcWalletBalance }) + btcBalanceWarning = LL.AccountScreen.btcBalanceWarning({ balance }) + balancePositive = true + } + + const fullMessage = ( + usdBalanceWarning + + "\n" + + btcBalanceWarning + + "\n" + + LL.support.deleteAccountBalanceWarning() + ).trim() + + const deleteAccountAction = async () => { + if (balancePositive) { + Alert.alert(LL.common.warning(), fullMessage, [ + { text: LL.common.cancel(), onPress: () => {} }, + { + text: LL.common.yes(), + onPress: async () => setModalVisible(true), + }, + ]) + } else { + setModalVisible(true) + } + } + + const deleteUserAccount = async () => { + try { + navigation.setOptions({ + headerLeft: () => null, // Hides the default back button + gestureEnabled: false, // Disables swipe to go back gesture + }) + setAccountIsBeingDeleted(true) + + const res = await deleteAccount() + + if (res.data?.accountDelete?.success) { + await logout(true) + setAccountIsBeingDeleted(false) + navigation.reset({ + index: 0, + routes: [{ name: "getStarted" }], + }) + Alert.alert(LL.support.bye(), LL.support.deleteAccountConfirmation(), [ + { + text: LL.common.ok(), + onPress: () => {}, + }, + ]) + } else { + Alert.alert( + LL.common.error(), + LL.support.deleteAccountError({ email: CONTACT_EMAIL_ADDRESS }) + + "\n\n" + + res.data?.accountDelete?.errors[0].message, + ) + } + } catch (err) { + console.error(err) + Alert.alert( + LL.common.error(), + LL.support.deleteAccountError({ email: CONTACT_EMAIL_ADDRESS }), + ) + } + } + + const userWroteDelete = + text.toLowerCase().trim() === LL.support.delete().toLocaleLowerCase().trim() + + const AccountDeletionModal = ( + + + + + {LL.support.deleteAccount()} + + + + {LL.support.typeDelete({ delete: LL.support.delete() })} + + + { + closeModal() + Alert.alert( + LL.support.finalConfirmationAccountDeletionTitle(), + LL.support.finalConfirmationAccountDeletionMessage(), + [ + { text: LL.common.cancel(), onPress: () => {} }, + { text: LL.common.ok(), onPress: () => deleteUserAccount() }, + ], + ) + }} + /> + + + + + ) + + return ( + <> + + {AccountDeletionModal} + + ) +} + +const useStyles = makeStyles(({ colors }) => ({ + view: { + marginHorizontal: 20, + backgroundColor: colors.grey5, + padding: 20, + borderRadius: 20, + display: "flex", + flexDirection: "column", + rowGap: 20, + }, + textInput: { + fontSize: 16, + backgroundColor: colors.grey4, + padding: 12, + color: colors.black, + borderRadius: 8, + }, + actionButtons: { + display: "flex", + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }, +})) diff --git a/app/screens/settings-screen/account/settings/email.tsx b/app/screens/settings-screen/account/settings/email.tsx new file mode 100644 index 0000000000..76861f44a5 --- /dev/null +++ b/app/screens/settings-screen/account/settings/email.tsx @@ -0,0 +1,185 @@ +import { Alert, View } from "react-native" + +import { gql } from "@apollo/client" +import { GaloyIconButton } from "@app/components/atomic/galoy-icon-button" +import { + useUserEmailDeleteMutation, + useUserEmailRegistrationInitiateMutation, +} from "@app/graphql/generated" +import { useI18nContext } from "@app/i18n/i18n-react" +import { TranslationFunctions } from "@app/i18n/i18n-types" +import { RootStackParamList } from "@app/navigation/stack-param-lists" +import { toastShow } from "@app/utils/toast" +import { useNavigation } from "@react-navigation/native" +import { StackNavigationProp } from "@react-navigation/stack" +import { makeStyles } from "@rneui/themed" + +import { SettingsRow } from "../../row" +import { useLoginMethods } from "../login-methods-hook" + +gql` + mutation userEmailDelete { + userEmailDelete { + errors { + message + } + me { + id + phone + totpEnabled + email { + address + verified + } + } + } + } + + mutation userEmailRegistrationInitiate($input: UserEmailRegistrationInitiateInput!) { + userEmailRegistrationInitiate(input: $input) { + errors { + message + } + emailRegistrationId + me { + id + email { + address + verified + } + } + } + } +` + +const title = ( + email: string | undefined, + emailVerified: boolean, + LL: TranslationFunctions, +): string => { + if (email) { + if (emailVerified) return LL.AccountScreen.email() + return LL.AccountScreen.unverifiedEmail() + } + return LL.AccountScreen.tapToAddEmail() +} + +export const EmailSetting: React.FC = () => { + const styles = useStyles() + + const { LL } = useI18nContext() + const { navigate } = useNavigation>() + + const { loading, email, emailVerified, bothEmailAndPhoneVerified } = useLoginMethods() + + const [emailDeleteMutation, { loading: emDelLoading }] = useUserEmailDeleteMutation() + const [setEmailMutation, { loading: emRegLoading }] = + useUserEmailRegistrationInitiateMutation() + + const deleteEmail = async () => { + try { + await emailDeleteMutation() + toastShow({ + type: "success", + message: LL.AccountScreen.emailDeletedSuccessfully(), + LL, + }) + } catch (err) { + Alert.alert(LL.common.error(), err instanceof Error ? err.message : "") + } + } + + const deleteEmailPrompt = async () => { + Alert.alert( + LL.AccountScreen.deleteEmailPromptTitle(), + LL.AccountScreen.deleteEmailPromptContent(), + [ + { text: LL.common.cancel(), onPress: () => {} }, + { + text: LL.common.yes(), + onPress: async () => { + deleteEmail() + }, + }, + ], + ) + } + + const tryConfirmEmailAgain = async (email: string) => { + try { + await emailDeleteMutation({ + // to avoid flacky behavior + // this could lead to inconsistent state if delete works but set fails + fetchPolicy: "no-cache", + }) + + const { data } = await setEmailMutation({ + variables: { input: { email } }, + }) + + const errors = data?.userEmailRegistrationInitiate.errors + if (errors && errors.length > 0) { + Alert.alert(errors[0].message) + } + + const emailRegistrationId = data?.userEmailRegistrationInitiate.emailRegistrationId + + if (emailRegistrationId) { + navigate("emailRegistrationValidate", { + emailRegistrationId, + email, + }) + } else { + console.warn("no flow returned") + } + } catch (err) { + console.error(err, "error in setEmailMutation") + } + } + + const reVerifyEmailPrompt = () => { + if (!email) return + Alert.alert( + LL.AccountScreen.emailUnverified(), + LL.AccountScreen.emailUnverifiedContent(), + [ + { text: LL.common.cancel(), onPress: () => {} }, + { + text: LL.common.ok(), + onPress: () => tryConfirmEmailAgain(email), + }, + ], + ) + } + + const RightIcon = email ? ( + + {!emailVerified && ( + + )} + {(bothEmailAndPhoneVerified || (email && !emailVerified)) && ( + + )} + + ) : undefined + + return ( + navigate("emailRegistrationInitiate")} + rightIcon={RightIcon} + /> + ) +} + +const useStyles = makeStyles(() => ({ + sidetoside: { + display: "flex", + flexDirection: "row", + columnGap: 10, + }, +})) diff --git a/app/screens/settings-screen/account/settings/logout.tsx b/app/screens/settings-screen/account/settings/logout.tsx new file mode 100644 index 0000000000..a3564d2a11 --- /dev/null +++ b/app/screens/settings-screen/account/settings/logout.tsx @@ -0,0 +1,68 @@ +import { Alert } from "react-native" + +import useLogout from "@app/hooks/use-logout" +import { useI18nContext } from "@app/i18n/i18n-react" +import { RootStackParamList } from "@app/navigation/stack-param-lists" +import { useNavigation } from "@react-navigation/native" +import { StackNavigationProp } from "@react-navigation/stack" + +import { SettingsButton } from "../../button" +import { useLoginMethods } from "../login-methods-hook" + +export const LogOut = () => { + const navigation = useNavigation>() + + const { phone, bothEmailAndPhoneVerified, email, emailVerified } = useLoginMethods() + const { LL } = useI18nContext() + + const { logout } = useLogout() + + const logoutAlert = () => { + const logAlertContent = () => { + if (phone && email && bothEmailAndPhoneVerified) { + return LL.AccountScreen.logoutAlertContentPhoneEmail({ + phoneNumber: phone, + email, + }) + } else if (email && emailVerified) { + return LL.AccountScreen.logoutAlertContentEmail({ email }) + } + // phone verified + if (phone) return LL.AccountScreen.logoutAlertContentPhone({ phoneNumber: phone }) + console.error("Phone and email both not verified - Impossible to reach") + } + + Alert.alert(LL.AccountScreen.logoutAlertTitle(), logAlertContent(), [ + { + text: LL.common.cancel(), + style: "cancel", + }, + { + text: LL.AccountScreen.IUnderstand(), + onPress: logoutAction, + }, + ]) + } + + const logoutAction = async () => { + await logout() + navigation.reset({ + index: 0, + routes: [{ name: "getStarted" }], + }) + Alert.alert(LL.common.loggedOut(), "", [ + { + text: LL.common.ok(), + onPress: () => {}, + }, + ]) + } + + return ( + + ) +} diff --git a/app/screens/settings-screen/account/settings/phone.tsx b/app/screens/settings-screen/account/settings/phone.tsx new file mode 100644 index 0000000000..706e5d098f --- /dev/null +++ b/app/screens/settings-screen/account/settings/phone.tsx @@ -0,0 +1,94 @@ +import { Alert } from "react-native" + +import { gql } from "@apollo/client" +import { GaloyIconButton } from "@app/components/atomic/galoy-icon-button" +import { useUserPhoneDeleteMutation } from "@app/graphql/generated" +import { useI18nContext } from "@app/i18n/i18n-react" +import { RootStackParamList } from "@app/navigation/stack-param-lists" +import { toastShow } from "@app/utils/toast" +import { useNavigation } from "@react-navigation/native" +import { StackNavigationProp } from "@react-navigation/stack" + +import { SettingsRow } from "../../row" +import { useLoginMethods } from "../login-methods-hook" + +gql` + mutation userPhoneDelete { + userPhoneDelete { + errors { + message + } + me { + id + phone + totpEnabled + email { + address + verified + } + } + } + } +` + +export const PhoneSetting: React.FC = () => { + const { LL } = useI18nContext() + const { navigate } = useNavigation>() + + const { loading, phone, emailVerified, phoneVerified } = useLoginMethods() + + const [phoneDeleteMutation, { loading: phoneDeleteLoading }] = + useUserPhoneDeleteMutation() + + const deletePhone = async () => { + try { + await phoneDeleteMutation() + toastShow({ + message: LL.AccountScreen.phoneDeletedSuccessfully(), + LL, + type: "success", + }) + } catch (err) { + Alert.alert(LL.common.error(), err instanceof Error ? err.message : "") + } + } + const deletePhonePrompt = async () => { + Alert.alert( + LL.AccountScreen.deletePhonePromptTitle(), + LL.AccountScreen.deletePhonePromptContent(), + [ + { text: LL.common.cancel(), onPress: () => {} }, + { + text: LL.common.yes(), + onPress: async () => { + deletePhone() + }, + }, + ], + ) + } + + return ( + navigate("phoneRegistrationInitiate")} + spinner={phoneDeleteLoading} + rightIcon={ + phoneVerified ? ( + emailVerified ? ( + + ) : null + ) : ( + "chevron-forward" + ) + } + /> + ) +} diff --git a/app/screens/settings-screen/account/settings/upgrade-trial-account.tsx b/app/screens/settings-screen/account/settings/upgrade-trial-account.tsx new file mode 100644 index 0000000000..4335e1f823 --- /dev/null +++ b/app/screens/settings-screen/account/settings/upgrade-trial-account.tsx @@ -0,0 +1,74 @@ +import { useState } from "react" +import { View } from "react-native" + +import { GaloyIcon } from "@app/components/atomic/galoy-icon" +import { GaloySecondaryButton } from "@app/components/atomic/galoy-secondary-button" +import { UpgradeAccountModal } from "@app/components/upgrade-account-modal" +import { AccountLevel, useLevel } from "@app/graphql/level-context" +import { useI18nContext } from "@app/i18n/i18n-react" +import { makeStyles, Text } from "@rneui/themed" + +import { useShowWarningSecureAccount } from "../show-warning-secure-account-hook" + +export const UpgradeTrialAccount: React.FC = () => { + const styles = useStyles() + const { currentLevel } = useLevel() + const { LL } = useI18nContext() + const hasBalance = useShowWarningSecureAccount() + + const [upgradeAccountModalVisible, setUpgradeAccountModalVisible] = useState(false) + const closeUpgradeAccountModal = () => setUpgradeAccountModalVisible(false) + const openUpgradeAccountModal = () => setUpgradeAccountModalVisible(true) + + if (currentLevel !== AccountLevel.Zero) return <> + + return ( + <> + + + + + {LL.common.trialAccount()} + + + + {LL.AccountScreen.itsATrialAccount()} + {hasBalance && ( + ⚠️ {LL.AccountScreen.fundsMoreThan5Dollars()} + )} + + + + ) +} + +const useStyles = makeStyles(({ colors }) => ({ + container: { + borderRadius: 20, + backgroundColor: colors.grey5, + padding: 16, + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "flex-start", + rowGap: 10, + }, + selfCenter: { alignSelf: "center" }, + sideBySide: { + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + width: "100%", + marginBottom: 4, + }, +})) diff --git a/app/screens/settings-screen/account/settings/upgrade.tsx b/app/screens/settings-screen/account/settings/upgrade.tsx new file mode 100644 index 0000000000..5639de44a1 --- /dev/null +++ b/app/screens/settings-screen/account/settings/upgrade.tsx @@ -0,0 +1,24 @@ +import { AccountLevel, useLevel } from "@app/graphql/level-context" +import { useI18nContext } from "@app/i18n/i18n-react" +import { RootStackParamList } from "@app/navigation/stack-param-lists" +import { useNavigation } from "@react-navigation/native" +import { StackNavigationProp } from "@react-navigation/stack" + +import { SettingsRow } from "../../row" + +export const UpgradeAccountLevelOne: React.FC = () => { + const { currentLevel } = useLevel() + const { LL } = useI18nContext() + + const { navigate } = useNavigation>() + + if (currentLevel !== AccountLevel.One) return <> + + return ( + navigate("fullOnboardingFlow")} + /> + ) +} diff --git a/app/screens/settings-screen/show-warning-secure-account.tsx b/app/screens/settings-screen/account/show-warning-secure-account-hook.ts similarity index 93% rename from app/screens/settings-screen/show-warning-secure-account.tsx rename to app/screens/settings-screen/account/show-warning-secure-account-hook.ts index 7dd9d05869..574954eb17 100644 --- a/app/screens/settings-screen/show-warning-secure-account.tsx +++ b/app/screens/settings-screen/account/show-warning-secure-account-hook.ts @@ -35,11 +35,11 @@ export const useShowWarningSecureAccount = () => { const isAuthed = useIsAuthed() const { data } = useWarningSecureAccountQuery({ - fetchPolicy: "cache-first", + fetchPolicy: "cache-and-network", skip: !isAuthed, }) - if (data?.me?.defaultAccount.level !== "ZERO") return false + if (data?.me?.defaultAccount?.level !== "ZERO") return false const btcWallet = getBtcWallet(data?.me?.defaultAccount?.wallets) const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) diff --git a/app/screens/settings-screen/button.tsx b/app/screens/settings-screen/button.tsx new file mode 100644 index 0000000000..8373709db8 --- /dev/null +++ b/app/screens/settings-screen/button.tsx @@ -0,0 +1,41 @@ +import { testProps } from "@app/utils/testProps" +import { Button, Skeleton, makeStyles } from "@rneui/themed" + +type Props = { + title: string + onPress: () => void + variant: "warning" | "danger" + loading?: boolean +} + +export const SettingsButton: React.FC = ({ title, onPress, variant, loading }) => { + const styles = useStyles(variant) + + if (loading) return + + return ( +