From 7dc3c04e61b56d504bfa8d5a15aa410090c30a48 Mon Sep 17 00:00:00 2001 From: Dread <34528298+islandbitcoin@users.noreply.github.com> Date: Wed, 17 Sep 2025 11:55:17 -0500 Subject: [PATCH 1/4] This PR implements new GraphQL-based email registration/login functionality, replacing the previous axios-based implementation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Key Changes 1. GraphQL Mutations Added - newUserEmailRegistrationInitiate - Initiates email registration by sending a code - newUserEmailRegistrationValidate - Validates the email code and returns auth token 2. Email Login Screens Refactored - email-login-initiate.tsx: - Migrated from axios to GraphQL mutation - Added Firebase Crashlytics error tracking - Removed useAppConfig dependency for auth URL - Improved error handling for rate limiting - email-login-validate.tsx: - Migrated from axios to GraphQL mutation - Added Crashlytics error tracking - Handles TOTP flow if 2FA is enabled - Better error messages for invalid codes and rate limiting 3. Code Quality Improvements - Removed unused useEffect import - Fixed parameter naming consistency (emailFlowId throughout) - Removed commented localhost URL from codegen.yml - Uses existing translation keys from LL.errors instead of creating new ones 4. Technical Details - Generated GraphQL types for type safety - Proper error handling with specific error codes (TOO_MANY_REQUEST, INVALID_CODE) - Analytics tracking for successful email login - Navigation flow: Initiate → Validate → (TOTP if required) → Home Files Modified - app/screens/email-login-screen/email-login-initiate.tsx - app/screens/email-login-screen/email-login-validate.tsx - app/navigation/stack-param-lists.ts - codegen.yml - app/graphql/generated.gql (auto-generated) - app/graphql/generated.ts (auto-generated) --- app/graphql/generated.gql | 25 ++++ app/graphql/generated.ts | 116 ++++++++++++++++++ app/i18n/i18n-types.ts | 14 +-- app/i18n/raw-i18n/source/en.json | 2 +- app/navigation/stack-param-lists.ts | 2 +- .../email-login-initiate.tsx | 73 ++++++----- .../email-login-validate.tsx | 74 ++++++----- 7 files changed, 235 insertions(+), 71 deletions(-) diff --git a/app/graphql/generated.gql b/app/graphql/generated.gql index 81163878e..29a37945f 100644 --- a/app/graphql/generated.gql +++ b/app/graphql/generated.gql @@ -447,6 +447,31 @@ mutation lnUsdInvoiceFeeProbe($input: LnUsdInvoiceFeeProbeInput!) { } } +mutation newUserEmailRegistrationInitiate($input: NewUserEmailRegistrationInitiateInput!) { + newUserEmailRegistrationInitiate(input: $input) { + errors { + message + code + __typename + } + emailFlowId + __typename + } +} + +mutation newUserEmailRegistrationValidate($input: NewUserEmailRegistrationValidateInput!) { + newUserEmailRegistrationValidate(input: $input) { + errors { + message + code + __typename + } + authToken + totpRequired + __typename + } +} + mutation onChainAddressCurrent($input: OnChainAddressCurrentInput!) { onChainAddressCurrent(input: $input) { errors { diff --git a/app/graphql/generated.ts b/app/graphql/generated.ts index a8cb4512e..9bdb60141 100644 --- a/app/graphql/generated.ts +++ b/app/graphql/generated.ts @@ -816,6 +816,8 @@ export type Mutation = { readonly lnUsdInvoiceCreateOnBehalfOfRecipient: LnInvoicePayload; readonly lnUsdInvoiceFeeProbe: CentAmountPayload; readonly merchantMapSuggest: MerchantPayload; + readonly newUserEmailRegistrationInitiate: NewUserEmailRegistrationInitiatePayload; + readonly newUserEmailRegistrationValidate: AuthTokenPayload; readonly onChainAddressCreate: OnChainAddressPayload; readonly onChainAddressCurrent: OnChainAddressPayload; readonly onChainPaymentSend: PaymentSendPayload; @@ -991,6 +993,16 @@ export type MutationMerchantMapSuggestArgs = { }; +export type MutationNewUserEmailRegistrationInitiateArgs = { + input: NewUserEmailRegistrationInitiateInput; +}; + + +export type MutationNewUserEmailRegistrationValidateArgs = { + input: NewUserEmailRegistrationValidateInput; +}; + + export type MutationOnChainAddressCreateArgs = { input: OnChainAddressCreateInput; }; @@ -1120,6 +1132,21 @@ export const Network = { } as const; export type Network = typeof Network[keyof typeof Network]; +export type NewUserEmailRegistrationInitiateInput = { + readonly email: Scalars['EmailAddress']['input']; +}; + +export type NewUserEmailRegistrationInitiatePayload = { + readonly __typename: 'NewUserEmailRegistrationInitiatePayload'; + readonly emailFlowId?: Maybe; + readonly errors: ReadonlyArray; +}; + +export type NewUserEmailRegistrationValidateInput = { + readonly code: Scalars['OneTimeAuthCode']['input']; + readonly emailFlowId: Scalars['String']['input']; +}; + export const NotificationChannel = { Push: 'PUSH' } as const; @@ -2340,6 +2367,20 @@ export type QuizCompletedMutationVariables = Exact<{ export type QuizCompletedMutation = { readonly __typename: 'Mutation', readonly quizCompleted: { readonly __typename: 'QuizCompletedPayload', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }>, readonly quiz?: { readonly __typename: 'Quiz', readonly id: string, readonly completed: boolean } | null } }; +export type NewUserEmailRegistrationInitiateMutationVariables = Exact<{ + input: NewUserEmailRegistrationInitiateInput; +}>; + + +export type NewUserEmailRegistrationInitiateMutation = { readonly __typename: 'Mutation', readonly newUserEmailRegistrationInitiate: { readonly __typename: 'NewUserEmailRegistrationInitiatePayload', readonly emailFlowId?: string | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string, readonly code?: string | null }> } }; + +export type NewUserEmailRegistrationValidateMutationVariables = Exact<{ + input: NewUserEmailRegistrationValidateInput; +}>; + + +export type NewUserEmailRegistrationValidateMutation = { readonly __typename: 'Mutation', readonly newUserEmailRegistrationValidate: { readonly __typename: 'AuthTokenPayload', readonly authToken?: string | null, readonly totpRequired?: boolean | null, readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string, readonly code?: string | null }> } }; + export type UserEmailRegistrationInitiateMutationVariables = Exact<{ input: UserEmailRegistrationInitiateInput; }>; @@ -5130,6 +5171,81 @@ export function useQuizCompletedMutation(baseOptions?: Apollo.MutationHookOption export type QuizCompletedMutationHookResult = ReturnType; export type QuizCompletedMutationResult = Apollo.MutationResult; export type QuizCompletedMutationOptions = Apollo.BaseMutationOptions; +export const NewUserEmailRegistrationInitiateDocument = gql` + mutation newUserEmailRegistrationInitiate($input: NewUserEmailRegistrationInitiateInput!) { + newUserEmailRegistrationInitiate(input: $input) { + errors { + message + code + } + emailFlowId + } +} + `; +export type NewUserEmailRegistrationInitiateMutationFn = Apollo.MutationFunction; + +/** + * __useNewUserEmailRegistrationInitiateMutation__ + * + * To run a mutation, you first call `useNewUserEmailRegistrationInitiateMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useNewUserEmailRegistrationInitiateMutation` 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 [newUserEmailRegistrationInitiateMutation, { data, loading, error }] = useNewUserEmailRegistrationInitiateMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useNewUserEmailRegistrationInitiateMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(NewUserEmailRegistrationInitiateDocument, options); + } +export type NewUserEmailRegistrationInitiateMutationHookResult = ReturnType; +export type NewUserEmailRegistrationInitiateMutationResult = Apollo.MutationResult; +export type NewUserEmailRegistrationInitiateMutationOptions = Apollo.BaseMutationOptions; +export const NewUserEmailRegistrationValidateDocument = gql` + mutation newUserEmailRegistrationValidate($input: NewUserEmailRegistrationValidateInput!) { + newUserEmailRegistrationValidate(input: $input) { + errors { + message + code + } + authToken + totpRequired + } +} + `; +export type NewUserEmailRegistrationValidateMutationFn = Apollo.MutationFunction; + +/** + * __useNewUserEmailRegistrationValidateMutation__ + * + * To run a mutation, you first call `useNewUserEmailRegistrationValidateMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useNewUserEmailRegistrationValidateMutation` 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 [newUserEmailRegistrationValidateMutation, { data, loading, error }] = useNewUserEmailRegistrationValidateMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useNewUserEmailRegistrationValidateMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(NewUserEmailRegistrationValidateDocument, options); + } +export type NewUserEmailRegistrationValidateMutationHookResult = ReturnType; +export type NewUserEmailRegistrationValidateMutationResult = Apollo.MutationResult; +export type NewUserEmailRegistrationValidateMutationOptions = Apollo.BaseMutationOptions; export const UserEmailRegistrationInitiateDocument = gql` mutation userEmailRegistrationInitiate($input: UserEmailRegistrationInitiateInput!) { userEmailRegistrationInitiate(input: $input) { diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index 67d0e2ca8..870a4f388 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -2708,7 +2708,7 @@ type RootTranslation = { */ advanceMode: string /** - * K​e​y​s​ ​m​a​n​a​g​e​m​e​n​t + * K​e​y​ ​m​a​n​a​g​e​m​e​n​t */ keysManagement: string /** @@ -7241,23 +7241,23 @@ export type TranslationFunctions = { /** * The amount you entered is less than the minimum amount required to send an on-chain transaction {amount}. Please consider sending this amount via Lightning! */ - onchainMinAmountInvoiceError: (arg: { amount: number | string }) => LocalizedString + onchainMinAmountInvoiceError: (arg: { amount: number }) => LocalizedString /** * The amount on the invoice is less than minimum amount {amount} */ - minAmountInvoiceError: (arg: { amount: number | string }) => LocalizedString + minAmountInvoiceError: (arg: { amount: number }) => LocalizedString /** * The amount on the invoice is greater than maximum amount {amount} */ - maxAmountInvoiceError: (arg: { amount: number | string }) => LocalizedString + maxAmountInvoiceError: (arg: { amount: number }) => LocalizedString /** * The conversion amount is less than minimum required amount {amount} */ - minAmountConvertError: (arg: { amount: number | string }) => LocalizedString + minAmountConvertError: (arg: { amount: number }) => LocalizedString /** * The conversion amount is greater than maximum amount {amount} */ - maxAmountConvertError: (arg: { amount: number | string }) => LocalizedString + maxAmountConvertError: (arg: { amount: number }) => LocalizedString } SettingsScreen: { /** @@ -7401,7 +7401,7 @@ export type TranslationFunctions = { */ advanceMode: () => LocalizedString /** - * Keys management + * Key management */ keysManagement: () => LocalizedString /** diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index 1416165e3..939753c67 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -461,7 +461,7 @@ "unlockQuestion": "To unlock, answer the question:", "youEarned": "You Earned", "registerTitle": "Need to upgrade your account", - "registerContent": "Register with your phone number to receive bitcoin" + "registerContent": "Register with your phone number to receive sats" }, "GetStartedScreen": { "logInCreateAccount": "Log in / create account", diff --git a/app/navigation/stack-param-lists.ts b/app/navigation/stack-param-lists.ts index 58e3530fa..787d11b58 100644 --- a/app/navigation/stack-param-lists.ts +++ b/app/navigation/stack-param-lists.ts @@ -123,7 +123,7 @@ export type RootStackParamList = { emailRegistrationInitiate: undefined emailRegistrationValidate: { email: string; emailRegistrationId: string } emailLoginInitiate: undefined - emailLoginValidate: { email: string; emailLoginId: string } + emailLoginValidate: { email: string; emailFlowId: string } totpRegistrationInitiate: undefined totpRegistrationValidate: { totpRegistrationId: string } totpLoginValidate: { authToken: string } diff --git a/app/screens/email-login-screen/email-login-initiate.tsx b/app/screens/email-login-screen/email-login-initiate.tsx index 4e07bd39f..85047559e 100644 --- a/app/screens/email-login-screen/email-login-initiate.tsx +++ b/app/screens/email-login-screen/email-login-initiate.tsx @@ -1,9 +1,11 @@ -import React, { useEffect, useState } from "react" +import React, { useState } from "react" import { View } from "react-native" -import axios, { isAxiosError } from "axios" +import crashlytics from "@react-native-firebase/crashlytics" import { Input, Text, makeStyles } from "@rneui/themed" import { StackScreenProps } from "@react-navigation/stack" import { RootStackParamList } from "@app/navigation/stack-param-lists" +import { gql } from "@apollo/client" +import { useNewUserEmailRegistrationInitiateMutation } from "@app/graphql/generated" // components import { Screen } from "../../components/screen" @@ -12,23 +14,35 @@ import { GaloyErrorBox } from "@app/components/atomic/galoy-error-box" // hooks import { useI18nContext } from "@app/i18n/i18n-react" -import { useAppConfig } from "@app/hooks" // utils import { testProps } from "@app/utils/testProps" import validator from "validator" +gql` + mutation newUserEmailRegistrationInitiate($input: NewUserEmailRegistrationInitiateInput!) { + newUserEmailRegistrationInitiate(input: $input) { + errors { + message + code + } + emailFlowId + } + } +` + type Props = StackScreenProps export const EmailLoginInitiateScreen: React.FC = ({ navigation }) => { const styles = useStyles() const { LL } = useI18nContext() - const { authUrl } = useAppConfig().appConfig.galoyInstance const [emailInput, setEmailInput] = useState("") const [errorMessage, setErrorMessage] = useState("") const [loading, setLoading] = useState(false) + const [newUserEmailRegistrationInitiate] = useNewUserEmailRegistrationInitiateMutation() + const updateInput = (text: string) => { setEmailInput(text) setErrorMessage("") @@ -42,40 +56,39 @@ export const EmailLoginInitiateScreen: React.FC = ({ navigation }) => { setLoading(true) - const url = `${authUrl}/auth/email/code` - try { - const res = await axios({ - url, - method: "POST", - data: { - email: emailInput, + const { data } = await newUserEmailRegistrationInitiate({ + variables: { + input: { + email: emailInput, + }, }, }) - // TODO: manage error on ip rate limit - // TODO: manage error when trying the same email too often - const emailLoginId = res.data.result - if (emailLoginId) { - console.log({ emailLoginId }) - navigation.navigate("emailLoginValidate", { emailLoginId, email: emailInput }) + const errors = data?.newUserEmailRegistrationInitiate?.errors + const emailFlowId = data?.newUserEmailRegistrationInitiate?.emailFlowId + + if (emailFlowId) { + navigation.navigate("emailLoginValidate", { + emailFlowId, + email: emailInput + }) + } else if (errors && errors.length > 0) { + // Handle specific error codes + const error = errors[0] + if (error.code === "TOO_MANY_REQUEST") { + setErrorMessage(LL.errors.tooManyRequestsPhoneCode()) + } else { + setErrorMessage(error.message || LL.errors.generic()) + } } else { - console.warn("no flow returned") + setErrorMessage(LL.errors.generic()) } } catch (err) { - console.error(err, "error in setEmailMutation") - if (isAxiosError(err)) { - console.log(err.message) // Gives you the basic error message - console.log(err.response?.data) // Gives you the response payload from the server - console.log(err.response?.status) // Gives you the HTTP status code - console.log(err.response?.headers) // Gives you the response headers - - // If the request was made but no response was received - if (!err.response) { - console.log(err.request) - } - setErrorMessage(err.message) + if (err instanceof Error) { + crashlytics().recordError(err) } + setErrorMessage(LL.errors.generic()) } finally { setLoading(false) } diff --git a/app/screens/email-login-screen/email-login-validate.tsx b/app/screens/email-login-screen/email-login-validate.tsx index 4070176af..37a2841fb 100644 --- a/app/screens/email-login-screen/email-login-validate.tsx +++ b/app/screens/email-login-screen/email-login-validate.tsx @@ -1,8 +1,10 @@ import React, { useCallback, useState } from "react" import analytics from "@react-native-firebase/analytics" +import crashlytics from "@react-native-firebase/crashlytics" import { StackScreenProps } from "@react-navigation/stack" import { RootStackParamList } from "@app/navigation/stack-param-lists" -import axios, { isAxiosError } from "axios" +import { gql } from "@apollo/client" +import { useNewUserEmailRegistrationValidateMutation } from "@app/graphql/generated" // components import { CodeInput } from "@app/components/code-input" @@ -11,36 +13,49 @@ import { CodeInput } from "@app/components/code-input" import { useAppConfig } from "@app/hooks" import { useI18nContext } from "@app/i18n/i18n-react" +gql` + mutation newUserEmailRegistrationValidate($input: NewUserEmailRegistrationValidateInput!) { + newUserEmailRegistrationValidate(input: $input) { + errors { + message + code + } + authToken + totpRequired + } + } +` + type Props = StackScreenProps export const EmailLoginValidateScreen: React.FC = ({ navigation, route }) => { const { LL } = useI18nContext() - const { authUrl } = useAppConfig().appConfig.galoyInstance const { saveToken } = useAppConfig() const [errorMessage, setErrorMessage] = useState("") const [loading, setLoading] = useState(false) - const { emailLoginId, email } = route.params + const { emailFlowId, email } = route.params + + const [newUserEmailRegistrationValidate] = useNewUserEmailRegistrationValidateMutation() const send = useCallback( async (code: string) => { try { setLoading(true) - const url = `${authUrl}/auth/email/login` - - const res2 = await axios({ - url, - method: "POST", - data: { - code, - emailLoginId, + const { data } = await newUserEmailRegistrationValidate({ + variables: { + input: { + code, + emailFlowId, + }, }, }) - const authToken = res2.data.result.authToken - const totpRequired = res2.data.result.totpRequired + const authToken = data?.newUserEmailRegistrationValidate?.authToken + const totpRequired = data?.newUserEmailRegistrationValidate?.totpRequired + const errors = data?.newUserEmailRegistrationValidate?.errors if (authToken) { if (totpRequired) { @@ -55,33 +70,28 @@ export const EmailLoginValidateScreen: React.FC = ({ navigation, route }) routes: [{ name: "authenticationCheck" }], }) } + } else if (errors && errors.length > 0) { + const error = errors[0] + if (error.code === "INVALID_CODE") { + setErrorMessage(LL.errors.generic()) + } else if (error.code === "TOO_MANY_REQUEST") { + setErrorMessage(LL.errors.tooManyRequestsPhoneCode()) + } else { + setErrorMessage(error.message || LL.errors.generic()) + } } else { - throw new Error(LL.common.errorAuthToken()) + setErrorMessage(LL.errors.generic()) } } catch (err) { - console.error(err, "error axios") - if (isAxiosError(err)) { - console.log(err.message) // Gives you the basic error message - console.log(err.response?.data) // Gives you the response payload from the server - console.log(err.response?.status) // Gives you the HTTP status code - console.log(err.response?.headers) // Gives you the response headers - - // If the request was made but no response was received - if (!err.response) { - console.log(err.request) - } - - if (err.response?.data?.error) { - setErrorMessage(err.response?.data?.error) - } else { - setErrorMessage(err.message) - } + if (err instanceof Error) { + crashlytics().recordError(err) } + setErrorMessage(LL.errors.generic()) } finally { setLoading(false) } }, - [emailLoginId, navigation, authUrl, saveToken, LL], + [emailFlowId, navigation, saveToken, LL, newUserEmailRegistrationValidate], ) const header = LL.EmailLoginValidateScreen.header({ email }) From 3e3075fd4d6161f53cc68823bfb652513446d82a Mon Sep 17 00:00:00 2001 From: Dread <34528298+islandbitcoin@users.noreply.github.com> Date: Wed, 17 Sep 2025 18:06:28 -0500 Subject: [PATCH 2/4] revert string type --- app/i18n/i18n-types.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index 870a4f388..d2d6c45bc 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -7241,23 +7241,23 @@ export type TranslationFunctions = { /** * The amount you entered is less than the minimum amount required to send an on-chain transaction {amount}. Please consider sending this amount via Lightning! */ - onchainMinAmountInvoiceError: (arg: { amount: number }) => LocalizedString + onchainMinAmountInvoiceError: (arg: { amount: number | string }) => LocalizedString /** * The amount on the invoice is less than minimum amount {amount} */ - minAmountInvoiceError: (arg: { amount: number }) => LocalizedString + minAmountInvoiceError: (arg: { amount: number | string }) => LocalizedString /** * The amount on the invoice is greater than maximum amount {amount} */ - maxAmountInvoiceError: (arg: { amount: number }) => LocalizedString + maxAmountInvoiceError: (arg: { amount: number | string }) => LocalizedString /** * The conversion amount is less than minimum required amount {amount} */ - minAmountConvertError: (arg: { amount: number }) => LocalizedString + minAmountConvertError: (arg: { amount: number | string }) => LocalizedString /** * The conversion amount is greater than maximum amount {amount} */ - maxAmountConvertError: (arg: { amount: number }) => LocalizedString + maxAmountConvertError: (arg: { amount: number | string }) => LocalizedString } SettingsScreen: { /** From 93763539fef4d91fecc0939ee72546ecf83184bc Mon Sep 17 00:00:00 2001 From: Dread <34528298+islandbitcoin@users.noreply.github.com> Date: Wed, 17 Sep 2025 18:08:16 -0500 Subject: [PATCH 3/4] update email label --- app/i18n/en/index.ts | 2 +- app/i18n/i18n-types.ts | 2 +- app/i18n/raw-i18n/source/en.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index e627b6aca..f174d18ee 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -977,7 +977,7 @@ const en: BaseTranslation = { phone: "Phone", importUsingPhone: "Import or Create your Cash Wallet using phone number", email: "Email", - importUsingEmail: "Import your Cash Wallet using email address", + importUsingEmail: "Import or Create your Cash wallet using email address", login: "Login/Create", done: "Done", }, diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index d2d6c45bc..14dbe76e5 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -7876,7 +7876,7 @@ export type TranslationFunctions = { */ email: () => LocalizedString /** - * Import your Cash Wallet using email address + * Import or Create your Cash wallet using email address */ importUsingEmail: () => LocalizedString /** diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index 939753c67..dfeb9ffff 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -914,7 +914,7 @@ "phone": "Phone", "importUsingPhone": "Import or Create your Cash Wallet using phone number", "email": "Email", - "importUsingEmail": "Import your Cash Wallet using email address", + "importUsingEmail": "Import or Create your Cash wallet using email address", "login": "Login/Create", "done": "Done" }, From e0edb80a1f8987801c3d7a6220eecefb9faa21ea Mon Sep 17 00:00:00 2001 From: Dread <34528298+islandbitcoin@users.noreply.github.com> Date: Wed, 22 Oct 2025 12:51:11 -0400 Subject: [PATCH 4/4] update email flow use cases, add dev comments --- app/components/home-screen/QuickStart.tsx | 2 +- app/graphql/generated.ts | 8 ++++- app/i18n/i18n-types.ts | 12 +++---- .../email-registration-validate.tsx | 33 +++++++++++++++---- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/app/components/home-screen/QuickStart.tsx b/app/components/home-screen/QuickStart.tsx index 129de243d..f1ff6bda6 100644 --- a/app/components/home-screen/QuickStart.tsx +++ b/app/components/home-screen/QuickStart.tsx @@ -142,7 +142,7 @@ const QuickStart = () => { carouselData = carouselData.filter((el) => el.type !== "nonCustodialWallet") } if ( - data?.me?.defaultAccount.level === AccountLevel.Zero || + // data?.me?.defaultAccount.level === AccountLevel.Zero || !!data?.me?.email?.address || persistentState?.closedQuickStartTypes?.includes("email") ) { diff --git a/app/graphql/generated.ts b/app/graphql/generated.ts index 9bdb60141..5f18a5ba1 100644 --- a/app/graphql/generated.ts +++ b/app/graphql/generated.ts @@ -450,6 +450,12 @@ export type InitiateCashoutInput = { readonly walletId: Scalars['WalletId']['input']; }; +export type InitiatedCashoutResponse = { + readonly __typename: 'InitiatedCashoutResponse'; + readonly errors: ReadonlyArray; + readonly journalId?: Maybe; +}; + export type InitiationVia = InitiationViaIntraLedger | InitiationViaLn | InitiationViaOnChain; export type InitiationViaIntraLedger = { @@ -740,7 +746,7 @@ export type Mutation = { * Start the Cashout process; * User sends USD to Flash via Ibex and receives USD or JMD to bank account. */ - readonly initiateCashout: SuccessPayload; + readonly initiateCashout: InitiatedCashoutResponse; /** * Actions a payment which is internal to the ledger e.g. it does * not use onchain/lightning. Returns payment status (success, diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index 14dbe76e5..49673c460 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -3190,7 +3190,7 @@ type RootTranslation = { */ email: string /** - * I​m​p​o​r​t​ ​y​o​u​r​ ​C​a​s​h​ ​W​a​l​l​e​t​ ​u​s​i​n​g​ ​e​m​a​i​l​ ​a​d​d​r​e​s​s + * I​m​p​o​r​t​ ​o​r​ ​C​r​e​a​t​e​ ​y​o​u​r​ ​C​a​s​h​ ​w​a​l​l​e​t​ ​u​s​i​n​g​ ​e​m​a​i​l​ ​a​d​d​r​e​s​s */ importUsingEmail: string /** @@ -7241,23 +7241,23 @@ export type TranslationFunctions = { /** * The amount you entered is less than the minimum amount required to send an on-chain transaction {amount}. Please consider sending this amount via Lightning! */ - onchainMinAmountInvoiceError: (arg: { amount: number | string }) => LocalizedString + onchainMinAmountInvoiceError: (arg: { amount: number }) => LocalizedString /** * The amount on the invoice is less than minimum amount {amount} */ - minAmountInvoiceError: (arg: { amount: number | string }) => LocalizedString + minAmountInvoiceError: (arg: { amount: number }) => LocalizedString /** * The amount on the invoice is greater than maximum amount {amount} */ - maxAmountInvoiceError: (arg: { amount: number | string }) => LocalizedString + maxAmountInvoiceError: (arg: { amount: number }) => LocalizedString /** * The conversion amount is less than minimum required amount {amount} */ - minAmountConvertError: (arg: { amount: number | string }) => LocalizedString + minAmountConvertError: (arg: { amount: number }) => LocalizedString /** * The conversion amount is greater than maximum amount {amount} */ - maxAmountConvertError: (arg: { amount: number | string }) => LocalizedString + maxAmountConvertError: (arg: { amount: number }) => LocalizedString } SettingsScreen: { /** diff --git a/app/screens/email-registration-screen/email-registration-validate.tsx b/app/screens/email-registration-screen/email-registration-validate.tsx index d8042cc4d..03eabb23b 100644 --- a/app/screens/email-registration-screen/email-registration-validate.tsx +++ b/app/screens/email-registration-screen/email-registration-validate.tsx @@ -5,10 +5,17 @@ import { useI18nContext } from "@app/i18n/i18n-react" import { RootStackParamList } from "@app/navigation/stack-param-lists" import { RouteProp, useNavigation } from "@react-navigation/native" import { StackNavigationProp } from "@react-navigation/stack" -import * as React from "react" import { useCallback, useState } from "react" import { Alert } from "react-native" +/** + * Validates the email verification code for existing users adding email to their account. + * + * For TRIAL accounts (AccountLevel.Zero), this mutation also handles the schema upgrade + * from 'username_password_deviceid_v0' to support email authentication. + * The backend will automatically upgrade TRIAL accounts to PERSONAL (AccountLevel.One) + * when email verification is successful. + */ gql` mutation userEmailRegistrationValidate($input: UserEmailRegistrationValidateInput!) { userEmailRegistrationValidate(input: $input) { @@ -33,14 +40,13 @@ type Props = { export const EmailRegistrationValidateScreen: React.FC = ({ route }) => { const navigation = useNavigation>() - - const [errorMessage, setErrorMessage] = React.useState("") - const { LL } = useI18nContext() + const [errorMessage, setErrorMessage] = useState("") + const [loading, setLoading] = useState(false) + const [emailVerify] = useUserEmailRegistrationValidateMutation() - const [loading, setLoading] = useState(false) const { emailRegistrationId, email } = route.params const send = useCallback( @@ -52,12 +58,14 @@ export const EmailRegistrationValidateScreen: React.FC = ({ route }) => { variables: { input: { code, emailRegistrationId } }, }) + // Handle validation errors (invalid code, expired code, etc.) if (res.data?.userEmailRegistrationValidate.errors) { const error = res.data.userEmailRegistrationValidate.errors[0]?.message // TODO: manage translation for errors setErrorMessage(error) } + // Email verification successful if (res.data?.userEmailRegistrationValidate.me?.email?.verified) { Alert.alert( LL.common.success(), @@ -66,7 +74,20 @@ export const EmailRegistrationValidateScreen: React.FC = ({ route }) => { { text: LL.common.ok(), onPress: () => { - navigation.navigate("settings") + /** + * Use navigation.reset() instead of navigation.navigate() to: + * 1. Clear the navigation stack + * 2. Force re-authentication via authenticationCheck + * 3. Refresh all user data (including account level) + * + * This is critical for TRIAL accounts, as the backend upgrades them + * to PERSONAL (AccountLevel.One) during email verification. + * The reset ensures the app displays the updated account level. + */ + navigation.reset({ + index: 0, + routes: [{ name: "authenticationCheck" }], + }) }, }, ],