diff --git a/.storybook/storybook.requires.js b/.storybook/storybook.requires.js index a67f453309..7afdeccef9 100644 --- a/.storybook/storybook.requires.js +++ b/.storybook/storybook.requires.js @@ -46,10 +46,10 @@ const getStories = () => { "./app/components/atomic/galoy-secondary-button/galoy-secondary-button.stories.tsx": require("../app/components/atomic/galoy-secondary-button/galoy-secondary-button.stories.tsx"), "./app/components/atomic/galoy-tertiary-button/galoy-tertiary-button.stories.tsx": require("../app/components/atomic/galoy-tertiary-button/galoy-tertiary-button.stories.tsx"), "./app/components/balance-header/balance-header.stories.tsx": require("../app/components/balance-header/balance-header.stories.tsx"), + "./app/components/button-group/button-group.stories.tsx": require("../app/components/button-group/button-group.stories.tsx"), "./app/components/currency-keyboard/currency-keyboard.stories.tsx": require("../app/components/currency-keyboard/currency-keyboard.stories.tsx"), "./app/components/custom-modal/custom-modal.stories.tsx": require("../app/components/custom-modal/custom-modal.stories.tsx"), "./app/components/menu-select/menu-select.stories.tsx": require("../app/components/menu-select/menu-select.stories.tsx"), - "./app/components/button-group/button-group.stories.tsx": require("../app/components/button-group/button-group.stories.tsx"), "./app/components/modal-nfc/modal-nfc.stories.tsx": require("../app/components/modal-nfc/modal-nfc.stories.tsx"), "./app/components/set-default-account-modal/set-default-account-modal.stories.tsx": require("../app/components/set-default-account-modal/set-default-account-modal.stories.tsx"), "./app/components/set-lightning-address-modal/set-lightning-address-modal.stories.tsx": require("../app/components/set-lightning-address-modal/set-lightning-address-modal.stories.tsx"), @@ -78,10 +78,10 @@ const getStories = () => { "./app/screens/get-started-screen/device-account-fail-modal.stories.tsx": require("../app/screens/get-started-screen/device-account-fail-modal.stories.tsx"), "./app/screens/get-started-screen/get-started-screen.stories.tsx": require("../app/screens/get-started-screen/get-started-screen.stories.tsx"), "./app/screens/home-screen/home-screen.stories.tsx": require("../app/screens/home-screen/home-screen.stories.tsx"), - "./app/screens/phone-auth-screen/phone-flow.stories.tsx": require("../app/screens/phone-auth-screen/phone-flow.stories.tsx"), - "./app/screens/phone-auth-screen/phone-validation.stories.tsx": require("../app/screens/phone-auth-screen/phone-validation.stories.tsx"), - "./app/screens/receive-bitcoin-screen/receive-screen.stories.tsx": require("../app/screens/receive-bitcoin-screen/receive-screen.stories.tsx"), + "./app/screens/phone-auth-screen/phone-login-flow.stories.tsx": require("../app/screens/phone-auth-screen/phone-login-flow.stories.tsx"), + "./app/screens/phone-auth-screen/phone-login-validation.stories.tsx": require("../app/screens/phone-auth-screen/phone-login-validation.stories.tsx"), "./app/screens/receive-bitcoin-screen/qr-view.stories.tsx": require("../app/screens/receive-bitcoin-screen/qr-view.stories.tsx"), + "./app/screens/receive-bitcoin-screen/receive-screen.stories.tsx": require("../app/screens/receive-bitcoin-screen/receive-screen.stories.tsx"), "./app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-detail-screen.stories.tsx": require("../app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-detail-screen.stories.tsx"), "./app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-result-screen.stories.tsx": require("../app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-result-screen.stories.tsx"), "./app/screens/send-bitcoin-screen/confirm-destination-modal.stories.tsx": require("../app/screens/send-bitcoin-screen/confirm-destination-modal.stories.tsx"), diff --git a/app/graphql/generated.gql b/app/graphql/generated.gql index 2a3d6da8ce..4aee057453 100644 --- a/app/graphql/generated.gql +++ b/app/graphql/generated.gql @@ -405,6 +405,7 @@ mutation userEmailDelete { } me { id + phone email { address verified @@ -488,6 +489,39 @@ mutation userPhoneDelete { } me { id + phone + email { + address + verified + __typename + } + __typename + } + __typename + } +} + +mutation userPhoneRegistrationInitiate($input: UserPhoneRegistrationInitiateInput!) { + userPhoneRegistrationInitiate(input: $input) { + errors { + message + __typename + } + success + __typename + } +} + +mutation userPhoneRegistrationValidate($input: UserPhoneRegistrationValidateInput!) { + userPhoneRegistrationValidate(input: $input) { + errors { + message + code + __typename + } + me { + id + phone email { address verified diff --git a/app/graphql/generated.ts b/app/graphql/generated.ts index 9173a83426..9af11b7ac1 100644 --- a/app/graphql/generated.ts +++ b/app/graphql/generated.ts @@ -1895,6 +1895,13 @@ export type UserLoginUpgradeMutationVariables = Exact<{ export type UserLoginUpgradeMutation = { readonly __typename: 'Mutation', readonly userLoginUpgrade: { readonly __typename: 'UpgradePayload', readonly success: boolean, readonly authToken?: string | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string, readonly code?: string | null }> } }; +export type UserPhoneRegistrationValidateMutationVariables = Exact<{ + input: UserPhoneRegistrationValidateInput; +}>; + + +export type UserPhoneRegistrationValidateMutation = { readonly __typename: 'Mutation', readonly userPhoneRegistrationValidate: { readonly __typename: 'UserPhoneRegistrationValidatePayload', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string, readonly code?: string | null }>, readonly me?: { readonly __typename: 'User', readonly id: string, readonly phone?: string | null, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null } | null } }; + export type CaptchaRequestAuthCodeMutationVariables = Exact<{ input: CaptchaRequestAuthCodeInput; }>; @@ -1907,6 +1914,13 @@ export type SupportedCountriesQueryVariables = Exact<{ [key: string]: never; }>; export type SupportedCountriesQuery = { readonly __typename: 'Query', readonly globals?: { readonly __typename: 'Globals', readonly supportedCountries: ReadonlyArray<{ readonly __typename: 'Country', readonly id: string, readonly supportedAuthChannels: ReadonlyArray }> } | null }; +export type UserPhoneRegistrationInitiateMutationVariables = Exact<{ + input: UserPhoneRegistrationInitiateInput; +}>; + + +export type UserPhoneRegistrationInitiateMutation = { readonly __typename: 'Mutation', readonly userPhoneRegistrationInitiate: { readonly __typename: 'SuccessPayload', readonly success?: boolean | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }> } }; + export type MyLnUpdatesSubscriptionVariables = Exact<{ [key: string]: never; }>; @@ -2123,12 +2137,12 @@ export type AccountDeleteMutation = { readonly __typename: 'Mutation', readonly export type UserEmailDeleteMutationVariables = Exact<{ [key: string]: never; }>; -export type UserEmailDeleteMutation = { readonly __typename: 'Mutation', readonly userEmailDelete: { readonly __typename: 'UserEmailDeletePayload', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }>, readonly me?: { readonly __typename: 'User', readonly id: string, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null } | null } }; +export type UserEmailDeleteMutation = { readonly __typename: 'Mutation', readonly userEmailDelete: { readonly __typename: 'UserEmailDeletePayload', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }>, readonly me?: { readonly __typename: 'User', readonly id: string, readonly phone?: string | null, readonly email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null } | null } }; 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 email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null } | null } }; +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 email?: { readonly __typename: 'Email', readonly address?: string | null, readonly verified?: boolean | null } | null } | null } }; export type AccountUpdateDefaultWalletIdMutationVariables = Exact<{ input: AccountUpdateDefaultWalletIdInput; @@ -3626,6 +3640,50 @@ export function useUserLoginUpgradeMutation(baseOptions?: Apollo.MutationHookOpt export type UserLoginUpgradeMutationHookResult = ReturnType; export type UserLoginUpgradeMutationResult = Apollo.MutationResult; export type UserLoginUpgradeMutationOptions = Apollo.BaseMutationOptions; +export const UserPhoneRegistrationValidateDocument = gql` + mutation userPhoneRegistrationValidate($input: UserPhoneRegistrationValidateInput!) { + userPhoneRegistrationValidate(input: $input) { + errors { + message + code + } + me { + id + phone + email { + address + verified + } + } + } +} + `; +export type UserPhoneRegistrationValidateMutationFn = Apollo.MutationFunction; + +/** + * __useUserPhoneRegistrationValidateMutation__ + * + * To run a mutation, you first call `useUserPhoneRegistrationValidateMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUserPhoneRegistrationValidateMutation` 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 [userPhoneRegistrationValidateMutation, { data, loading, error }] = useUserPhoneRegistrationValidateMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useUserPhoneRegistrationValidateMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UserPhoneRegistrationValidateDocument, options); + } +export type UserPhoneRegistrationValidateMutationHookResult = ReturnType; +export type UserPhoneRegistrationValidateMutationResult = Apollo.MutationResult; +export type UserPhoneRegistrationValidateMutationOptions = Apollo.BaseMutationOptions; export const CaptchaRequestAuthCodeDocument = gql` mutation captchaRequestAuthCode($input: CaptchaRequestAuthCodeInput!) { captchaRequestAuthCode(input: $input) { @@ -3700,6 +3758,42 @@ export function useSupportedCountriesLazyQuery(baseOptions?: Apollo.LazyQueryHoo export type SupportedCountriesQueryHookResult = ReturnType; export type SupportedCountriesLazyQueryHookResult = ReturnType; export type SupportedCountriesQueryResult = Apollo.QueryResult; +export const UserPhoneRegistrationInitiateDocument = gql` + mutation userPhoneRegistrationInitiate($input: UserPhoneRegistrationInitiateInput!) { + userPhoneRegistrationInitiate(input: $input) { + errors { + message + } + success + } +} + `; +export type UserPhoneRegistrationInitiateMutationFn = Apollo.MutationFunction; + +/** + * __useUserPhoneRegistrationInitiateMutation__ + * + * To run a mutation, you first call `useUserPhoneRegistrationInitiateMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUserPhoneRegistrationInitiateMutation` 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 [userPhoneRegistrationInitiateMutation, { data, loading, error }] = useUserPhoneRegistrationInitiateMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useUserPhoneRegistrationInitiateMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UserPhoneRegistrationInitiateDocument, options); + } +export type UserPhoneRegistrationInitiateMutationHookResult = ReturnType; +export type UserPhoneRegistrationInitiateMutationResult = Apollo.MutationResult; +export type UserPhoneRegistrationInitiateMutationOptions = Apollo.BaseMutationOptions; export const MyLnUpdatesDocument = gql` subscription myLnUpdates { myUpdates { @@ -4982,6 +5076,7 @@ export const UserEmailDeleteDocument = gql` } me { id + phone email { address verified @@ -5023,6 +5118,7 @@ export const UserPhoneDeleteDocument = gql` } me { id + phone email { address verified diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index 6cee7831a7..77a32e9269 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -871,6 +871,21 @@ const en: BaseTranslation = { sendViaOtherChannel: "You selected to receive the code via {channel: string}. You can try receiving via {other: string} instead", }, + PhoneRegistrationInitiateScreen: { + title: "Phone set up", + header: "Enter your phone number, and we'll text you an access code.", + headerVerify: "Verify you are human", + errorRequestingCode: "Something went wrong requesting the phone code, please try again later.", + errorInvalidPhoneNumber: "Invalid phone number. Are you sure you entered the right number?", + errorUnsupportedCountry: "We are unable to support customers in your country.", + placeholder: "Phone Number", + verify: "Click to Verify", + sms: "Send via SMS", + whatsapp: "Send via WhatsApp", + }, + PhoneRegistrationValidateScreen: { + successTitle: "Phone number confirmed", + }, EmailRegistrationInitiateScreen: { title: "Add your email", header: "Enter your email address, and we'll send you an access code.", diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index 740a8c08f5..bf130b1137 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -2738,6 +2738,54 @@ type RootTranslation = { */ sendViaOtherChannel: RequiredParams<'channel' | 'other'> } + PhoneRegistrationInitiateScreen: { + /** + * P​h​o​n​e​ ​s​e​t​ ​u​p + */ + title: string + /** + * E​n​t​e​r​ ​y​o​u​r​ ​p​h​o​n​e​ ​n​u​m​b​e​r​,​ ​a​n​d​ ​w​e​'​l​l​ ​t​e​x​t​ ​y​o​u​ ​a​n​ ​a​c​c​e​s​s​ ​c​o​d​e​. + */ + header: string + /** + * V​e​r​i​f​y​ ​y​o​u​ ​a​r​e​ ​h​u​m​a​n + */ + headerVerify: string + /** + * S​o​m​e​t​h​i​n​g​ ​w​e​n​t​ ​w​r​o​n​g​ ​r​e​q​u​e​s​t​i​n​g​ ​t​h​e​ ​p​h​o​n​e​ ​c​o​d​e​,​ ​p​l​e​a​s​e​ ​t​r​y​ ​a​g​a​i​n​ ​l​a​t​e​r​. + */ + errorRequestingCode: string + /** + * I​n​v​a​l​i​d​ ​p​h​o​n​e​ ​n​u​m​b​e​r​.​ ​A​r​e​ ​y​o​u​ ​s​u​r​e​ ​y​o​u​ ​e​n​t​e​r​e​d​ ​t​h​e​ ​r​i​g​h​t​ ​n​u​m​b​e​r​? + */ + errorInvalidPhoneNumber: string + /** + * W​e​ ​a​r​e​ ​u​n​a​b​l​e​ ​t​o​ ​s​u​p​p​o​r​t​ ​c​u​s​t​o​m​e​r​s​ ​i​n​ ​y​o​u​r​ ​c​o​u​n​t​r​y​. + */ + errorUnsupportedCountry: string + /** + * P​h​o​n​e​ ​N​u​m​b​e​r + */ + placeholder: string + /** + * C​l​i​c​k​ ​t​o​ ​V​e​r​i​f​y + */ + verify: string + /** + * S​e​n​d​ ​v​i​a​ ​S​M​S + */ + sms: string + /** + * S​e​n​d​ ​v​i​a​ ​W​h​a​t​s​A​p​p + */ + whatsapp: string + } + PhoneRegistrationValidateScreen: { + /** + * P​h​o​n​e​ ​n​u​m​b​e​r​ ​c​o​n​f​i​r​m​e​d + */ + successTitle: string + } EmailRegistrationInitiateScreen: { /** * A​d​d​ ​y​o​u​r​ ​e​m​a​i​l @@ -6061,6 +6109,54 @@ export type TranslationFunctions = { */ sendViaOtherChannel: (arg: { channel: string, other: string }) => LocalizedString } + PhoneRegistrationInitiateScreen: { + /** + * Phone set up + */ + title: () => LocalizedString + /** + * Enter your phone number, and we'll text you an access code. + */ + header: () => LocalizedString + /** + * Verify you are human + */ + headerVerify: () => LocalizedString + /** + * Something went wrong requesting the phone code, please try again later. + */ + errorRequestingCode: () => LocalizedString + /** + * Invalid phone number. Are you sure you entered the right number? + */ + errorInvalidPhoneNumber: () => LocalizedString + /** + * We are unable to support customers in your country. + */ + errorUnsupportedCountry: () => LocalizedString + /** + * Phone Number + */ + placeholder: () => LocalizedString + /** + * Click to Verify + */ + verify: () => LocalizedString + /** + * Send via SMS + */ + sms: () => LocalizedString + /** + * Send via WhatsApp + */ + whatsapp: () => LocalizedString + } + PhoneRegistrationValidateScreen: { + /** + * Phone number confirmed + */ + successTitle: () => LocalizedString + } EmailRegistrationInitiateScreen: { /** * Add your email diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index 0d451f0560..9c2619fd91 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -795,6 +795,21 @@ "tryAgain": "Try Again", "sendViaOtherChannel": "You selected to receive the code via {channel: string}. You can try receiving via {other: string} instead" }, + "PhoneRegistrationInitiateScreen": { + "title": "Phone set up", + "header": "Enter your phone number, and we'll text you an access code.", + "headerVerify": "Verify you are human", + "errorRequestingCode": "Something went wrong requesting the phone code, please try again later.", + "errorInvalidPhoneNumber": "Invalid phone number. Are you sure you entered the right number?", + "errorUnsupportedCountry": "We are unable to support customers in your country.", + "placeholder": "Phone Number", + "verify": "Click to Verify", + "sms": "Send via SMS", + "whatsapp": "Send via WhatsApp" + }, + "PhoneRegistrationValidationScreen": { + "successTitle": "Phone number confirmed" + }, "EmailRegistrationInitiateScreen": { "title": "Add your email", "header": "Enter your email address, and we'll send you an access code.", diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx index c6a99bc620..61eda5d66d 100644 --- a/app/navigation/root-navigator.tsx +++ b/app/navigation/root-navigator.tsx @@ -68,6 +68,8 @@ import { PrimaryStackParamList, RootStackParamList, } from "./stack-param-lists" +import { PhoneRegistrationInitiateScreen } from "@app/screens/phone-auth-screen/phone-registration-input" +import { PhoneRegistrationValidateScreen } from "@app/screens/phone-auth-screen/phone-registration-validation" const useStyles = makeStyles(({ colors }) => ({ bottomNavigatorStyle: { @@ -288,6 +290,22 @@ export const RootStack = () => { title: LL.PhoneLoginSetScreen.title(), }} /> + + { return ( } phoneFlow: undefined + phoneRegistrationInitiate: undefined + phoneRegistrationValidate: { phone: string; channel: PhoneCodeChannelType } transactionDetail: { txid: string } transactionHistory?: undefined Earn: undefined @@ -93,7 +95,7 @@ export type ContactStackParamList = { export type PhoneValidationStackParamList = { Primary: undefined - phoneLoginSet: undefined + phoneLoginInitiate: undefined phoneLoginValidate: { phone: string; channel: PhoneCodeChannelType } authentication: { screenPurpose: AuthenticationScreenPurpose diff --git a/app/screens/phone-auth-screen/phone-login-input.tsx b/app/screens/phone-auth-screen/phone-login-input.tsx index e6d0a3fe82..f1d909954c 100644 --- a/app/screens/phone-auth-screen/phone-login-input.tsx +++ b/app/screens/phone-auth-screen/phone-login-input.tsx @@ -17,13 +17,12 @@ import { ContactSupportButton } from "@app/components/contact-support-button/con import { useI18nContext } from "@app/i18n/i18n-react" import { makeStyles, useTheme, Text, Input } from "@rneui/themed" import { Screen } from "../../components/screen" -import { useAppConfig } from "../../hooks" import type { PhoneValidationStackParamList } from "../../navigation/stack-param-lists" import { ErrorType, RequestPhoneCodeStatus, - useRequestPhoneCode, -} from "./useRequestPhoneCode" + useRequestPhoneCodeLogin, +} from "./request-phone-code-login" import { GaloyPrimaryButton } from "@app/components/atomic/galoy-primary-button" import { GaloySecondaryButton } from "@app/components/atomic/galoy-secondary-button" import { GaloyErrorBox } from "@app/components/atomic/galoy-error-box" @@ -103,8 +102,10 @@ export const PhoneLoginSetScreen: React.FC = () => { const styles = useStyles() const navigation = - useNavigation>() - const { appConfig } = useAppConfig() + useNavigation< + StackNavigationProp + >() + const { theme: { colors, mode: themeMode }, } = useTheme() @@ -124,9 +125,7 @@ export const PhoneLoginSetScreen: React.FC = () => { setCountryCode, supportedCountries, loadingSupportedCountries, - } = useRequestPhoneCode({ - skipRequestPhoneCode: appConfig.galoyInstance.name === "Local", - }) + } = useRequestPhoneCodeLogin() const { LL } = useI18nContext() @@ -150,8 +149,6 @@ export const PhoneLoginSetScreen: React.FC = () => { ) } - const showCaptcha = false - let errorMessage: string | undefined if (error) { switch (error) { @@ -225,11 +222,7 @@ export const PhoneLoginSetScreen: React.FC = () => { > - - {showCaptcha - ? LL.PhoneLoginSetScreen.headerVerify() - : LL.PhoneLoginSetScreen.header()} - + {LL.PhoneLoginSetScreen.header()} diff --git a/app/screens/phone-auth-screen/phone-login-validation.stories.tsx b/app/screens/phone-auth-screen/phone-login-validation.stories.tsx index 739242adf8..c47a648fc8 100644 --- a/app/screens/phone-auth-screen/phone-login-validation.stories.tsx +++ b/app/screens/phone-auth-screen/phone-login-validation.stories.tsx @@ -3,7 +3,7 @@ import { Meta } from "@storybook/react" import { MockedProvider } from "@apollo/client/testing" import { createCache } from "../../graphql/cache" import { StoryScreen } from "../../../.storybook/views" -import { PhoneLoginValidationScreen } from "./phone-registration-validation" +import { PhoneLoginValidationScreen } from "./phone-login-validation" const mocks = [] diff --git a/app/screens/phone-auth-screen/phone-login-validation.tsx b/app/screens/phone-auth-screen/phone-login-validation.tsx index a078e95a27..4be14f7977 100644 --- a/app/screens/phone-auth-screen/phone-login-validation.tsx +++ b/app/screens/phone-auth-screen/phone-login-validation.tsx @@ -26,63 +26,9 @@ import { logUpgradeLoginSuccess, logValidateAuthCodeFailure, } from "@app/utils/analytics" -import { PhoneCodeChannelToFriendlyName } from "./useRequestPhoneCode" +import { PhoneCodeChannelToFriendlyName } from "./request-phone-code-login" import { AccountLevel, useLevel } from "@app/graphql/level-context" -const useStyles = makeStyles(({ colors }) => ({ - screenStyle: { - padding: 20, - flexGrow: 1, - }, - flex: { flex: 1 }, - flexAndMinHeight: { flex: 1, minHeight: 16 }, - viewWrapper: { flex: 1 }, - - activityIndicator: { marginTop: 12 }, - extraInfoContainer: { - marginBottom: 20, - flex: 1, - }, - sendAgainButtonRow: { - flexDirection: "row", - justifyContent: "center", - paddingHorizontal: 25, - textAlign: "center", - }, - textContainer: { - marginBottom: 20, - }, - timerRow: { - flexDirection: "row", - justifyContent: "center", - textAlign: "center", - }, - marginBottom: { - marginBottom: 10, - }, - inputComponentContainerStyle: { - flexDirection: "row", - marginBottom: 20, - paddingLeft: 0, - paddingRight: 0, - justifyContent: "center", - }, - inputContainerStyle: { - minWidth: 160, - minHeight: 60, - borderWidth: 2, - borderBottomWidth: 2, - paddingHorizontal: 10, - borderColor: colors.primary5, - borderRadius: 8, - marginRight: 0, - }, - inputStyle: { - fontSize: 24, - textAlign: "center", - }, -})) - gql` mutation userLogin($input: UserLoginInput!) { userLogin(input: $input) { @@ -402,3 +348,57 @@ export const PhoneLoginValidationScreen: React.FC ) } + +const useStyles = makeStyles(({ colors }) => ({ + screenStyle: { + padding: 20, + flexGrow: 1, + }, + flex: { flex: 1 }, + flexAndMinHeight: { flex: 1, minHeight: 16 }, + viewWrapper: { flex: 1 }, + + activityIndicator: { marginTop: 12 }, + extraInfoContainer: { + marginBottom: 20, + flex: 1, + }, + sendAgainButtonRow: { + flexDirection: "row", + justifyContent: "center", + paddingHorizontal: 25, + textAlign: "center", + }, + textContainer: { + marginBottom: 20, + }, + timerRow: { + flexDirection: "row", + justifyContent: "center", + textAlign: "center", + }, + marginBottom: { + marginBottom: 10, + }, + inputComponentContainerStyle: { + flexDirection: "row", + marginBottom: 20, + paddingLeft: 0, + paddingRight: 0, + justifyContent: "center", + }, + inputContainerStyle: { + minWidth: 160, + minHeight: 60, + borderWidth: 2, + borderBottomWidth: 2, + paddingHorizontal: 10, + borderColor: colors.primary5, + borderRadius: 8, + marginRight: 0, + }, + inputStyle: { + fontSize: 24, + textAlign: "center", + }, +})) diff --git a/app/screens/phone-auth-screen/phone-registration-input.tsx b/app/screens/phone-auth-screen/phone-registration-input.tsx new file mode 100644 index 0000000000..fe327f418a --- /dev/null +++ b/app/screens/phone-auth-screen/phone-registration-input.tsx @@ -0,0 +1,270 @@ +import * as React from "react" +import { ActivityIndicator, View } from "react-native" +import CountryPicker, { + CountryCode, + DARK_THEME, + DEFAULT_THEME, + Flag, +} from "react-native-country-picker-modal" +import { + CountryCode as PhoneNumberCountryCode, + getCountryCallingCode, +} from "libphonenumber-js/mobile" +import { ContactSupportButton } from "@app/components/contact-support-button/contact-support-button" +import { useI18nContext } from "@app/i18n/i18n-react" +import { makeStyles, useTheme, Text, Input } from "@rneui/themed" +import { Screen } from "../../components/screen" +import { + ErrorType, + RequestPhoneCodeStatus, + useRequestPhoneCodeRegistration, +} from "./request-phone-code-registration" +import { GaloyPrimaryButton } from "@app/components/atomic/galoy-primary-button" +import { GaloySecondaryButton } from "@app/components/atomic/galoy-secondary-button" +import { GaloyErrorBox } from "@app/components/atomic/galoy-error-box" +import { PhoneCodeChannelType } from "@app/graphql/generated" +import { TouchableOpacity } from "react-native-gesture-handler" + +const DEFAULT_COUNTRY_CODE = "SV" +const PLACEHOLDER_PHONE_NUMBER = "123-456-7890" + +export const PhoneRegistrationInitiateScreen: React.FC = () => { + const styles = useStyles() + + const { + theme: { colors, mode: themeMode }, + } = useTheme() + + const { + submitPhoneNumber, + status, + setPhoneNumber, + isSmsSupported, + isWhatsAppSupported, + phoneInputInfo, + phoneCodeChannel, + error, + setCountryCode, + supportedCountries, + } = useRequestPhoneCodeRegistration() + + const { LL } = useI18nContext() + + if (status === RequestPhoneCodeStatus.LoadingCountryCode) { + return ( + + + + + + ) + } + + let errorMessage: string | undefined + if (error) { + switch (error) { + case ErrorType.RequestCodeError: + errorMessage = LL.PhoneRegistrationInitiateScreen.errorRequestingCode() + break + case ErrorType.TooManyAttemptsError: + errorMessage = LL.errors.tooManyRequestsPhoneCode() + break + case ErrorType.InvalidPhoneNumberError: + errorMessage = LL.PhoneRegistrationInitiateScreen.errorInvalidPhoneNumber() + break + case ErrorType.UnsupportedCountryError: + errorMessage = LL.PhoneRegistrationInitiateScreen.errorUnsupportedCountry() + break + } + } + if (!isSmsSupported && !isWhatsAppSupported) { + errorMessage = LL.PhoneRegistrationInitiateScreen.errorUnsupportedCountry() + } + + let PrimaryButton = undefined + let SecondaryButton = undefined + switch (true) { + case isSmsSupported && isWhatsAppSupported: + PrimaryButton = ( + submitPhoneNumber(PhoneCodeChannelType.Sms)} + /> + ) + SecondaryButton = ( + submitPhoneNumber(PhoneCodeChannelType.Whatsapp)} + /> + ) + break + case isSmsSupported && !isWhatsAppSupported: + PrimaryButton = ( + submitPhoneNumber(PhoneCodeChannelType.Sms)} + /> + ) + break + case !isSmsSupported && isWhatsAppSupported: + PrimaryButton = ( + submitPhoneNumber(PhoneCodeChannelType.Whatsapp)} + /> + ) + break + } + + return ( + + + + {LL.PhoneRegistrationInitiateScreen.header()} + + + + setCountryCode(country.cca2 as PhoneNumberCountryCode)} + renderFlagButton={({ countryCode, onOpen }) => { + return ( + countryCode && ( + + + + +{getCountryCallingCode(countryCode as PhoneNumberCountryCode)} + + + ) + ) + }} + withCallingCodeButton={true} + withFilter={true} + filterProps={{ + autoFocus: true, + }} + withCallingCode={true} + /> + + + {errorMessage && ( + + + + + )} + + + {SecondaryButton} + {PrimaryButton} + + + + ) +} + +const useStyles = makeStyles(({ colors }) => ({ + screenStyle: { + padding: 20, + flexGrow: 1, + }, + buttonsContainer: { + flex: 1, + justifyContent: "flex-end", + }, + + inputContainer: { + marginBottom: 20, + flexDirection: "row", + alignItems: "stretch", + minHeight: 48, + }, + textContainer: { + marginBottom: 20, + }, + viewWrapper: { flex: 1 }, + + activityIndicator: { marginTop: 12 }, + + keyboardContainer: { + paddingHorizontal: 10, + }, + + codeTextStyle: {}, + countryPickerButtonStyle: { + minWidth: 110, + borderColor: colors.primary5, + borderWidth: 2, + borderRadius: 8, + paddingHorizontal: 10, + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + flex: 1, + }, + inputComponentContainerStyle: { + flex: 1, + marginLeft: 20, + paddingLeft: 0, + paddingRight: 0, + }, + inputContainerStyle: { + flex: 1, + borderWidth: 2, + borderBottomWidth: 2, + paddingHorizontal: 10, + borderColor: colors.primary5, + borderRadius: 8, + }, + errorContainer: { + marginBottom: 20, + }, + whatsAppButton: { + marginBottom: 20, + }, + contactSupportButton: { + marginTop: 10, + }, + + loadingView: { flex: 1, justifyContent: "center", alignItems: "center" }, +})) diff --git a/app/screens/phone-auth-screen/phone-registration-validation.tsx b/app/screens/phone-auth-screen/phone-registration-validation.tsx new file mode 100644 index 0000000000..f7b8fd2c6b --- /dev/null +++ b/app/screens/phone-auth-screen/phone-registration-validation.tsx @@ -0,0 +1,335 @@ +import { gql } from "@apollo/client" +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" +import { useI18nContext } from "@app/i18n/i18n-react" +import { TranslationFunctions } from "@app/i18n/i18n-types" +import { logAddPhoneAttempt, logValidateAuthCodeFailure } from "@app/utils/analytics" +import crashlytics from "@react-native-firebase/crashlytics" +import { RouteProp, useNavigation } from "@react-navigation/native" +import { StackNavigationProp } from "@react-navigation/stack" +import { Input, Text, makeStyles, useTheme } from "@rneui/themed" +import * as React from "react" +import { useCallback, useEffect, useState } from "react" +import { ActivityIndicator, Alert, View } from "react-native" +import { Screen } from "../../components/screen" +import type { RootStackParamList } from "../../navigation/stack-param-lists" +import { parseTimer } from "../../utils/timer" +import { PhoneCodeChannelToFriendlyName } from "./request-phone-code-login" + +gql` + mutation userPhoneRegistrationValidate($input: UserPhoneRegistrationValidateInput!) { + userPhoneRegistrationValidate(input: $input) { + errors { + message + code + } + me { + id + phone + email { + address + verified + } + } + } + } +` + +type PhoneRegistrationValidateScreenProps = { + route: RouteProp +} + +const ValidatePhoneCodeStatus = { + WaitingForCode: "WaitingForCode", + LoadingAuthResult: "LoadingAuthResult", + ReadyToRegenerate: "ReadyToRegenerate", + Success: "Success", +} + +type ValidatePhoneCodeStatusType = + (typeof ValidatePhoneCodeStatus)[keyof typeof ValidatePhoneCodeStatus] + +const ValidatePhoneCodeErrors = { + InvalidCode: "InvalidCode", + TooManyAttempts: "TooManyAttempts", + UnknownError: "UnknownError", +} as const + +const mapGqlErrorsToValidatePhoneCodeErrors = ( + errors: readonly { code?: string | null | undefined }[], +): ValidatePhoneCodeErrorsType | undefined => { + if (errors.some((error) => error.code === "PHONE_CODE_ERROR")) { + return ValidatePhoneCodeErrors.InvalidCode + } + + if (errors.some((error) => error.code === "TOO_MANY_REQUEST")) { + return ValidatePhoneCodeErrors.TooManyAttempts + } + + if (errors.length > 0) { + return ValidatePhoneCodeErrors.UnknownError + } + + return undefined +} + +const mapValidatePhoneCodeErrorsToMessage = ( + error: ValidatePhoneCodeErrorsType, + LL: TranslationFunctions, +): string => { + switch (error) { + case ValidatePhoneCodeErrors.InvalidCode: + return LL.PhoneLoginValidationScreen.errorLoggingIn() + case ValidatePhoneCodeErrors.TooManyAttempts: + return LL.PhoneLoginValidationScreen.errorTooManyAttempts() + case ValidatePhoneCodeErrors.UnknownError: + default: + return LL.errors.generic() + } +} + +export type ValidatePhoneCodeErrorsType = + (typeof ValidatePhoneCodeErrors)[keyof typeof ValidatePhoneCodeErrors] + +export const PhoneRegistrationValidateScreen: React.FC< + PhoneRegistrationValidateScreenProps +> = ({ route }) => { + const styles = useStyles() + const navigation = + useNavigation>() + + const [status, setStatus] = useState( + ValidatePhoneCodeStatus.WaitingForCode, + ) + const [error, setError] = useState() + + const { LL } = useI18nContext() + + const [phoneValidate] = useUserPhoneRegistrationValidateMutation() + + const [code, _setCode] = useState("") + const [secondsRemaining, setSecondsRemaining] = useState(30) + const { phone, channel } = route.params + + const { + theme: { colors }, + } = useTheme() + + const send = useCallback( + async (code: string) => { + if (status === ValidatePhoneCodeStatus.LoadingAuthResult) { + return + } + + try { + setStatus(ValidatePhoneCodeStatus.LoadingAuthResult) + logAddPhoneAttempt() + const { data } = await phoneValidate({ + variables: { input: { phone, code } }, + refetchQueries: [AccountScreenDocument], + }) + + const errors = data?.userPhoneRegistrationValidate?.errors || [] + + const error = mapGqlErrorsToValidatePhoneCodeErrors(errors) + + if (error) { + console.error(error, "error validating phone code") + logValidateAuthCodeFailure({ + error, + }) + + setError(error) + _setCode("") + setStatus(ValidatePhoneCodeStatus.ReadyToRegenerate) + } else { + setStatus(ValidatePhoneCodeStatus.Success) + Alert.alert(LL.PhoneRegistrationValidateScreen.successTitle(), undefined, [ + { + text: LL.common.ok(), + onPress: () => navigation.pop(2), + }, + ]) + } + } catch (err) { + if (err instanceof Error) { + crashlytics().recordError(err) + console.debug({ err }) + } + setError(ValidatePhoneCodeErrors.UnknownError) + _setCode("") + setStatus(ValidatePhoneCodeStatus.ReadyToRegenerate) + } + }, + [status, phoneValidate, phone, _setCode, navigation, LL], + ) + + const setCode = (code: string) => { + if (code.length > 6) { + return + } + + setError(undefined) + _setCode(code) + if (code.length === 6) { + send(code) + } + } + + useEffect(() => { + const timerId = setTimeout(() => { + if (secondsRemaining > 0) { + setSecondsRemaining(secondsRemaining - 1) + } else if (status === ValidatePhoneCodeStatus.WaitingForCode) { + setStatus(ValidatePhoneCodeStatus.ReadyToRegenerate) + } + }, 1000) + return () => clearTimeout(timerId) + }, [secondsRemaining, status]) + + const errorMessage = error && mapValidatePhoneCodeErrorsToMessage(error, LL) + let extraInfoContent = undefined + switch (status) { + case ValidatePhoneCodeStatus.ReadyToRegenerate: + extraInfoContent = ( + <> + {errorMessage && ( + + + + )} + + + {LL.PhoneLoginValidationScreen.sendViaOtherChannel({ + channel: PhoneCodeChannelToFriendlyName[channel], + other: + PhoneCodeChannelToFriendlyName[ + channel === PhoneCodeChannelType.Sms + ? PhoneCodeChannelType.Whatsapp + : PhoneCodeChannelType.Sms + ], + })} + + + navigation.goBack()} + /> + + ) + break + case ValidatePhoneCodeStatus.LoadingAuthResult: + extraInfoContent = ( + + ) + break + case ValidatePhoneCodeStatus.WaitingForCode: + extraInfoContent = ( + + + {LL.PhoneLoginValidationScreen.sendAgain()} {parseTimer(secondsRemaining)} + + + ) + break + } + + return ( + + + + + {LL.PhoneLoginValidationScreen.header({ + channel: PhoneCodeChannelToFriendlyName[channel], + phoneNumber: phone, + })} + + + + + + {extraInfoContent} + + + ) +} + +const useStyles = makeStyles(({ colors }) => ({ + screenStyle: { + padding: 20, + flexGrow: 1, + }, + flex: { flex: 1 }, + flexAndMinHeight: { flex: 1, minHeight: 16 }, + viewWrapper: { flex: 1 }, + + activityIndicator: { marginTop: 12 }, + extraInfoContainer: { + marginBottom: 20, + flex: 1, + }, + sendAgainButtonRow: { + flexDirection: "row", + justifyContent: "center", + paddingHorizontal: 25, + textAlign: "center", + }, + textContainer: { + marginBottom: 20, + }, + timerRow: { + flexDirection: "row", + justifyContent: "center", + textAlign: "center", + }, + marginBottom: { + marginBottom: 10, + }, + inputComponentContainerStyle: { + flexDirection: "row", + marginBottom: 20, + paddingLeft: 0, + paddingRight: 0, + justifyContent: "center", + }, + inputContainerStyle: { + minWidth: 160, + minHeight: 60, + borderWidth: 2, + borderBottomWidth: 2, + paddingHorizontal: 10, + borderColor: colors.primary5, + borderRadius: 8, + marginRight: 0, + }, + inputStyle: { + fontSize: 24, + textAlign: "center", + }, +})) diff --git a/app/screens/phone-auth-screen/useRequestPhoneCode.ts b/app/screens/phone-auth-screen/request-phone-code-login.ts similarity index 96% rename from app/screens/phone-auth-screen/useRequestPhoneCode.ts rename to app/screens/phone-auth-screen/request-phone-code-login.ts index e8cdec2545..cd5e0ee323 100644 --- a/app/screens/phone-auth-screen/useRequestPhoneCode.ts +++ b/app/screens/phone-auth-screen/request-phone-code-login.ts @@ -44,12 +44,8 @@ type PhoneInputInfo = { rawPhoneNumber: string } -export type UseRequestPhoneCodeProps = { - skipRequestPhoneCode?: boolean -} - export type UseRequestPhoneCodeReturn = { - submitPhoneNumber: (phoneCodeChannel?: PhoneCodeChannelType) => void + submitPhoneNumber: (phoneCodeChannel: PhoneCodeChannelType) => void setStatus: (status: RequestPhoneCodeStatus) => void status: RequestPhoneCodeStatus phoneInputInfo?: PhoneInputInfo @@ -80,9 +76,7 @@ gql` success } } -` -gql` query supportedCountries { globals { supportedCountries { @@ -93,9 +87,7 @@ gql` } ` -export const useRequestPhoneCode = ({ - skipRequestPhoneCode, -}: UseRequestPhoneCodeProps): UseRequestPhoneCodeReturn => { +export const useRequestPhoneCodeLogin = (): UseRequestPhoneCodeReturn => { const [status, setStatus] = useState( RequestPhoneCodeStatus.LoadingCountryCode, ) @@ -106,6 +98,7 @@ export const useRequestPhoneCode = ({ PhoneCodeChannelType.Sms, ) const { appConfig } = useAppConfig() + const skipRequestPhoneCode = appConfig.galoyInstance.name === "Local" const [error, setError] = useState() const [captchaRequestAuthCode] = useCaptchaRequestAuthCodeMutation() @@ -188,7 +181,7 @@ export const useRequestPhoneCode = ({ setStatus(RequestPhoneCodeStatus.InputtingPhoneNumber) } - const submitPhoneNumber = (phoneCodeChannel?: PhoneCodeChannelType) => { + const submitPhoneNumber = (phoneCodeChannel: PhoneCodeChannelType) => { if ( status === RequestPhoneCodeStatus.LoadingCountryCode || status === RequestPhoneCodeStatus.RequestingCode diff --git a/app/screens/phone-auth-screen/request-phone-code-registration.ts b/app/screens/phone-auth-screen/request-phone-code-registration.ts new file mode 100644 index 0000000000..b36210b8b3 --- /dev/null +++ b/app/screens/phone-auth-screen/request-phone-code-registration.ts @@ -0,0 +1,264 @@ +import { useAppConfig } from "@app/hooks" +import { useEffect, useMemo, useState } from "react" +import parsePhoneNumber, { + AsYouType, + CountryCode, + getCountryCallingCode, +} from "libphonenumber-js/mobile" +import { gql } from "@apollo/client" +import { + PhoneCodeChannelType, + useSupportedCountriesQuery, + useUserPhoneRegistrationInitiateMutation, +} from "@app/graphql/generated" + +export const RequestPhoneCodeStatus = { + LoadingCountryCode: "LoadingCountryCode", + InputtingPhoneNumber: "InputtingPhoneNumber", + RequestingCode: "RequestingCode", + SuccessRequestingCode: "SuccessRequestingCode", + Error: "Error", +} as const + +export const ErrorType = { + InvalidPhoneNumberError: "InvalidPhoneNumberError", + TooManyAttemptsError: "TooManyAttemptsError", + RequestCodeError: "RequestCodeError", + UnsupportedCountryError: "UnsupportedCountryError", +} as const + +import axios from "axios" +import { useNavigation } from "@react-navigation/native" +import { StackNavigationProp } from "@react-navigation/stack" +import { RootStackParamList } from "@app/navigation/stack-param-lists" + +type ErrorType = (typeof ErrorType)[keyof typeof ErrorType] + +export type RequestPhoneCodeStatus = + (typeof RequestPhoneCodeStatus)[keyof typeof RequestPhoneCodeStatus] + +type PhoneInputInfo = { + countryCode: CountryCode + countryCallingCode: string + formattedPhoneNumber: string + rawPhoneNumber: string +} + +export type UseRequestPhoneCodeReturn = { + submitPhoneNumber: (phoneCodeChannel: PhoneCodeChannelType) => void + setStatus: (status: RequestPhoneCodeStatus) => void + status: RequestPhoneCodeStatus + phoneInputInfo?: PhoneInputInfo + validatedPhoneNumber?: string + isWhatsAppSupported: boolean + isSmsSupported: boolean + phoneCodeChannel: PhoneCodeChannelType + error?: ErrorType + setCountryCode: (countryCode: CountryCode) => void + setPhoneNumber: (number: string) => void + supportedCountries: CountryCode[] +} + +export const PhoneCodeChannelToFriendlyName = { + [PhoneCodeChannelType.Sms]: "SMS", + [PhoneCodeChannelType.Whatsapp]: "WhatsApp", +} + +gql` + mutation userPhoneRegistrationInitiate($input: UserPhoneRegistrationInitiateInput!) { + userPhoneRegistrationInitiate(input: $input) { + errors { + message + } + success + } + } + + query supportedCountries { + globals { + supportedCountries { + id + supportedAuthChannels + } + } + } +` + +export const useRequestPhoneCodeRegistration = (): UseRequestPhoneCodeReturn => { + const [status, setStatus] = useState( + RequestPhoneCodeStatus.LoadingCountryCode, + ) + + const [countryCode, setCountryCode] = useState() + const [rawPhoneNumber, setRawPhoneNumber] = useState("") + const [validatedPhoneNumber, setValidatedPhoneNumber] = useState() + const [phoneCodeChannel, setPhoneCodeChannel] = useState( + PhoneCodeChannelType.Sms, + ) + const { appConfig } = useAppConfig() + const skipRequestPhoneCode = appConfig.galoyInstance.name === "Local" + + const [registerPhone] = useUserPhoneRegistrationInitiateMutation() + + const [error, setError] = useState() + + const navigation = + useNavigation>() + + const { data } = useSupportedCountriesQuery() + + const { isWhatsAppSupported, isSmsSupported, allSupportedCountries } = useMemo(() => { + const currentCountry = data?.globals?.supportedCountries.find( + (country) => country.id === countryCode, + ) + + const allSupportedCountries = (data?.globals?.supportedCountries.map( + (country) => country.id, + ) || []) as CountryCode[] + + const isWhatsAppSupported = + currentCountry?.supportedAuthChannels.includes(PhoneCodeChannelType.Whatsapp) || + false + const isSmsSupported = + currentCountry?.supportedAuthChannels.includes(PhoneCodeChannelType.Sms) || false + + return { + isWhatsAppSupported, + isSmsSupported, + allSupportedCountries, + } + }, [data?.globals, countryCode]) + + useEffect(() => { + const getCountryCodeFromIP = async () => { + let defaultCountryCode = "SV" as CountryCode + try { + const response = await axios({ + method: "get", + url: "https://ipapi.co/json/", + timeout: 5000, + }) + const data = response.data + + if (data && data.country_code) { + const countryCode = data.country_code + defaultCountryCode = countryCode + } else { + console.warn("no data or country_code in response") + } + } catch (error) { + console.error(error) + } + + setCountryCode(defaultCountryCode) + setStatus(RequestPhoneCodeStatus.InputtingPhoneNumber) + } + + getCountryCodeFromIP() + }, []) + + const setPhoneNumber = (number: string) => { + if (status === RequestPhoneCodeStatus.RequestingCode) { + return + } + // handle paste + if (number.length - rawPhoneNumber.length > 1) { + const parsedPhoneNumber = parsePhoneNumber(number, countryCode) + + if (parsedPhoneNumber?.isValid()) { + parsedPhoneNumber.country && setCountryCode(parsedPhoneNumber.country) + } + } + + setRawPhoneNumber(number) + setError(undefined) + setStatus(RequestPhoneCodeStatus.InputtingPhoneNumber) + } + + const submitPhoneNumber = async (phoneCodeChannel: PhoneCodeChannelType) => { + if ( + status === RequestPhoneCodeStatus.LoadingCountryCode || + status === RequestPhoneCodeStatus.RequestingCode + ) { + return + } + + const parsedPhoneNumber = parsePhoneNumber(rawPhoneNumber, countryCode) + phoneCodeChannel && setPhoneCodeChannel(phoneCodeChannel) + if (parsedPhoneNumber?.isValid()) { + if ( + !parsedPhoneNumber.country || + (phoneCodeChannel === PhoneCodeChannelType.Sms && !isSmsSupported) || + (phoneCodeChannel === PhoneCodeChannelType.Whatsapp && !isWhatsAppSupported) + ) { + setStatus(RequestPhoneCodeStatus.Error) + setError(ErrorType.UnsupportedCountryError) + return + } + + setValidatedPhoneNumber(parsedPhoneNumber.number) + + if (skipRequestPhoneCode) { + navigation.navigate("phoneRegistrationValidate", { + phone: parsedPhoneNumber.number, + channel: phoneCodeChannel, + }) + return + } + + setStatus(RequestPhoneCodeStatus.RequestingCode) + + try { + const res = await registerPhone({ + variables: { + input: { phone: parsedPhoneNumber.number, channel: phoneCodeChannel }, + }, + }) + + if (res.data?.userPhoneRegistrationInitiate?.errors?.length) { + setStatus(RequestPhoneCodeStatus.Error) + // TODO: show error message + setError(ErrorType.RequestCodeError) + } else { + setStatus(RequestPhoneCodeStatus.SuccessRequestingCode) + navigation.navigate("phoneRegistrationValidate", { + phone: parsedPhoneNumber.number, + channel: phoneCodeChannel, + }) + } + } catch (error) { + console.error(error) + setStatus(RequestPhoneCodeStatus.Error) + setError(ErrorType.RequestCodeError) + } + } else { + setStatus(RequestPhoneCodeStatus.Error) + setError(ErrorType.InvalidPhoneNumberError) + } + } + + let phoneInputInfo: PhoneInputInfo | undefined = undefined + if (countryCode) { + phoneInputInfo = { + countryCode, + formattedPhoneNumber: new AsYouType(countryCode).input(rawPhoneNumber), + countryCallingCode: getCountryCallingCode(countryCode), + rawPhoneNumber, + } + } + + return { + status, + setStatus, + phoneInputInfo, + validatedPhoneNumber, + error, + submitPhoneNumber, + phoneCodeChannel, + isWhatsAppSupported, + isSmsSupported, + setCountryCode, + setPhoneNumber, + supportedCountries: allSupportedCountries, + } +} diff --git a/app/screens/settings-screen/account-screen.tsx b/app/screens/settings-screen/account-screen.tsx index d20915f7b4..8fa9183841 100644 --- a/app/screens/settings-screen/account-screen.tsx +++ b/app/screens/settings-screen/account-screen.tsx @@ -64,6 +64,7 @@ gql` } me { id + phone email { address verified @@ -79,6 +80,7 @@ gql` } me { id + phone email { address verified @@ -118,13 +120,15 @@ export const AccountScreen = () => { skip: !isAtLeastLevelZero, }) - const phoneNumber = data?.me?.phone || "unknown" const email = data?.me?.email?.address - const emailAndVerified = Boolean(email) && Boolean(data?.me?.email?.verified) - const emailSetButUnverified = Boolean(email) && (!data?.me?.email?.verified || false) - const phoneAndEmailVerified = Boolean(data?.me?.phone) && emailAndVerified + const emailVerified = Boolean(email) && Boolean(data?.me?.email?.verified) + const phoneVerified = Boolean(data?.me?.phone) + const emailUnverified = Boolean(email) && (!data?.me?.email?.verified || false) + const phoneAndEmailVerified = phoneVerified && emailVerified const emailString = String(email) + const showWarningSecureAccount = useShowWarningSecureAccount() + const [setEmailMutation] = useUserEmailRegistrationInitiateMutation() const btcWallet = getBtcWallet(data?.me?.defaultAccount?.wallets) @@ -210,14 +214,16 @@ export const AccountScreen = () => { const logoutAlert = () => { const logAlertContent = () => { + const phoneNumber = String(data?.me?.phone) if (phoneAndEmailVerified) { return LL.AccountScreen.logoutAlertContentPhoneEmail({ phoneNumber, email: emailString, }) - } else if (emailAndVerified) { + } else if (emailVerified) { return LL.AccountScreen.logoutAlertContentEmail({ email: emailString }) } + // phone verified return LL.AccountScreen.logoutAlertContentPhone({ phoneNumber }) } @@ -359,9 +365,6 @@ export const AccountScreen = () => { } } - const emailAndPhoneActivated = Boolean(phoneNumber) && Boolean(emailAndVerified) - const showWarningSecureAccount = useShowWarningSecureAccount() - const accountSettingsList: SettingRow[] = [ { category: LL.AccountScreen.accountLevel(), @@ -399,40 +402,41 @@ export const AccountScreen = () => { category: LL.AccountScreen.phoneNumberAuthentication(), id: "phone", icon: "call-outline", - subTitleText: phoneNumber, - action: deletePhonePrompt, - enabled: emailAndPhoneActivated, - chevronLogo: emailAndPhoneActivated ? "close-circle-outline" : undefined, - chevronColor: emailAndPhoneActivated ? colors.red : undefined, - chevronSize: emailAndPhoneActivated ? 28 : undefined, + 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()}${ - emailSetButUnverified ? LL.AccountScreen.unverified() : "" + emailUnverified ? LL.AccountScreen.unverified() : "" }`, id: "email", icon: "mail-outline", subTitleText: email ?? LL.AccountScreen.tapToAdd(), action: emailSet, - enabled: !emailAndVerified, - greyed: emailAndVerified, - chevronLogo: emailSetButUnverified ? "alert-circle-outline" : undefined, - chevronColor: emailSetButUnverified ? colors.primary : undefined, - chevronSize: emailSetButUnverified ? 24 : undefined, - styleDivider: !email, + enabled: !emailVerified, + greyed: emailVerified, + chevronLogo: emailUnverified ? "alert-circle-outline" : undefined, + chevronColor: emailUnverified ? colors.primary : undefined, + chevronSize: emailUnverified ? 24 : undefined, + styleDivider: !phoneAndEmailVerified, }, { category: LL.AccountScreen.removeEmail(), id: "remove-email", icon: "trash-outline", action: deleteEmailPrompt, - enabled: Boolean(email), - greyed: !email, + enabled: Boolean(phoneAndEmailVerified), chevron: false, styleDivider: true, - hidden: !email, + hidden: !phoneAndEmailVerified, }, ] diff --git a/app/screens/settings-screen/types.d.ts b/app/screens/settings-screen/types.d.ts index 0940d10f2a..281bb07d3b 100644 --- a/app/screens/settings-screen/types.d.ts +++ b/app/screens/settings-screen/types.d.ts @@ -4,7 +4,7 @@ type SettingRow = { category: string hidden?: boolean enabled?: boolean - subTitleText?: string + subTitleText?: string | null subTitleDefaultValue?: string action?: () => void greyed?: boolean diff --git a/app/utils/analytics.ts b/app/utils/analytics.ts index ff7128a76a..18bc6683ee 100644 --- a/app/utils/analytics.ts +++ b/app/utils/analytics.ts @@ -64,6 +64,10 @@ export const logUpgradeLoginAttempt = () => { analytics().logEvent("upgrade_login_attempt") } +export const logAddPhoneAttempt = () => { + analytics().logEvent("add_phone_attempt") +} + export const logUpgradeLoginSuccess = () => { analytics().logEvent("upgrade_login_success") } diff --git a/e2e/utils/email.ts b/e2e/utils/email.ts index 0bdeac7161..801210924e 100644 --- a/e2e/utils/email.ts +++ b/e2e/utils/email.ts @@ -40,7 +40,6 @@ const getEmail = async (inboxId: string, index: number) => { try { const { data } = await axios.request(optionsGetEmails) const { subject, body } = data - console.log({ subject, body }) return { subject, body } } catch (error) { console.error(error)