Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Domain-To-Plan Credit: Add plan notice for domain credits #98792

Merged
merged 12 commits into from
Feb 5, 2025
Merged
4 changes: 2 additions & 2 deletions client/blocks/importer/wordpress/upgrade-plan/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Icon, reusableBlock } from '@wordpress/icons';
import { useTranslate } from 'i18n-calypso';
import React, { useEffect } from 'react';
import useCheckEligibilityMigrationTrialPlan from 'calypso/data/plans/use-check-eligibility-migration-trial-plan';
import PlanNoticeCreditUpgrade from 'calypso/my-sites/plans-features-main/components/plan-notice-credit-update';
import PlanNoticePlanToHigherPlanCredit from 'calypso/my-sites/plans-features-main/components/plan-notice-plan-to-higher-plan-credit';
import useCheckPlanAvailabilityForPurchase from 'calypso/my-sites/plans-features-main/hooks/use-check-plan-availability-for-purchase';
import { useUpgradePlanHostingDetailsList } from './hooks/use-get-upgrade-plan-hosting-details-list';
import { Skeleton } from './skeleton';
Expand Down Expand Up @@ -215,7 +215,7 @@ export const UnwrappedUpgradePlan: React.FunctionComponent< UpgradePlanProps > =
</div>
) }

<PlanNoticeCreditUpgrade siteId={ site.ID } visiblePlans={ [ visiblePlan ] } />
<PlanNoticePlanToHigherPlanCredit siteId={ site.ID } visiblePlans={ [ visiblePlan ] } />
<UpgradePlanDetails
planSlugs={ planSlugs }
pricing={ pricing as { [ key: string ]: PricingMetaForGridPlan } }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { formatCurrency } from '@automattic/format-currency';
import { localizeUrl } from '@automattic/i18n-utils';
import { useTranslate } from 'i18n-calypso';
import QuerySitePlans from 'calypso/components/data/query-site-plans';
import InlineSupportLink from 'calypso/components/inline-support-link';
import Notice from 'calypso/components/notice';
import { useDomainToPlanCreditsApplicable } from 'calypso/my-sites/plans-features-main/hooks/use-domain-to-plan-credits-applicable';
import { useSelector } from 'calypso/state';
import { getCurrentUserCurrencyCode } from 'calypso/state/currency-code/selectors';
import type { PlanSlug } from '@automattic/calypso-products';

type Props = {
className?: string;
onDismissClick?: () => void;
siteId: number;
visiblePlans?: PlanSlug[];
};

const PlanNoticeDomainToPlanCredit = ( {
className,
onDismissClick,
siteId,
visiblePlans,
}: Props ) => {
const translate = useTranslate();
const currencyCode = useSelector( getCurrentUserCurrencyCode );
const domainToPlanCreditsApplicable = useDomainToPlanCreditsApplicable( siteId, visiblePlans );
const upgradeCreditDocsUrl = localizeUrl(
'https://wordpress.com/support/manage-purchases/upgrade-your-plan/#upgrade-credit'
);
const showNotice =
visiblePlans &&
visiblePlans.length > 0 &&
domainToPlanCreditsApplicable !== null &&
domainToPlanCreditsApplicable > 0;

return (
<>
<QuerySitePlans siteId={ siteId } />
{ showNotice && (
<Notice
className={ className }
showDismiss={ !! onDismissClick }
onDismissClick={ onDismissClick }
icon="info-outline"
status="is-success"
isReskinned
>
{ translate(
'You have {{b}}%(amountInCurrency)s{{/b}} in {{a}}upgrade credits{{/a}} available from your current domain. This credit will be applied to the pricing below at checkout if you purchase a plan today!',
{
args: {
amountInCurrency: formatCurrency(
domainToPlanCreditsApplicable,
currencyCode ?? '',
{
isSmallestUnit: true,
}
),
},
components: {
b: <strong />,
a: <InlineSupportLink supportLink={ upgradeCreditDocsUrl } />,
},
}
) }
</Notice>
) }
</>
);
};

