diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 54fcd1e839931..ad022b92e8ce8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2635,4 +2635,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: d5e281e5370cb0211a104efd90eb5fa7af936e14 -COCOAPODS: 1.15.2 +COCOAPODS: 1.15.2 \ No newline at end of file diff --git a/src/CONST.ts b/src/CONST.ts index a1193fd8bf32b..3dfc64c7f9c7c 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3866,6 +3866,9 @@ const CONST = { ENABLED: 'ENABLED', DISABLED: 'DISABLED', }, + STRIPE_GBP_AUTH_STATUSES: { + SUCCEEDED: 'succeeded', + }, TAB: { NEW_CHAT_TAB_ID: 'NewChatTab', NEW_CHAT: 'chat', @@ -5284,8 +5287,10 @@ const CONST = { PAYMENT_CARD_CURRENCY: { USD: 'USD', AUD: 'AUD', + GBP: 'GBP', NZD: 'NZD', }, + GBP_AUTHENTICATION_COMPLETE: '3DS-authentication-complete', SUBSCRIPTION_PRICE_FACTOR: 2, FEEDBACK_SURVEY_OPTIONS: { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index dfbe28fe52092..0d93cbca21943 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -826,6 +826,7 @@ type OnyxValuesMapping = { [ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID]: string; [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: boolean; [ONYXKEYS.LAST_VISITED_PATH]: string | undefined; + [ONYXKEYS.VERIFY_3DS_SUBSCRIPTION]: string; [ONYXKEYS.RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields; [ONYXKEYS.UPDATE_REQUIRED]: boolean; [ONYXKEYS.RESET_REQUIRED]: boolean; diff --git a/src/hooks/useSubscriptionPossibleCostSavings.ts b/src/hooks/useSubscriptionPossibleCostSavings.ts index 059445ce002dd..ef92009549fe0 100644 --- a/src/hooks/useSubscriptionPossibleCostSavings.ts +++ b/src/hooks/useSubscriptionPossibleCostSavings.ts @@ -13,6 +13,10 @@ const POSSIBLE_COST_SAVINGS = { [CONST.POLICY.TYPE.TEAM]: 1400, [CONST.POLICY.TYPE.CORPORATE]: 3000, }, + [CONST.PAYMENT_CARD_CURRENCY.GBP]: { + [CONST.POLICY.TYPE.TEAM]: 800, + [CONST.POLICY.TYPE.CORPORATE]: 1400, + }, [CONST.PAYMENT_CARD_CURRENCY.NZD]: { [CONST.POLICY.TYPE.TEAM]: 1600, [CONST.POLICY.TYPE.CORPORATE]: 3200, diff --git a/src/hooks/useSubscriptionPrice.ts b/src/hooks/useSubscriptionPrice.ts index 9279ff94757de..0b71fe62c7c8d 100644 --- a/src/hooks/useSubscriptionPrice.ts +++ b/src/hooks/useSubscriptionPrice.ts @@ -25,6 +25,16 @@ const SUBSCRIPTION_PRICES = { [CONST.SUBSCRIPTION.TYPE.PAYPERUSE]: 1400, }, }, + [CONST.PAYMENT_CARD_CURRENCY.GBP]: { + [CONST.POLICY.TYPE.CORPORATE]: { + [CONST.SUBSCRIPTION.TYPE.ANNUAL]: 700, + [CONST.SUBSCRIPTION.TYPE.PAYPERUSE]: 1400, + }, + [CONST.POLICY.TYPE.TEAM]: { + [CONST.SUBSCRIPTION.TYPE.ANNUAL]: 400, + [CONST.SUBSCRIPTION.TYPE.PAYPERUSE]: 800, + }, + }, [CONST.PAYMENT_CARD_CURRENCY.NZD]: { [CONST.POLICY.TYPE.CORPORATE]: { [CONST.SUBSCRIPTION.TYPE.ANNUAL]: 1600, diff --git a/src/languages/en.ts b/src/languages/en.ts index 9bfe8e0faf380..67f13e17f979a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4051,6 +4051,7 @@ export default { mergedWithCashTransaction: 'matched a receipt to this transaction.', }, subscription: { + authenticatePaymentCard: 'Authenticate payment card', mobileReducedFunctionalityMessage: 'You can’t make changes to your subscription in the mobile app.', badge: { freeTrial: ({numOfDays}) => `Free trial: ${numOfDays} ${numOfDays === 1 ? 'day' : 'days'} left`, diff --git a/src/languages/es.ts b/src/languages/es.ts index a3bc94e81352d..4041716449637 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4572,6 +4572,7 @@ export default { mergedWithCashTransaction: 'encontró un recibo para esta transacción.', }, subscription: { + authenticatePaymentCard: 'Autenticar tarjeta de pago', mobileReducedFunctionalityMessage: 'No puedes hacer cambios en tu suscripción en la aplicación móvil.', badge: { freeTrial: ({numOfDays}) => `Prueba gratuita: ${numOfDays === 1 ? `queda 1 día` : `quedan ${numOfDays} días`}`, diff --git a/src/libs/API/parameters/VerifySetupIntentParams.ts b/src/libs/API/parameters/VerifySetupIntentParams.ts new file mode 100644 index 0000000000000..60138f607ad49 --- /dev/null +++ b/src/libs/API/parameters/VerifySetupIntentParams.ts @@ -0,0 +1,5 @@ +type VerifySetupIntentParams = { + accountID: number; + isVerifying: boolean; +}; +export default VerifySetupIntentParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 53705e1f502dc..0b3b0e1ece934 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -1,6 +1,7 @@ export type {default as ActivatePhysicalExpensifyCardParams} from './ActivatePhysicalExpensifyCardParams'; export type {default as AddNewContactMethodParams} from './AddNewContactMethodParams'; export type {default as AddPaymentCardParams} from './AddPaymentCardParams'; +export type {default as VerifySetupIntentParams} from './VerifySetupIntentParams'; export type {default as AddPersonalBankAccountParams} from './AddPersonalBankAccountParams'; export type {default as RestartBankAccountSetupParams} from './RestartBankAccountSetupParams'; export type {default as AddSchoolPrincipalParams} from './AddSchoolPrincipalParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index df844793d579b..5c9d4a9e93b97 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -35,6 +35,8 @@ const WRITE_COMMANDS = { CHRONOS_REMOVE_OOO_EVENT: 'Chronos_RemoveOOOEvent', MAKE_DEFAULT_PAYMENT_METHOD: 'MakeDefaultPaymentMethod', ADD_PAYMENT_CARD: 'AddPaymentCard', + ADD_PAYMENT_CARD_GBP: 'AddPaymentCardGBP', + VERIFY_SETUP_INTENT: 'User_VerifySetupIntent', TRANSFER_WALLET_BALANCE: 'TransferWalletBalance', DELETE_PAYMENT_CARD: 'DeletePaymentCard', UPDATE_PRONOUNS: 'UpdatePronouns', @@ -341,6 +343,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_EXPENSIFY_CARD_LIMIT]: Parameters.UpdateExpensifyCardLimitParams; [WRITE_COMMANDS.MAKE_DEFAULT_PAYMENT_METHOD]: Parameters.MakeDefaultPaymentMethodParams; [WRITE_COMMANDS.ADD_PAYMENT_CARD]: Parameters.AddPaymentCardParams; + [WRITE_COMMANDS.ADD_PAYMENT_CARD_GBP]: Parameters.AddPaymentCardParams; + [WRITE_COMMANDS.VERIFY_SETUP_INTENT]: Parameters.VerifySetupIntentParams; [WRITE_COMMANDS.DELETE_PAYMENT_CARD]: Parameters.DeletePaymentCardParams; [WRITE_COMMANDS.UPDATE_PRONOUNS]: Parameters.UpdatePronounsParams; [WRITE_COMMANDS.UPDATE_DISPLAY_NAME]: Parameters.UpdateDisplayNameParams; @@ -757,7 +761,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = { OPEN_OLD_DOT_LINK: 'OpenOldDotLink', OPEN_REPORT: 'OpenReport', RECONNECT_APP: 'ReconnectApp', - ADD_PAYMENT_CARD_GBR: 'AddPaymentCardGBP', + ADD_PAYMENT_CARD_GBP: 'AddPaymentCardGBP', REVEAL_EXPENSIFY_CARD_DETAILS: 'RevealExpensifyCardDetails', SWITCH_TO_OLD_DOT: 'SwitchToOldDot', TWO_FACTOR_AUTH_VALIDATE: 'TwoFactorAuth_Validate', @@ -774,7 +778,7 @@ type SideEffectRequestCommandParameters = { [SIDE_EFFECT_REQUEST_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams; [SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP]: Parameters.ReconnectAppParams; [SIDE_EFFECT_REQUEST_COMMANDS.GENERATE_SPOTNANA_TOKEN]: Parameters.GenerateSpotnanaTokenParams; - [SIDE_EFFECT_REQUEST_COMMANDS.ADD_PAYMENT_CARD_GBR]: Parameters.AddPaymentCardParams; + [SIDE_EFFECT_REQUEST_COMMANDS.ADD_PAYMENT_CARD_GBP]: Parameters.AddPaymentCardParams; [SIDE_EFFECT_REQUEST_COMMANDS.ACCEPT_SPOTNANA_TERMS]: null; [SIDE_EFFECT_REQUEST_COMMANDS.TWO_FACTOR_AUTH_VALIDATE]: Parameters.ValidateTwoFactorAuthParams; }; diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index 39e8172566175..bac3739af810d 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -13,7 +13,7 @@ import type { TransferWalletBalanceParams, UpdateBillingCurrencyParams, } from '@libs/API/parameters'; -import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as CardUtils from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; @@ -253,11 +253,24 @@ function addSubscriptionPaymentCard(cardData: { }, ]; - API.write(WRITE_COMMANDS.ADD_PAYMENT_CARD, parameters, { - optimisticData, - successData, - failureData, - }); + if (currency === CONST.PAYMENT_CARD_CURRENCY.GBP) { + // eslint-disable-next-line rulesdir/no-api-side-effects-method + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.ADD_PAYMENT_CARD_GBP, parameters, {optimisticData, successData, failureData}).then((response) => { + if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) { + return; + } + + // We are using this onyx key to open Modal and preview iframe. Potentially we can save the whole object which come from side effect + Onyx.set(ONYXKEYS.VERIFY_3DS_SUBSCRIPTION, (response as {authenticationLink: string}).authenticationLink); + }); + } else { + // eslint-disable-next-line rulesdir/no-multiple-api-calls + API.write(WRITE_COMMANDS.ADD_PAYMENT_CARD, parameters, { + optimisticData, + successData, + failureData, + }); + } } /** @@ -288,6 +301,14 @@ function clearPaymentCard3dsVerification() { Onyx.set(ONYXKEYS.VERIFY_3DS_SUBSCRIPTION, ''); } +/** + * Properly updates the nvp_privateStripeCustomerID onyx data for 3DS payment + * + */ +function verifySetupIntent(accountID: number, isVerifying = true) { + API.write(WRITE_COMMANDS.VERIFY_SETUP_INTENT, {accountID, isVerifying}); +} + /** * Set currency for payments * @@ -533,4 +554,5 @@ export { clearPaymentCard3dsVerification, clearWalletTermsError, setPaymentCardForm, + verifySetupIntent, }; diff --git a/src/pages/settings/Subscription/CardAuthenticationModal/index.tsx b/src/pages/settings/Subscription/CardAuthenticationModal/index.tsx new file mode 100644 index 0000000000000..41ed4d667fc4b --- /dev/null +++ b/src/pages/settings/Subscription/CardAuthenticationModal/index.tsx @@ -0,0 +1,96 @@ +import React, {useCallback, useEffect, useState} from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Modal from '@components/Modal'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import * as PaymentMethods from '@userActions/PaymentMethods'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; + +type CardAuthenticationModalProps = { + /** Title shown in the header of the modal */ + headerTitle?: string; +}; +function CardAuthenticationModal({headerTitle}: CardAuthenticationModalProps) { + const styles = useThemeStyles(); + const [authenticationLink] = useOnyx(ONYXKEYS.VERIFY_3DS_SUBSCRIPTION); + const [session] = useOnyx(ONYXKEYS.SESSION); + const [privateStripeCustomerID] = useOnyx(ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + if (privateStripeCustomerID?.status !== CONST.STRIPE_GBP_AUTH_STATUSES.SUCCEEDED) { + return; + } + PaymentMethods.clearPaymentCard3dsVerification(); + Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION); + }, [privateStripeCustomerID]); + + const handleGBPAuthentication = useCallback( + (event: MessageEvent) => { + const message = event.data; + if (message === CONST.GBP_AUTHENTICATION_COMPLETE) { + PaymentMethods.verifySetupIntent(session?.accountID ?? -1, true); + } + }, + [session?.accountID], + ); + + useEffect(() => { + window.addEventListener('message', handleGBPAuthentication); + return () => { + window.removeEventListener('message', handleGBPAuthentication); + }; + }, [handleGBPAuthentication]); + + const onModalClose = () => { + PaymentMethods.clearPaymentCard3dsVerification(); + }; + + return ( + + + + {isLoading && } + +