Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { isEnabled } from '@automattic/calypso-config';
import { HelpCenter } from '@automattic/data-stores';
import { Button } from '@wordpress/components';
import { useDispatch as useDataStoreDispatch } from '@wordpress/data';
import { useTranslate } from 'i18n-calypso';
import { useCallback, useEffect, type MouseEvent } from 'react';
import { CONTACT_URL_HASH_FRAGMENT } from 'calypso/a8c-for-agencies/components/a4a-contact-support-widget';
import { useCallback, useEffect, type ReactNode } from 'react';
import LayoutBanner from 'calypso/a8c-for-agencies/components/layout/banner';
import {
A4A_PAYMENT_METHODS_LINK,
Expand All @@ -16,7 +13,7 @@ import usePaymentRiskNotice from './use-payment-risk-notice';

import './style.scss';

const HELP_CENTER_STORE = HelpCenter.register();
const isExternalUrl = ( url: string ) => /^https?:\/\//.test( url );

type PaymentRiskNoticeBannerProps = {
isFullWidth?: boolean;
Expand All @@ -29,7 +26,6 @@ export default function PaymentRiskNoticeBanner( {
}: PaymentRiskNoticeBannerProps ) {
const translate = useTranslate();
const dispatch = useDispatch();
const { setShowHelpCenter, setNavigateToRoute } = useDataStoreDispatch( HELP_CENTER_STORE );
const paymentNotice = usePaymentRiskNotice();
const noticeState = paymentNotice?.state;
const noticeSeverity = paymentNotice?.severity;
Expand All @@ -49,70 +45,72 @@ export default function PaymentRiskNoticeBanner( {
}
}, [ dispatch, noticeState, noticeSeverity, source ] );

const onCtaClick = useCallback( () => {
if ( ! noticeState || ! noticeSeverity ) {
return;
}

dispatch(
recordTracksEvent( 'calypso_a4a_payment_risk_notice_banner_cta_click', {
source,
state: noticeState,
severity: noticeSeverity,
} )
);
}, [ dispatch, noticeState, noticeSeverity, source ] );

const onContactUsClick = useCallback(
( event: MouseEvent< HTMLAnchorElement > ) => {
event.preventDefault();

const onActionClick = useCallback(
( action: 'primary' | 'secondary' ) => {
if ( ! noticeState || ! noticeSeverity ) {
return;
}

setShowHelpCenter( true );
setNavigateToRoute( '/contact-form' );
dispatch(
recordTracksEvent( 'calypso_a4a_payment_risk_notice_banner_contact_us_click', {
recordTracksEvent( 'calypso_a4a_payment_risk_notice_banner_cta_click', {
action,
source,
state: noticeState,
severity: noticeSeverity,
} )
);
},
[ dispatch, noticeState, noticeSeverity, setNavigateToRoute, setShowHelpCenter, source ]
[ dispatch, noticeState, noticeSeverity, source ]
);

if ( ! paymentNotice || ! noticeState || ! noticeSeverity ) {
return null;
}

const fixPaymentMethodCta = (
<Button
key="update-payment-method"
variant="primary"
href={ ctaUrl }
onClick={ onCtaClick }
target="_blank"
rel="noopener noreferrer"
__next40pxDefaultSize
>
{ translate( 'Fix payment method' ) }
</Button>
);
const canManagePaymentMethod = paymentNotice.can_current_user_manage_payment_method !== false;
const primaryActionLabel =
paymentNotice.primary_action_label ??
paymentNotice.action_label ??
( canManagePaymentMethod ? translate( 'Fix payment method' ) : undefined );
const primaryActionUrl =
paymentNotice.primary_action_url ??
paymentNotice.action_url ??
( canManagePaymentMethod ? ctaUrl : undefined );
const actions: ReactNode[] = [];

const contactUsCta = (
<Button
key="contact-us"
variant="secondary"
href={ CONTACT_URL_HASH_FRAGMENT }
onClick={ onContactUsClick }
__next40pxDefaultSize
>
{ translate( 'Contact us' ) }
</Button>
);
if ( primaryActionLabel && primaryActionUrl ) {
actions.push(
<Button
key="primary-action"
variant="primary"
href={ primaryActionUrl }
onClick={ () => onActionClick( 'primary' ) }
target={ isExternalUrl( primaryActionUrl ) ? '_blank' : undefined }
rel={ isExternalUrl( primaryActionUrl ) ? 'noopener noreferrer' : undefined }
__next40pxDefaultSize
>
{ primaryActionLabel }
</Button>
);
}

if ( paymentNotice.secondary_action_label && paymentNotice.secondary_action_url ) {
actions.push(
<Button
key="secondary-action"
variant="secondary"
href={ paymentNotice.secondary_action_url }
onClick={ () => onActionClick( 'secondary' ) }
target={ isExternalUrl( paymentNotice.secondary_action_url ) ? '_blank' : undefined }
rel={
isExternalUrl( paymentNotice.secondary_action_url ) ? 'noopener noreferrer' : undefined
}
__next40pxDefaultSize
>
{ paymentNotice.secondary_action_label }
</Button>
);
}

return (
<LayoutBanner
Expand All @@ -123,7 +121,7 @@ export default function PaymentRiskNoticeBanner( {
paymentNotice.title ??
translate( 'Action required: We’re unable to renew your subscription(s)' )
}
actions={ [ fixPaymentMethodCta, contactUsCta ] }
actions={ actions }
hideCloseButton
allowTemporaryDismissal
preferenceName="a4a-payment-risk-notice-banner-temporary-dismissed"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,13 @@ import PaymentRiskNoticeBanner from '..';
import type { PaymentNotice } from 'calypso/state/a8c-for-agencies/types';
import type { ReactNode } from 'react';

const mockSetShowHelpCenter = jest.fn();
const mockSetNavigateToRoute = jest.fn();
const mockDispatch = jest.fn();
let mockPaymentNotice: PaymentNotice | null = null;

jest.mock( '@automattic/calypso-config', () => ( {
isEnabled: jest.fn(),
} ) );

jest.mock( '@automattic/data-stores', () => ( {
HelpCenter: {
register: jest.fn( () => 'help-center-store' ),
},
} ) );

jest.mock( '@wordpress/data', () => ( {
useDispatch: () => ( {
setShowHelpCenter: mockSetShowHelpCenter,
setNavigateToRoute: mockSetNavigateToRoute,
} ),
} ) );

jest.mock( 'i18n-calypso', () => ( {
useTranslate: () => ( text: string ) => text,
} ) );
Expand Down Expand Up @@ -61,10 +46,6 @@ jest.mock( '@wordpress/components', () => ( {
),
} ) );

jest.mock( 'calypso/a8c-for-agencies/components/a4a-contact-support-widget', () => ( {
CONTACT_URL_HASH_FRAGMENT: '#contact-support',
} ) );

jest.mock( 'calypso/a8c-for-agencies/components/layout/banner', () => ( {
__esModule: true,
default: ( {
Expand Down Expand Up @@ -122,6 +103,8 @@ describe( 'PaymentRiskNoticeBanner', () => {
title: 'Action required: We are unable to renew your subscription(s)',
content:
'We could not process payment for one or more of your subscriptions with the payment method we have on file.',
primary_action_label: 'Update your payment method',
primary_action_url: 'https://wordpress.com/me/billing/purchases',
};
mockedIsEnabled.mockImplementation( ( flag ) => flag === 'a4a-payment-risk-notice-banner' );
} );
Expand Down Expand Up @@ -168,6 +151,11 @@ describe( 'PaymentRiskNoticeBanner', () => {
title: 'Action required: Update your payment method',
content:
'The payment method we have on file is expiring soon. Please update it to avoid interruption to your subscriptions and sites.',
primary_action_label: 'Update your payment method',
primary_action_url: 'https://wordpress.com/me/billing/purchases',
secondary_action_label: 'View billing FAQ',
secondary_action_url:
'https://agencieshelp.automattic.com/knowledge-base/add-or-update-a-payment-method/',
};

render( <PaymentRiskNoticeBanner source="overview" /> );
Expand All @@ -189,25 +177,80 @@ describe( 'PaymentRiskNoticeBanner', () => {
{ source: 'overview', state: 'card_expiry', severity: 'warning' }
);

const fixPaymentMethodButton = screen.getByRole( 'button', { name: 'Fix payment method' } );
const updatePaymentMethodButton = screen.getByRole( 'button', {
name: 'Update your payment method',
} );

expect( fixPaymentMethodButton ).toHaveAttribute( 'data-target', '_blank' );
expect( fixPaymentMethodButton ).toHaveAttribute( 'data-rel', 'noopener noreferrer' );
expect( updatePaymentMethodButton ).toHaveAttribute(
'data-href',
'https://wordpress.com/me/billing/purchases'
);
expect( updatePaymentMethodButton ).toHaveAttribute( 'data-target', '_blank' );
expect( updatePaymentMethodButton ).toHaveAttribute( 'data-rel', 'noopener noreferrer' );

await user.click( fixPaymentMethodButton );
await user.click( updatePaymentMethodButton );

expect( mockedRecordTracksEvent ).toHaveBeenCalledWith(
'calypso_a4a_payment_risk_notice_banner_cta_click',
{ source: 'overview', state: 'card_expiry', severity: 'warning' }
{ action: 'primary', source: 'overview', state: 'card_expiry', severity: 'warning' }
);

await user.click( screen.getByRole( 'button', { name: 'Contact us' } ) );
const viewBillingFaqButton = screen.getByRole( 'button', { name: 'View billing FAQ' } );

expect( viewBillingFaqButton ).toHaveAttribute(
'data-href',
'https://agencieshelp.automattic.com/knowledge-base/add-or-update-a-payment-method/'
);
expect( viewBillingFaqButton ).toHaveAttribute( 'data-target', '_blank' );
expect( viewBillingFaqButton ).toHaveAttribute( 'data-rel', 'noopener noreferrer' );

await user.click( viewBillingFaqButton );

expect( mockedRecordTracksEvent ).toHaveBeenCalledWith(
'calypso_a4a_payment_risk_notice_banner_contact_us_click',
{ source: 'overview', state: 'card_expiry', severity: 'warning' }
'calypso_a4a_payment_risk_notice_banner_cta_click',
{ action: 'secondary', source: 'overview', state: 'card_expiry', severity: 'warning' }
);
} );

it( 'renders a referral notice with only the client instructions CTA', () => {
mockPaymentNotice = {
state: 'renewal_failure',
severity: 'error',
title: 'Action required: Client payment failed',
content:
'We couldn’t process payment for Jane Doe’s Pressable plan. Please ask them to update their payment method.',
primary_action_label: 'Send your client update instructions',
primary_action_url:
'https://agencieshelp.automattic.com/knowledge-base/update-the-payment-method-for-your-referral-purchase/',
can_current_user_manage_payment_method: false,
};

render( <PaymentRiskNoticeBanner source="purchases_billing" /> );

expect(
screen.getByRole( 'button', { name: 'Send your client update instructions' } )
).toHaveAttribute(
'data-href',
'https://agencieshelp.automattic.com/knowledge-base/update-the-payment-method-for-your-referral-purchase/'
);
expect( mockSetShowHelpCenter ).toHaveBeenCalledWith( true );
expect( mockSetNavigateToRoute ).toHaveBeenCalledWith( '/contact-form' );
expect(
screen.queryByRole( 'button', { name: 'Fix payment method' } )
).not.toBeInTheDocument();
expect( screen.queryByRole( 'button', { name: 'Contact us' } ) ).not.toBeInTheDocument();
} );

it( 'does not render a fallback CTA for users who cannot manage payment methods', () => {
mockPaymentNotice = {
state: 'renewal_failure',
severity: 'error',
title: 'Action required: Client payment failed',
content:
'We couldn’t process payment for Jane Doe’s Pressable plan. Please ask them to update their payment method.',
can_current_user_manage_payment_method: false,
};

render( <PaymentRiskNoticeBanner source="purchases_billing" /> );

expect( screen.queryByRole( 'button' ) ).not.toBeInTheDocument();
} );
} );
4 changes: 4 additions & 0 deletions client/state/a8c-for-agencies/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export interface PaymentNotice {
content?: string;
action_label?: string;
action_url?: string;
primary_action_label?: string;
primary_action_url?: string;
secondary_action_label?: string;
secondary_action_url?: string;
failed_at?: string;
grace_period_ends_at?: string;
affected_subscription_ids?: number[];
Expand Down
Loading