export default PlanNoticeDomainToPlanCredit;
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ type Props = {
visiblePlans?: PlanSlug[];
};

const PlanNoticeCreditUpgrade = ( { className, onDismissClick, siteId, visiblePlans }: Props ) => {
const PlanNoticePlanToHigherPlanCredit = ( {
className,
onDismissClick,
siteId,
visiblePlans,
}: Props ) => {
const translate = useTranslate();
const currencyCode = useSelector( getCurrentUserCurrencyCode );

Expand Down Expand Up @@ -67,4 +72,4 @@ const PlanNoticeCreditUpgrade = ( { className, onDismissClick, siteId, visiblePl
);
};

export default PlanNoticeCreditUpgrade;
export default PlanNoticePlanToHigherPlanCredit;
26 changes: 22 additions & 4 deletions client/my-sites/plans-features-main/components/plan-notice.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isEnabled } from '@automattic/calypso-config';
import { PlanSlug, isProPlan, isStarterPlan } from '@automattic/calypso-products';
import { Site, SiteMediaStorage } from '@automattic/data-stores';
import { useTranslate } from 'i18n-calypso';
Expand All @@ -7,12 +8,14 @@ import MarketingMessage from 'calypso/components/marketing-message';
import Notice from 'calypso/components/notice';
import { getDiscountByName } from 'calypso/lib/discounts';
import { ActiveDiscount } from 'calypso/lib/discounts/active-discounts';
import { useDomainToPlanCreditsApplicable } from 'calypso/my-sites/plans-features-main/hooks/use-domain-to-plan-credits-applicable';
import { usePlanUpgradeCreditsApplicable } from 'calypso/my-sites/plans-features-main/hooks/use-plan-upgrade-credits-applicable';
import { useSelector } from 'calypso/state';
import { getByPurchaseId } from 'calypso/state/purchases/selectors';
import { getCurrentPlan, isCurrentUserCurrentPlanOwner } from 'calypso/state/sites/plans/selectors';
import { getSitePlan, isCurrentPlanPaid } from 'calypso/state/sites/selectors';
import PlanNoticeCreditUpgrade from './plan-notice-credit-update';
import PlanNoticeDomainToPlanCredit from './plan-notice-domain-to-plan-credit';
import PlanNoticePlanToHigherPlanCredit from './plan-notice-plan-to-higher-plan-credit';

export type PlanNoticeProps = {
siteId: number;
Expand All @@ -33,6 +36,7 @@ const MARKETING_NOTICE = 'marketing-notice';
const PLAN_RETIREMENT_NOTICE = 'plan-retirement-notice';
const CURRENT_PLAN_IN_APP_PURCHASE_NOTICE = 'current-plan-in-app-purchase-notice';
const PLAN_LEGACY_STORAGE_NOTICE = 'plan-legacy-storage-notice';
const DOMAIN_TO_PLAN_CREDIT_NOTICE = 'domain-to-plan-credit-notice';

export type PlanNoticeTypes =
| typeof NO_NOTICE
Expand All @@ -42,7 +46,8 @@ export type PlanNoticeTypes =
| typeof MARKETING_NOTICE
| typeof PLAN_RETIREMENT_NOTICE
| typeof CURRENT_PLAN_IN_APP_PURCHASE_NOTICE
| typeof PLAN_LEGACY_STORAGE_NOTICE;
| typeof PLAN_LEGACY_STORAGE_NOTICE
| typeof DOMAIN_TO_PLAN_CREDIT_NOTICE;

function useResolveNoticeType(
{
Expand All @@ -62,6 +67,7 @@ function useResolveNoticeType(
discountInformation &&
getDiscountByName( discountInformation.coupon, discountInformation.discountEndDate );
const planUpgradeCreditsApplicable = usePlanUpgradeCreditsApplicable( siteId, visiblePlans );
const domainToPlanCreditsApplicable = useDomainToPlanCreditsApplicable( siteId );
const sitePlan = useSelector( ( state ) => getSitePlan( state, siteId ) );
const sitePlanSlug = sitePlan?.product_slug ?? '';
const isCurrentPlanRetired = isProPlan( sitePlanSlug ) || isStarterPlan( sitePlanSlug );
Expand All @@ -84,6 +90,8 @@ function useResolveNoticeType(
return ACTIVE_DISCOUNT_NOTICE;
} else if ( planUpgradeCreditsApplicable ) {
return PLAN_UPGRADE_CREDIT_NOTICE;
} else if ( domainToPlanCreditsApplicable ) {
return DOMAIN_TO_PLAN_CREDIT_NOTICE;
}
return MARKETING_NOTICE;
}
Expand Down Expand Up @@ -163,7 +171,7 @@ export default function PlanNotice( props: PlanNoticeProps ) {
);
case PLAN_UPGRADE_CREDIT_NOTICE:
return (
<PlanNoticeCreditUpgrade
<PlanNoticePlanToHigherPlanCredit
className="plan-features-main__notice"
onDismissClick={ handleDismissNotice }
siteId={ siteId }
Expand Down Expand Up @@ -197,7 +205,17 @@ export default function PlanNotice( props: PlanNoticeProps ) {
) }
></Notice>
);

case DOMAIN_TO_PLAN_CREDIT_NOTICE:
return (
isEnabled( 'domain-to-plan-credit' ) && (
<PlanNoticeDomainToPlanCredit
className="plan-features-main__notice"
onDismissClick={ handleDismissNotice }
siteId={ siteId }
visiblePlans={ visiblePlans }
/>
)
);
case MARKETING_NOTICE:
default:
return <MarketingMessage siteId={ siteId } />;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** @jest-environment jsdom */

import { isEnabled } from '@automattic/calypso-config';
import {
PLAN_BUSINESS,
PLAN_PREMIUM,
Expand All @@ -15,6 +16,7 @@ import { useMarketingMessage } from 'calypso/components/marketing-message/use-ma
import { getDiscountByName } from 'calypso/lib/discounts';
import { Purchase } from 'calypso/lib/purchases/types';
import PlanNotice from 'calypso/my-sites/plans-features-main/components/plan-notice';
import { useDomainToPlanCreditsApplicable } from 'calypso/my-sites/plans-features-main/hooks/use-domain-to-plan-credits-applicable';
import { usePlanUpgradeCreditsApplicable } from 'calypso/my-sites/plans-features-main/hooks/use-plan-upgrade-credits-applicable';
import { getCurrentUserCurrencyCode } from 'calypso/state/currency-code/selectors';
import { getByPurchaseId } from 'calypso/state/purchases/selectors';
Expand All @@ -31,6 +33,7 @@ jest.mock( '@automattic/calypso-products', () => ( {
} ) );
jest.mock( 'calypso/state/purchases/selectors', () => ( {
getByPurchaseId: jest.fn(),
hasPurchasedDomain: jest.fn(),
} ) );
jest.mock( 'calypso/state/sites/plans/selectors', () => ( {
isCurrentUserCurrentPlanOwner: jest.fn(),
Expand All @@ -53,12 +56,19 @@ jest.mock(
usePlanUpgradeCreditsApplicable: jest.fn(),
} )
);
jest.mock(
'calypso/my-sites/plans-features-main/hooks/use-domain-to-plan-credits-applicable',
() => ( {
useDomainToPlanCreditsApplicable: jest.fn(),
} )
);
jest.mock( 'calypso/my-sites/plans-features-main/hooks/use-max-plan-upgrade-credits', () => ( {
useMaxPlanUpgradeCredits: jest.fn(),
} ) );
jest.mock( 'calypso/state/currency-code/selectors', () => ( {
getCurrentUserCurrencyCode: jest.fn(),
} ) );
jest.mock( '@automattic/calypso-config' );

const mGetDiscountByName = getDiscountByName as jest.MockedFunction< typeof getDiscountByName >;
const mUseMarketingMessage = useMarketingMessage as jest.MockedFunction<
Expand All @@ -74,11 +84,15 @@ const mIsRequestingSitePlans = isRequestingSitePlans as jest.MockedFunction<
const mUsePlanUpgradeCreditsApplicable = usePlanUpgradeCreditsApplicable as jest.MockedFunction<
typeof usePlanUpgradeCreditsApplicable
>;
const mUseDomainToPlanCreditsApplicable = useDomainToPlanCreditsApplicable as jest.MockedFunction<
typeof useDomainToPlanCreditsApplicable
>;
const mGetCurrentUserCurrencyCode = getCurrentUserCurrencyCode as jest.MockedFunction<
typeof getCurrentUserCurrencyCode
>;
const mGetByPurchaseId = getByPurchaseId as jest.MockedFunction< typeof getByPurchaseId >;
const mIsProPlan = isProPlan as jest.MockedFunction< typeof isProPlan >;
const mIsEnabled = isEnabled as jest.MockedFunction< typeof isEnabled >;

const plansList: PlanSlug[] = [
PLAN_FREE,
Expand All @@ -105,8 +119,10 @@ describe( '<PlanNotice /> Tests', () => {
mIsRequestingSitePlans.mockImplementation( () => true );
mGetCurrentUserCurrencyCode.mockImplementation( () => 'USD' );
mUsePlanUpgradeCreditsApplicable.mockImplementation( () => 100 );
mUseDomainToPlanCreditsApplicable.mockImplementation( () => 100 );
mGetByPurchaseId.mockImplementation( () => ( { isInAppPurchase: false } ) as Purchase );
mIsProPlan.mockImplementation( () => false );
mIsEnabled.mockImplementation( ( key ) => key !== 'domain-to-plan-credit' );
} );

test( 'A contact site owner <PlanNotice /> should be shown no matter what other conditions are met, when the current site owner is not logged in, and the site plan is paid', () => {
Expand Down Expand Up @@ -164,11 +180,30 @@ describe( '<PlanNotice /> Tests', () => {
);
} );

test( 'A domain-to-plan credit <PlanNotice /> should be shown in a site where a domain has been purchased without a paid plan', () => {
mUsePlanUpgradeCreditsApplicable.mockImplementation( () => null );
mUseDomainToPlanCreditsApplicable.mockImplementation( () => 1000 );
mIsEnabled.mockImplementation( ( key ) => key === 'domain-to-plan-credit' );

renderWithProvider(
<PlanNotice
discountInformation={ { coupon: 'test', discountEndDate: new Date() } }
visiblePlans={ plansList }
isInSignup={ false }
siteId={ 32234 }
/>
);
expect( screen.getByRole( 'status' ).textContent ).toBe(
'You have $10.00 in upgrade credits(opens in a new tab) available from your current domain. This credit will be applied to the pricing below at checkout if you purchase a plan today!'
);
} );

test( 'A marketing message <PlanNotice /> when no other notices are available and marketing messages are available and the user is not in signup', () => {
mIsCurrentUserCurrentPlanOwner.mockImplementation( () => true );
mIsCurrentPlanPaid.mockImplementation( () => true );
mGetDiscountByName.mockImplementation( () => false );
mUsePlanUpgradeCreditsApplicable.mockImplementation( () => null );
mUseDomainToPlanCreditsApplicable.mockImplementation( () => null );
mUseMarketingMessage.mockImplementation( () => [
false,
[ { id: '12121', text: 'An important marketing message' } ],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* @jest-environment jsdom
*/

import { SitePlan, useSitePlans } from '@automattic/data-stores/src/plans';
import { COST_OVERRIDE_REASONS } from '@automattic/data-stores/src/plans/constants';
import { UseQueryResult } from '@tanstack/react-query';
import { useDomainToPlanCreditsApplicable } from 'calypso/my-sites/plans-features-main/hooks/use-domain-to-plan-credits-applicable';
import { useMaxPlanUpgradeCredits } from 'calypso/my-sites/plans-features-main/hooks/use-max-plan-upgrade-credits';
import { hasPurchasedDomain } from 'calypso/state/purchases/selectors/has-purchased-domain';
import { isCurrentPlanPaid } from 'calypso/state/sites/selectors';
import { renderHookWithProvider } from 'calypso/test-helpers/testing-library';

jest.mock( 'calypso/my-sites/plans-features-main/hooks/use-max-plan-upgrade-credits', () => ( {
useMaxPlanUpgradeCredits: jest.fn(),
} ) );

jest.mock( 'calypso/state/purchases/selectors/has-purchased-domain', () => ( {
hasPurchasedDomain: jest.fn(),
} ) );

jest.mock( 'calypso/state/sites/selectors', () => ( {
isCurrentPlanPaid: jest.fn(),
} ) );

jest.mock( '@automattic/data-stores/src/plans/queries/use-site-plans', () => jest.fn() );

const mockUseMaxPlanUpgradeCredits = useMaxPlanUpgradeCredits as jest.MockedFunction<
typeof useMaxPlanUpgradeCredits
>;
const mockHasPurchasedDomain = hasPurchasedDomain as jest.MockedFunction<
typeof hasPurchasedDomain
>;
const mockIsCurrentPlanPaid = isCurrentPlanPaid as jest.MockedFunction< typeof isCurrentPlanPaid >;
const mockUseSitePlans = useSitePlans as jest.MockedFunction< typeof useSitePlans >;
const siteId = 1;
const overrideCode = COST_OVERRIDE_REASONS.RECENT_DOMAIN_PRORATION;

describe( 'useDomainToPlanCreditsApplicable', () => {
beforeEach( () => {
jest.resetAllMocks();

mockUseMaxPlanUpgradeCredits.mockImplementation( () => 1000 );
mockHasPurchasedDomain.mockImplementation( () => true );
mockIsCurrentPlanPaid.mockImplementation( () => false );
mockUseSitePlans.mockImplementation(
() =>
( {
data: { free_plan: { pricing: { costOverrides: [ { overrideCode } ] } } },
} ) as unknown as UseQueryResult< { [ planSlug: string ]: SitePlan } >
);
} );

test( 'Returns the credit value for a site that is eligible (has a domain and is on the free plan)', () => {
const { result } = renderHookWithProvider( () => useDomainToPlanCreditsApplicable( siteId ) );
expect( result.current ).toEqual( 1000 );
} );

test( "Returns null when the site is not eligible because it doesn't have a domain)", () => {
mockHasPurchasedDomain.mockImplementation( () => false );
const { result } = renderHookWithProvider( () => useDomainToPlanCreditsApplicable( siteId ) );
expect( result.current ).toEqual( null );
} );

test( 'Returns null when the site is not eligible because it is on a paid plan', () => {
mockIsCurrentPlanPaid.mockImplementation( () => true );
const { result } = renderHookWithProvider( () => useDomainToPlanCreditsApplicable( siteId ) );
expect( result.current ).toEqual( null );
} );

test( 'Returns null when the site is not eligible because the upgrade credit is not for domain proration', () => {
mockUseSitePlans.mockImplementation(
() =>
( {
data: { free_plan: { pricing: { costOverrides: [] } } },
} ) as unknown as UseQueryResult< { [ planSlug: string ]: SitePlan } >
);
const { result } = renderHookWithProvider( () => useDomainToPlanCreditsApplicable( siteId ) );
expect( result.current ).toEqual( null );
} );

test( 'Returns 0 (rather than null) for for a site that is eligible and has a credit value of 0', () => {
// ie. distinguishes between a site having zero credits, and a site being ineligible for credits (returning null)
mockUseMaxPlanUpgradeCredits.mockImplementation( () => 0 );
const { result } = renderHookWithProvider( () => useDomainToPlanCreditsApplicable( siteId ) );
expect( result.current ).toEqual( 0 );
} );
} );
Loading