diff --git a/shared/lib/shield.ts b/shared/lib/shield.ts index 204dbe88be17..57d5512b5b0b 100644 --- a/shared/lib/shield.ts +++ b/shared/lib/shield.ts @@ -103,6 +103,20 @@ export function getIsShieldSubscriptionEndingSoon( ); } +/** + * Check if the subscription can change payment method from crypto to card. + * + * @param subscriptions + * @returns + */ +export function getIsShieldSubscriptionCanChangePaymentMethodToCard( + subscriptions: Subscription | Subscription[], +): boolean { + // atm crypto to card change payment method only work if there is no stripe subscription yet, which means provisional -> (invalid payment method) -> paused + const shieldSubscription = getShieldSubscription(subscriptions); + return shieldSubscription?.status === SUBSCRIPTION_STATUSES.paused; +} + /** * Get the subscription payment data. * diff --git a/ui/hooks/subscription/useSubscription.ts b/ui/hooks/subscription/useSubscription.ts index f6776d3b96d3..cdc9c3f8880c 100644 --- a/ui/hooks/subscription/useSubscription.ts +++ b/ui/hooks/subscription/useSubscription.ts @@ -52,7 +52,6 @@ import { getMetaMetricsId, getModalTypeForShieldEntryModal, getUnapprovedConfirmations, - selectNetworkConfigurationByChainId, } from '../../selectors'; import { useSubscriptionMetrics } from '../shield/metrics/useSubscriptionMetrics'; import { CaptureShieldSubscriptionRequestParams } from '../shield/metrics/types'; @@ -68,6 +67,8 @@ import { openWindow } from '../../helpers/utils/window'; import { SUPPORT_LINK } from '../../../shared/lib/ui-utils'; import { MetaMetricsEventName } from '../../../shared/constants/metametrics'; import { useAccountTotalFiatBalance } from '../useAccountTotalFiatBalance'; +import { getNetworkConfigurationsByChainId } from '../../../shared/modules/selectors/networks'; +import { isCryptoPaymentMethod } from '../../pages/settings/transaction-shield-tab/types'; import { TokenWithApprovalAmount, useSubscriptionPricing, @@ -326,13 +327,9 @@ export const useSubscriptionCryptoApprovalTransaction = ( // Account address will be the same for all EVM accounts getInternalAccountBySelectedAccountGroupAndCaip(state, 'eip155:1'), ); - const networkConfiguration = useSelector((state) => - selectNetworkConfigurationByChainId(state, selectedToken?.chainId as Hex), + const networkConfigurationsByChainId = useSelector( + getNetworkConfigurationsByChainId, ); - const networkClientId = - networkConfiguration?.rpcEndpoints[ - networkConfiguration.defaultRpcEndpointIndex ?? 0 - ]?.networkClientId; const hasPendingApprovals = useSelector(getUnapprovedConfirmations).length > 0; @@ -356,6 +353,13 @@ export const useSubscriptionCryptoApprovalTransaction = ( throw new Error('No token selected'); } + const networkConfiguration = + networkConfigurationsByChainId[selectedToken.chainId as Hex]; + const networkClientId = + networkConfiguration?.rpcEndpoints[ + networkConfiguration.defaultRpcEndpointIndex ?? 0 + ]?.networkClientId; + const spenderAddress = subscriptionPricing?.paymentMethods ?.find((method) => method.type === PAYMENT_TYPES.byCrypto) ?.chains?.find( @@ -382,8 +386,8 @@ export const useSubscriptionCryptoApprovalTransaction = ( setShieldTransactionDispatched, subscriptionPricing, evmInternalAccount, + networkConfigurationsByChainId, selectedToken, - networkClientId, ]); return { @@ -650,3 +654,84 @@ export const useHandleSubscriptionSupportAction = () => { handleClickContactSupport, }; }; + +export const useUpdateSubscriptionCryptoPaymentMethod = ({ + subscription, +}: { + subscription?: Subscription; +}) => { + const dispatch = useDispatch(); + const [selectedChangePaymentToken, setSelectedChangePaymentToken] = useState< + | Pick< + TokenWithApprovalAmount, + 'chainId' | 'address' | 'approvalAmount' | 'symbol' + > + | undefined + >(); + + const { execute: executeSubscriptionCryptoApprovalTransaction } = + useSubscriptionCryptoApprovalTransaction(selectedChangePaymentToken); + + // This update the subscription payment method to crypto + // and trigger the approval transaction flow + const [handler, result] = useAsyncCallback(async () => { + if (!subscription) { + throw new Error('No subscription exist'); + } + // only allow update payment method to crypto -> crypto atm + if (!isCryptoPaymentMethod(subscription.paymentMethod)) { + throw new Error('Subscription is not a crypto payment method'); + } + if (!selectedChangePaymentToken) { + throw new Error('No token selected'); + } + + // save the changing payment method as last used subscription payment method and plan to Redux store + await dispatch( + setLastUsedSubscriptionPaymentDetails(PRODUCT_TYPES.SHIELD, { + type: PAYMENT_TYPES.byCrypto, + paymentTokenAddress: selectedChangePaymentToken.address as Hex, + paymentTokenSymbol: selectedChangePaymentToken.symbol, + plan: subscription.interval, + }), + ); + await executeSubscriptionCryptoApprovalTransaction(); + }, [ + subscription, + executeSubscriptionCryptoApprovalTransaction, + dispatch, + selectedChangePaymentToken, + ]); + + const selectedPaymentTokenAddress = selectedChangePaymentToken?.address; + // trigger update subscription crypto payment method when selected change payment token changes + useEffect(() => { + if (selectedPaymentTokenAddress) { + handler().then(() => { + // reset selected change payment token after update subscription crypto payment method succeeded + setSelectedChangePaymentToken(undefined); + }); + } + }, [selectedPaymentTokenAddress, handler]); + + // execute update subscription crypto payment method with new token by settings state with useEffect above to workaround useAsyncCallback hook not accepting callback parameter + const execute = useCallback( + ( + newToken: Pick< + TokenWithApprovalAmount, + 'chainId' | 'address' | 'approvalAmount' | 'symbol' + >, + ) => { + if (!newToken) { + throw new Error('No token selected'); + } + setSelectedChangePaymentToken(newToken); + }, + [setSelectedChangePaymentToken], + ); + + return { + execute, + result, + }; +}; diff --git a/ui/pages/settings/transaction-shield-tab/index.scss b/ui/pages/settings/transaction-shield-tab/index.scss index db74fac1092e..e53fa4141c29 100644 --- a/ui/pages/settings/transaction-shield-tab/index.scss +++ b/ui/pages/settings/transaction-shield-tab/index.scss @@ -48,10 +48,6 @@ color: var(--color-text-default); } - .warning-button:hover { - color: var(--color-warning-default); - } - &__notification-banner { margin-inline: 0; diff --git a/ui/pages/settings/transaction-shield-tab/payment-method-row.tsx b/ui/pages/settings/transaction-shield-tab/payment-method-row.tsx new file mode 100644 index 000000000000..9dd204e2ab4c --- /dev/null +++ b/ui/pages/settings/transaction-shield-tab/payment-method-row.tsx @@ -0,0 +1,336 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { + CRYPTO_PAYMENT_METHOD_ERRORS, + PAYMENT_TYPES, + PaymentType, + PricingResponse, + PRODUCT_TYPES, + Subscription, + SUBSCRIPTION_STATUSES, + SubscriptionCryptoPaymentMethod, +} from '@metamask/subscription-controller'; +import { + Box, + FontWeight, + IconName, + IconSize, + Text, + TextButton, + TextVariant, +} from '@metamask/design-system-react'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + useAvailableTokenBalances, + useSubscriptionProductPlans, + useSubscriptionPaymentMethods, + TokenWithApprovalAmount, +} from '../../../hooks/subscription/useSubscriptionPricing'; +import Tooltip from '../../../components/ui/tooltip'; +import { ShieldPaymentModal } from '../../shield-plan/shield-payment-modal'; +import { + getIsShieldSubscriptionCanChangePaymentMethodToCard, + getIsShieldSubscriptionEndingSoon, +} from '../../../../shared/lib/shield'; +import { isCryptoPaymentMethod } from './types'; + +type PaymentMethodRowProps = { + displayedShieldSubscription?: Subscription; + subscriptionPricing?: PricingResponse; + onPaymentMethodChange: ( + paymentType: PaymentType, + token?: TokenWithApprovalAmount, + ) => void; + showSkeletonLoader: boolean; + isCheckSubscriptionInsufficientFundsDisabled?: boolean; + isPaused?: boolean; + isUnexpectedErrorCryptoPayment?: boolean; + handlePaymentError: () => void; + handlePaymentErrorInsufficientFunds: () => void; +}; + +export const PaymentMethodRow = ({ + displayedShieldSubscription, + subscriptionPricing, + onPaymentMethodChange, + showSkeletonLoader, + isCheckSubscriptionInsufficientFundsDisabled, + isPaused, + isUnexpectedErrorCryptoPayment, + handlePaymentError, + handlePaymentErrorInsufficientFunds, +}: PaymentMethodRowProps) => { + const t = useI18nContext(); + const [showPaymentModal, setShowPaymentModal] = useState(false); + + const canChangePaymentMethodToCard = useMemo(() => { + if (!displayedShieldSubscription) { + return false; + } + return getIsShieldSubscriptionCanChangePaymentMethodToCard( + displayedShieldSubscription, + ); + }, [displayedShieldSubscription]); + + // Derive isCryptoPayment from subscription + const isCryptoPayment = useMemo(() => { + return ( + displayedShieldSubscription?.paymentMethod && + isCryptoPaymentMethod(displayedShieldSubscription.paymentMethod) + ); + }, [displayedShieldSubscription]); + + // Derive isSubscriptionEndingSoon from subscription + const isSubscriptionEndingSoon = useMemo(() => { + if (!displayedShieldSubscription) { + return false; + } + return getIsShieldSubscriptionEndingSoon(displayedShieldSubscription); + }, [displayedShieldSubscription]); + + // Derive cryptoPaymentMethod from subscriptionPricing + const cryptoPaymentMethod = useSubscriptionPaymentMethods( + PAYMENT_TYPES.byCrypto, + subscriptionPricing, + ); + + // Get product price for current subscription interval + const pricingPlans = useSubscriptionProductPlans( + PRODUCT_TYPES.SHIELD, + subscriptionPricing, + ); + + const selectedProductPrice = useMemo(() => { + if (!displayedShieldSubscription || !pricingPlans) { + return undefined; + } + return pricingPlans.find( + (plan) => plan.interval === displayedShieldSubscription.interval, + ); + }, [pricingPlans, displayedShieldSubscription]); + + // Get available token balances + const { availableTokenBalances } = useAvailableTokenBalances({ + paymentChains: cryptoPaymentMethod?.chains, + price: selectedProductPrice, + productType: PRODUCT_TYPES.SHIELD, + }); + // change crypto payment method shouldn't change to current token + const availableTokenBalancesWithoutCurrentToken = useMemo(() => { + if ( + !displayedShieldSubscription || + !isCryptoPaymentMethod(displayedShieldSubscription.paymentMethod) + ) { + return availableTokenBalances; + } + + const cryptoPayment = + displayedShieldSubscription.paymentMethod as SubscriptionCryptoPaymentMethod; + + return availableTokenBalances.filter( + (token) => + token.symbol !== cryptoPayment.crypto.tokenSymbol || + token.chainId.toLowerCase() !== + cryptoPayment.crypto.chainId.toLowerCase(), + ); + }, [availableTokenBalances, displayedShieldSubscription]); + + const hasAvailableToken = + availableTokenBalancesWithoutCurrentToken.length > 0; + + const isInsufficientFundsCrypto = + displayedShieldSubscription && + isCryptoPaymentMethod(displayedShieldSubscription.paymentMethod) && + displayedShieldSubscription.paymentMethod.crypto.error === + CRYPTO_PAYMENT_METHOD_ERRORS.INSUFFICIENT_BALANCE; + + // Compute tokensSupported + const tokensSupported = useMemo(() => { + const chainsAndTokensSupported = cryptoPaymentMethod?.chains ?? []; + return [ + ...new Set( + chainsAndTokensSupported.flatMap((chain) => + chain.tokens.map((token) => token.symbol), + ), + ), + ] as string[]; + }, [cryptoPaymentMethod?.chains]); + + // Derive selectedPaymentMethod from subscription + const selectedPaymentMethod = useMemo(() => { + if (!displayedShieldSubscription) { + return PAYMENT_TYPES.byCard; + } + return displayedShieldSubscription.paymentMethod.type; + }, [displayedShieldSubscription]); + + // Derive selectedToken from subscription or first available + const selectedToken = useMemo(() => { + if (!displayedShieldSubscription) { + return availableTokenBalancesWithoutCurrentToken[0]; + } + + if (isCryptoPaymentMethod(displayedShieldSubscription.paymentMethod)) { + const tokenInfo = ( + displayedShieldSubscription.paymentMethod as SubscriptionCryptoPaymentMethod + ).crypto; + const token = availableTokenBalancesWithoutCurrentToken.find( + (at) => + at.symbol === tokenInfo.tokenSymbol && + at.chainId === tokenInfo.chainId, + ); + return token || availableTokenBalancesWithoutCurrentToken[0]; + } + + return availableTokenBalancesWithoutCurrentToken[0]; + }, [displayedShieldSubscription, availableTokenBalancesWithoutCurrentToken]); + + const handleClose = useCallback(() => { + setShowPaymentModal(false); + }, []); + + const handlePaymentMethodChangeInternal = useCallback( + (newPaymentMethod: PaymentType) => { + // only trigger payment method change if it's card payment + // crypto payment method change will be triggered by handleTokenChange + if (newPaymentMethod === PAYMENT_TYPES.byCard) { + onPaymentMethodChange(newPaymentMethod); + } + }, + [onPaymentMethodChange], + ); + + const handleTokenChange = useCallback( + (token: TokenWithApprovalAmount) => { + onPaymentMethodChange(PAYMENT_TYPES.byCrypto, token); + setShowPaymentModal(false); + }, + [onPaymentMethodChange], + ); + + const paymentMethodDisplay = useMemo(() => { + if (!displayedShieldSubscription) { + return ''; + } + if (isPaused && !isUnexpectedErrorCryptoPayment) { + let tooltipText = ''; + let buttonText = ''; + let buttonDisabled = false; + let buttonOnClick = handlePaymentError; + if (isCryptoPayment) { + tooltipText = 'shieldTxMembershipErrorPausedCryptoTooltip'; + buttonText = 'shieldTxMembershipErrorInsufficientToken'; + if (isInsufficientFundsCrypto) { + buttonOnClick = handlePaymentErrorInsufficientFunds; + // disable button if insufficient funds and not enough token balance to trigger subscription check + if (isCheckSubscriptionInsufficientFundsDisabled) { + buttonDisabled = true; + } + } + } else { + // card payment error case + tooltipText = 'shieldTxMembershipErrorPausedCardTooltip'; + buttonText = 'shieldTxMembershipErrorUpdateCard'; + } + + return ( + + + {t(buttonText, [ + isCryptoPaymentMethod(displayedShieldSubscription?.paymentMethod) + ? displayedShieldSubscription.paymentMethod.crypto.tokenSymbol + : '', + ])} + + + ); + } + if (isSubscriptionEndingSoon && displayedShieldSubscription) { + return ( + + {isCryptoPaymentMethod(displayedShieldSubscription.paymentMethod) + ? displayedShieldSubscription.paymentMethod.crypto.tokenSymbol + : ''} + + ); + } + + if (isCryptoPaymentMethod(displayedShieldSubscription.paymentMethod)) { + return ( + setShowPaymentModal(true)} + endIconName={IconName.ArrowRight} + disabled={ + displayedShieldSubscription.status === + SUBSCRIPTION_STATUSES.canceled || // can't change payment method if subscription is canceled + displayedShieldSubscription.status === + SUBSCRIPTION_STATUSES.provisional // payment method crypto verifying, can't change yet + } + > + {displayedShieldSubscription.paymentMethod.crypto.tokenSymbol} + + ); + } + + return `${displayedShieldSubscription.paymentMethod.card.brand.charAt(0).toUpperCase() + displayedShieldSubscription.paymentMethod.card.brand.slice(1)} - ${displayedShieldSubscription.paymentMethod.card.last4}`; + }, [ + displayedShieldSubscription, + isPaused, + isUnexpectedErrorCryptoPayment, + isCryptoPayment, + isSubscriptionEndingSoon, + t, + handlePaymentError, + isCheckSubscriptionInsufficientFundsDisabled, + handlePaymentErrorInsufficientFunds, + isInsufficientFundsCrypto, + ]); + + if (showSkeletonLoader) { + return ( + + - + + ); + } + + // For error states, return the error button (not clickable for modal) + if ( + (isPaused && !isUnexpectedErrorCryptoPayment) || + isSubscriptionEndingSoon + ) { + return <>{paymentMethodDisplay}; + } + + // For normal states, make it clickable + return ( + <> + {paymentMethodDisplay} + + + ); +}; diff --git a/ui/pages/settings/transaction-shield-tab/transaction-shield.tsx b/ui/pages/settings/transaction-shield-tab/transaction-shield.tsx index dd003bbc957d..25eda4377919 100644 --- a/ui/pages/settings/transaction-shield-tab/transaction-shield.tsx +++ b/ui/pages/settings/transaction-shield-tab/transaction-shield.tsx @@ -3,6 +3,7 @@ import classnames from 'classnames'; import { CRYPTO_PAYMENT_METHOD_ERRORS, PAYMENT_TYPES, + PaymentType, Product, PRODUCT_TYPES, RECURRING_INTERVALS, @@ -11,13 +12,11 @@ import { TokenPaymentInfo, } from '@metamask/subscription-controller'; import { useLocation, useNavigate } from 'react-router-dom-v5-compat'; -import { NameType } from '@metamask/name-controller'; import { BannerAlert, BannerAlertSeverity, Box, BoxProps, - ButtonLink, Icon, IconName, IconSize, @@ -46,6 +45,7 @@ import { useSubscriptionCryptoApprovalTransaction, useUnCancelSubscription, useUpdateSubscriptionCardPaymentMethod, + useUpdateSubscriptionCryptoPaymentMethod, useUserLastSubscriptionByProduct, useUserSubscriptionByProduct, useUserSubscriptions, @@ -58,13 +58,13 @@ import { } from '../../../helpers/constants/routes'; import { TRANSACTION_SHIELD_LINK } from '../../../helpers/constants/common'; import { getProductPrice } from '../../shield-plan/utils'; -import Tooltip from '../../../components/ui/tooltip'; import { useFormatters } from '../../../hooks/useFormatters'; import LoadingScreen from '../../../components/ui/loading-screen'; import AddFundsModal from '../../../components/app/modals/add-funds-modal/add-funds-modal'; import { useSubscriptionPaymentMethods, useSubscriptionPricing, + TokenWithApprovalAmount, } from '../../../hooks/subscription/useSubscriptionPricing'; import { getSubscriptionCryptoApprovalAmount } from '../../../store/actions'; import { ConfirmInfoRowAddress } from '../../../components/app/confirm/info/row'; @@ -76,7 +76,6 @@ import { import { useAsyncResult } from '../../../hooks/useAsync'; import { useTimeout } from '../../../hooks/useTimeout'; import { MINUTE } from '../../../../shared/constants/time'; -import Name from '../../../components/app/name'; import { useHandleShieldAddFundTrigger, useShieldSubscriptionCryptoSufficientBalanceCheck, @@ -97,6 +96,7 @@ import ApiErrorHandler from '../../../components/app/api-error-handler'; import CancelMembershipModal from './cancel-membership-modal'; import { isCardPaymentMethod, isCryptoPaymentMethod } from './types'; import ShieldBannerAnimation from './shield-banner-animation'; +import { PaymentMethodRow } from './payment-method-row'; const TransactionShield = () => { const t = useI18nContext(); @@ -237,24 +237,9 @@ const TransactionShield = () => { newRecurringInterval: currentShieldSubscription?.interval, }); - const hasApiError = - subscriptionsError || - subscriptionPricingError || - cancelSubscriptionResult.error || - unCancelSubscriptionResult.error || - openGetSubscriptionBillingPortalResult.error || - updateSubscriptionCardPaymentMethodResult.error; - const isWaitingForSubscriptionCreation = shouldWaitForSubscriptionCreation && !currentShieldSubscription; - const loading = - resultTriggerSubscriptionCheckInsufficientFunds.pending || - cancelSubscriptionResult.pending || - unCancelSubscriptionResult.pending || - openGetSubscriptionBillingPortalResult.pending || - updateSubscriptionCardPaymentMethodResult.pending; - const showSkeletonLoader = isWaitingForSubscriptionCreation || subscriptionsLoading || @@ -407,7 +392,13 @@ const TransactionShield = () => { return amount; }, [currentToken, displayedShieldSubscription]); - const paymentToken = useMemo(() => { + const paymentToken = useMemo< + | Pick< + TokenWithApprovalAmount, + 'chainId' | 'address' | 'approvalAmount' | 'symbol' + > + | undefined + >(() => { if ( !displayedShieldSubscription || !currentToken || @@ -422,6 +413,7 @@ const TransactionShield = () => { return { chainId: displayedShieldSubscription.paymentMethod.crypto.chainId, address: currentToken.address, + symbol: currentToken.symbol, approvalAmount: { approveAmount: subscriptionCryptoApprovalAmount.approveAmount, chainId: displayedShieldSubscription.paymentMethod.crypto.chainId, @@ -440,6 +432,55 @@ const TransactionShield = () => { const { execute: executeSubscriptionCryptoApprovalTransaction } = useSubscriptionCryptoApprovalTransaction(paymentToken); + const { + execute: executeUpdateSubscriptionCryptoPaymentMethod, + result: updateSubscriptionCryptoPaymentMethodResult, + } = useUpdateSubscriptionCryptoPaymentMethod({ + subscription: currentShieldSubscription, + }); + + const handlePaymentMethodChange = useCallback( + async ( + paymentType: PaymentType, + selectedToken?: TokenWithApprovalAmount, + ) => { + try { + if (paymentType === PAYMENT_TYPES.byCard) { + await executeUpdateSubscriptionCardPaymentMethod(); + } else if (paymentType === PAYMENT_TYPES.byCrypto) { + if (!selectedToken) { + throw new Error('No token selected'); + } + executeUpdateSubscriptionCryptoPaymentMethod(selectedToken); + } + } catch (error) { + console.error('Error changing payment method', error); + } + }, + [ + executeUpdateSubscriptionCardPaymentMethod, + executeUpdateSubscriptionCryptoPaymentMethod, + ], + ); + + const hasApiError = + subscriptionsError || + subscriptionPricingError || + cancelSubscriptionResult.error || + unCancelSubscriptionResult.error || + openGetSubscriptionBillingPortalResult.error || + updateSubscriptionCardPaymentMethodResult.error || + updateSubscriptionCryptoPaymentMethodResult.error || + resultTriggerSubscriptionCheckInsufficientFunds.error; + + const loading = + cancelSubscriptionResult.pending || + unCancelSubscriptionResult.pending || + openGetSubscriptionBillingPortalResult.pending || + updateSubscriptionCardPaymentMethodResult.pending || + updateSubscriptionCryptoPaymentMethodResult.pending || + resultTriggerSubscriptionCheckInsufficientFunds.pending; + const isCardPayment = currentShieldSubscription && isCardPaymentMethod(currentShieldSubscription.paymentMethod); @@ -594,107 +635,6 @@ const TransactionShield = () => { handlePaymentError, ]); - const paymentMethod = useMemo(() => { - if (!displayedShieldSubscription) { - return ''; - } - if (isPaused && !isUnexpectedErrorCryptoPayment) { - let tooltipText = ''; - let buttonText = ''; - let buttonDisabled = false; - let buttonOnClick = handlePaymentError; - if (isCryptoPayment) { - tooltipText = 'shieldTxMembershipErrorPausedCryptoTooltip'; - buttonText = 'shieldTxMembershipErrorInsufficientToken'; - if (isInsufficientFundsCrypto) { - buttonOnClick = handlePaymentErrorInsufficientFunds; - // disable button if insufficient funds and not enough token balance to trigger subscription check - if (!hasAvailableSelectedTokenToTriggerCheckInsufficientFunds) { - buttonDisabled = true; - } - } - } else { - // card payment error case - tooltipText = 'shieldTxMembershipErrorPausedCardTooltip'; - buttonText = 'shieldTxMembershipErrorUpdateCard'; - } - - return ( - - - {t(buttonText, [ - isCryptoPaymentMethod(displayedShieldSubscription?.paymentMethod) - ? displayedShieldSubscription.paymentMethod.crypto.tokenSymbol - : '', - ])} - - - ); - } - if (isSubscriptionEndingSoon && displayedShieldSubscription) { - return ( - - {isCryptoPaymentMethod(displayedShieldSubscription.paymentMethod) - ? displayedShieldSubscription.paymentMethod.crypto.tokenSymbol - : ''} - - ); - } - - if (isCryptoPaymentMethod(displayedShieldSubscription.paymentMethod)) { - const tokenInfo = displayedShieldSubscription.paymentMethod.crypto; - const tokenAddress = cryptoPaymentMethod?.chains - ?.find((chain) => chain.chainId === tokenInfo.chainId) - ?.tokens.find( - (token) => token.symbol === tokenInfo.tokenSymbol, - )?.address; - - if (!tokenAddress) { - return tokenInfo.tokenSymbol; - } - return ( - - ); - } - - return `${displayedShieldSubscription.paymentMethod.card.brand.charAt(0).toUpperCase() + displayedShieldSubscription.paymentMethod.card.brand.slice(1)} - ${displayedShieldSubscription.paymentMethod.card.last4}`; // display card info for card payment method; - }, [ - isPaused, - isUnexpectedErrorCryptoPayment, - displayedShieldSubscription, - isCryptoPayment, - isInsufficientFundsCrypto, - hasAvailableSelectedTokenToTriggerCheckInsufficientFunds, - handlePaymentErrorInsufficientFunds, - isSubscriptionEndingSoon, - t, - handlePaymentError, - cryptoPaymentMethod, - ]); - const handleViewFullBenefitsClicked = useCallback(() => { window.open(TRANSACTION_SHIELD_LINK, '_blank', 'noopener noreferrer'); captureShieldCtaClickedEvent({ @@ -998,7 +938,23 @@ const TransactionShield = () => { )} {billingDetails( t('shieldTxMembershipBillingDetailsPaymentMethod'), - paymentMethod, + , 'shield-detail-payment-method', )} diff --git a/ui/pages/shield-plan/shield-payment-modal.tsx b/ui/pages/shield-plan/shield-payment-modal.tsx index aa1a4d7b1467..909f8cf5008b 100644 --- a/ui/pages/shield-plan/shield-payment-modal.tsx +++ b/ui/pages/shield-plan/shield-payment-modal.tsx @@ -56,6 +56,7 @@ export const ShieldPaymentModal = ({ onAssetChange, hasStableTokenWithBalance, tokensSupported, + disableCardOption, }: { isOpen: boolean; onClose: () => void; @@ -66,6 +67,7 @@ export const ShieldPaymentModal = ({ onAssetChange: (asset: TokenWithApprovalAmount) => void; hasStableTokenWithBalance: boolean; tokensSupported: string[]; + disableCardOption?: boolean; }) => { const t = useI18nContext(); const [showAssetPickerModal, setShowAssetPickerModal] = useState(false); @@ -251,62 +253,67 @@ export const ShieldPaymentModal = ({ )} - selectPaymentMethod(PAYMENT_TYPES.byCard)} - > - {selectedPaymentMethod === PAYMENT_TYPES.byCard && ( - - )} + {!disableCardOption && ( selectPaymentMethod(PAYMENT_TYPES.byCard)} > + {selectedPaymentMethod === PAYMENT_TYPES.byCard && ( + + )} - - - - {t('shieldPlanPayWithCard')} - - - Mastercard - Visa - American Express + + + + + {t('shieldPlanPayWithCard')} + + + Mastercard + Visa + American Express + - + )}