Skip to content

Commit

Permalink
Merge pull request #44321 from waterim/feat-42432-Implement_3ds_flow
Browse files Browse the repository at this point in the history
  • Loading branch information
blimpich authored Jul 23, 2024
2 parents 7675c95 + ee3d7d1 commit bf770dd
Show file tree
Hide file tree
Showing 14 changed files with 172 additions and 9 deletions.
2 changes: 1 addition & 1 deletion ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2635,4 +2635,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: d5e281e5370cb0211a104efd90eb5fa7af936e14

COCOAPODS: 1.15.2
COCOAPODS: 1.15.2
5 changes: 5 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3869,6 +3869,9 @@ const CONST = {
ENABLED: 'ENABLED',
DISABLED: 'DISABLED',
},
STRIPE_GBP_AUTH_STATUSES: {
SUCCEEDED: 'succeeded',
},
TAB: {
NEW_CHAT_TAB_ID: 'NewChatTab',
NEW_CHAT: 'chat',
Expand Down Expand Up @@ -5287,8 +5290,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: {
Expand Down
1 change: 1 addition & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/hooks/useSubscriptionPossibleCostSavings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions src/hooks/useSubscriptionPrice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4058,6 +4058,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`,
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4579,6 +4579,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`}`,
Expand Down
5 changes: 5 additions & 0 deletions src/libs/API/parameters/VerifySetupIntentParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type VerifySetupIntentParams = {
accountID: number;
isVerifying: boolean;
};
export default VerifySetupIntentParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
8 changes: 6 additions & 2 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand All @@ -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;
};
Expand Down
34 changes: 28 additions & 6 deletions src/libs/actions/PaymentMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
});
}
}

/**
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -533,4 +554,5 @@ export {
clearPaymentCard3dsVerification,
clearWalletTermsError,
setPaymentCardForm,
verifySetupIntent,
};
96 changes: 96 additions & 0 deletions src/pages/settings/Subscription/CardAuthenticationModal/index.tsx
Original file line number Diff line number Diff line change
@@ -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<string>) => {
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 (
<Modal
type={CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE}
isVisible={!!authenticationLink}
onClose={onModalClose}
onModalHide={onModalClose}
>
<ScreenWrapper
style={styles.pb0}
includePaddingTop={false}
includeSafeAreaPaddingBottom={false}
testID={CardAuthenticationModal.displayName}
>
<HeaderWithBackButton
title={headerTitle}
shouldShowBorderBottom
shouldShowCloseButton
onCloseButtonPress={onModalClose}
shouldShowBackButton={false}
/>
{isLoading && <FullScreenLoadingIndicator />}
<View style={[styles.flex1]}>
<iframe
src={authenticationLink}
title="Statements"
height="100%"
width="100%"
seamless
style={{border: 'none'}}
onLoad={() => {
setIsLoading(false);
}}
/>
</View>
</ScreenWrapper>
</Modal>
);
}

CardAuthenticationModal.displayName = 'CardAuthenticationModal';

export default CardAuthenticationModal;
2 changes: 2 additions & 0 deletions src/pages/settings/Subscription/PaymentCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import useSubscriptionPrice from '@hooks/useSubscriptionPrice';
import useThemeStyles from '@hooks/useThemeStyles';
import * as CardUtils from '@libs/CardUtils';
import {convertToShortDisplayString} from '@libs/CurrencyUtils';
import CardAuthenticationModal from '@pages/settings/Subscription/CardAuthenticationModal';
import * as PaymentMethods from '@userActions/PaymentMethods';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -107,6 +108,7 @@ function AddPaymentCard() {
}
/>
</View>
<CardAuthenticationModal headerTitle={translate('subscription.authenticatePaymentCard')} />
</ScreenWrapper>
);
}
Expand Down
11 changes: 11 additions & 0 deletions src/types/onyx/PrivateStripeCustomer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** Model of private stripe customer ID */
type PrivateStripeCustomer = {
/** Currency of the stripe payment */
[currency: string]: string;
/** paymentMethodID of the stripe payment */
paymentMethodID: string;
/** Status of the stripe payment */
status: string;
};

export default PrivateStripeCustomer;

0 comments on commit bf770dd

Please sign in to comment.