Skip to content

Commit 3474e76

Browse files
authored
Domain-To-Plan Credit: Add plan notice for domain credits (#98792)
1 parent 87c721c commit 3474e76

File tree

8 files changed

+267
-8
lines changed

8 files changed

+267
-8
lines changed

client/blocks/importer/wordpress/upgrade-plan/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { Icon, reusableBlock } from '@wordpress/icons';
1414
import { useTranslate } from 'i18n-calypso';
1515
import React, { useEffect } from 'react';
1616
import useCheckEligibilityMigrationTrialPlan from 'calypso/data/plans/use-check-eligibility-migration-trial-plan';
17-
import PlanNoticeCreditUpgrade from 'calypso/my-sites/plans-features-main/components/plan-notice-credit-update';
17+
import PlanNoticePlanToHigherPlanCredit from 'calypso/my-sites/plans-features-main/components/plan-notice-plan-to-higher-plan-credit';
1818
import useCheckPlanAvailabilityForPurchase from 'calypso/my-sites/plans-features-main/hooks/use-check-plan-availability-for-purchase';
1919
import { useUpgradePlanHostingDetailsList } from './hooks/use-get-upgrade-plan-hosting-details-list';
2020
import { Skeleton } from './skeleton';
@@ -215,7 +215,7 @@ export const UnwrappedUpgradePlan: React.FunctionComponent< UpgradePlanProps > =
215215
</div>
216216
) }
217217

218-
<PlanNoticeCreditUpgrade siteId={ site.ID } visiblePlans={ [ visiblePlan ] } />
218+
<PlanNoticePlanToHigherPlanCredit siteId={ site.ID } visiblePlans={ [ visiblePlan ] } />
219219
<UpgradePlanDetails
220220
planSlugs={ planSlugs }
221221
pricing={ pricing as { [ key: string ]: PricingMetaForGridPlan } }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { formatCurrency } from '@automattic/format-currency';
2+
import { localizeUrl } from '@automattic/i18n-utils';
3+
import { useTranslate } from 'i18n-calypso';
4+
import QuerySitePlans from 'calypso/components/data/query-site-plans';
5+
import InlineSupportLink from 'calypso/components/inline-support-link';
6+
import Notice from 'calypso/components/notice';
7+
import { useDomainToPlanCreditsApplicable } from 'calypso/my-sites/plans-features-main/hooks/use-domain-to-plan-credits-applicable';
8+
import { useSelector } from 'calypso/state';
9+
import { getCurrentUserCurrencyCode } from 'calypso/state/currency-code/selectors';
10+
import type { PlanSlug } from '@automattic/calypso-products';
11+
12+
type Props = {
13+
className?: string;
14+
onDismissClick?: () => void;
15+
siteId: number;
16+
visiblePlans?: PlanSlug[];
17+
};
18+
19+
const PlanNoticeDomainToPlanCredit = ( {
20+
className,
21+
onDismissClick,
22+
siteId,
23+
visiblePlans,
24+
}: Props ) => {
25+
const translate = useTranslate();
26+
const currencyCode = useSelector( getCurrentUserCurrencyCode );
27+
const domainToPlanCreditsApplicable = useDomainToPlanCreditsApplicable( siteId, visiblePlans );
28+
const upgradeCreditDocsUrl = localizeUrl(
29+
'https://wordpress.com/support/manage-purchases/upgrade-your-plan/#upgrade-credit'
30+
);
31+
const showNotice =
32+
visiblePlans &&
33+
visiblePlans.length > 0 &&
34+
domainToPlanCreditsApplicable !== null &&
35+
domainToPlanCreditsApplicable > 0;
36+
37+
return (
38+
<>
39+
<QuerySitePlans siteId={ siteId } />
40+
{ showNotice && (
41+
<Notice
42+
className={ className }
43+
showDismiss={ !! onDismissClick }
44+
onDismissClick={ onDismissClick }
45+
icon="info-outline"
46+
status="is-success"
47+
isReskinned
48+
>
49+
{ translate(
50+
'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!',
51+
{
52+
args: {
53+
amountInCurrency: formatCurrency(
54+
domainToPlanCreditsApplicable,
55+
currencyCode ?? '',
56+
{
57+
isSmallestUnit: true,
58+
}
59+
),
60+
},
61+
components: {
62+
b: <strong />,
63+
a: <InlineSupportLink supportLink={ upgradeCreditDocsUrl } />,
64+
},
65+
}
66+
) }
67+
</Notice>
68+
) }
69+
</>
70+
);
71+
};
72+
73+
export default PlanNoticeDomainToPlanCredit;

client/my-sites/plans-features-main/components/plan-notice-credit-update.tsx client/my-sites/plans-features-main/components/plan-notice-plan-to-higher-plan-credit.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ type Props = {
1616
visiblePlans?: PlanSlug[];
1717
};
1818

19-
const PlanNoticeCreditUpgrade = ( { className, onDismissClick, siteId, visiblePlans }: Props ) => {
19+
const PlanNoticePlanToHigherPlanCredit = ( {
20+
className,
21+
onDismissClick,
22+
siteId,
23+
visiblePlans,
24+
}: Props ) => {
2025
const translate = useTranslate();
2126
const currencyCode = useSelector( getCurrentUserCurrencyCode );
2227

@@ -67,4 +72,4 @@ const PlanNoticeCreditUpgrade = ( { className, onDismissClick, siteId, visiblePl
6772
);
6873
};
6974

70-
export default PlanNoticeCreditUpgrade;
75+
export default PlanNoticePlanToHigherPlanCredit;

client/my-sites/plans-features-main/components/plan-notice.tsx

+22-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isEnabled } from '@automattic/calypso-config';
12
import { PlanSlug, isProPlan, isStarterPlan } from '@automattic/calypso-products';
23
import { Site, SiteMediaStorage } from '@automattic/data-stores';
34
import { useTranslate } from 'i18n-calypso';
@@ -7,12 +8,14 @@ import MarketingMessage from 'calypso/components/marketing-message';
78
import Notice from 'calypso/components/notice';
89
import { getDiscountByName } from 'calypso/lib/discounts';
910
import { ActiveDiscount } from 'calypso/lib/discounts/active-discounts';
11+
import { useDomainToPlanCreditsApplicable } from 'calypso/my-sites/plans-features-main/hooks/use-domain-to-plan-credits-applicable';
1012
import { usePlanUpgradeCreditsApplicable } from 'calypso/my-sites/plans-features-main/hooks/use-plan-upgrade-credits-applicable';
1113
import { useSelector } from 'calypso/state';
1214
import { getByPurchaseId } from 'calypso/state/purchases/selectors';
1315
import { getCurrentPlan, isCurrentUserCurrentPlanOwner } from 'calypso/state/sites/plans/selectors';
1416
import { getSitePlan, isCurrentPlanPaid } from 'calypso/state/sites/selectors';
15-
import PlanNoticeCreditUpgrade from './plan-notice-credit-update';
17+
import PlanNoticeDomainToPlanCredit from './plan-notice-domain-to-plan-credit';
18+
import PlanNoticePlanToHigherPlanCredit from './plan-notice-plan-to-higher-plan-credit';
1619

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

3741
export type PlanNoticeTypes =
3842
| typeof NO_NOTICE
@@ -42,7 +46,8 @@ export type PlanNoticeTypes =
4246
| typeof MARKETING_NOTICE
4347
| typeof PLAN_RETIREMENT_NOTICE
4448
| typeof CURRENT_PLAN_IN_APP_PURCHASE_NOTICE
45-
| typeof PLAN_LEGACY_STORAGE_NOTICE;
49+
| typeof PLAN_LEGACY_STORAGE_NOTICE
50+
| typeof DOMAIN_TO_PLAN_CREDIT_NOTICE;
4651

4752
function useResolveNoticeType(
4853
{
@@ -62,6 +67,7 @@ function useResolveNoticeType(
6267
discountInformation &&
6368
getDiscountByName( discountInformation.coupon, discountInformation.discountEndDate );
6469
const planUpgradeCreditsApplicable = usePlanUpgradeCreditsApplicable( siteId, visiblePlans );
70+
const domainToPlanCreditsApplicable = useDomainToPlanCreditsApplicable( siteId );
6571
const sitePlan = useSelector( ( state ) => getSitePlan( state, siteId ) );
6672
const sitePlanSlug = sitePlan?.product_slug ?? '';
6773
const isCurrentPlanRetired = isProPlan( sitePlanSlug ) || isStarterPlan( sitePlanSlug );
@@ -84,6 +90,8 @@ function useResolveNoticeType(
8490
return ACTIVE_DISCOUNT_NOTICE;
8591
} else if ( planUpgradeCreditsApplicable ) {
8692
return PLAN_UPGRADE_CREDIT_NOTICE;
93+
} else if ( domainToPlanCreditsApplicable ) {
94+
return DOMAIN_TO_PLAN_CREDIT_NOTICE;
8795
}
8896
return MARKETING_NOTICE;
8997
}
@@ -163,7 +171,7 @@ export default function PlanNotice( props: PlanNoticeProps ) {
163171
);
164172
case PLAN_UPGRADE_CREDIT_NOTICE:
165173
return (
166-
<PlanNoticeCreditUpgrade
174+
<PlanNoticePlanToHigherPlanCredit
167175
className="plan-features-main__notice"
168176
onDismissClick={ handleDismissNotice }
169177
siteId={ siteId }
@@ -197,7 +205,17 @@ export default function PlanNotice( props: PlanNoticeProps ) {
197205
) }
198206
></Notice>
199207
);
200-
208+
case DOMAIN_TO_PLAN_CREDIT_NOTICE:
209+
return (
210+
isEnabled( 'domain-to-plan-credit' ) && (
211+
<PlanNoticeDomainToPlanCredit
212+
className="plan-features-main__notice"
213+
onDismissClick={ handleDismissNotice }
214+
siteId={ siteId }
215+
visiblePlans={ visiblePlans }
216+
/>
217+
)
218+
);
201219
case MARKETING_NOTICE:
202220
default:
203221
return <MarketingMessage siteId={ siteId } />;

client/my-sites/plans-features-main/components/test/plan-notice.tsx

+35
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/** @jest-environment jsdom */
22

3+
import { isEnabled } from '@automattic/calypso-config';
34
import {
45
PLAN_BUSINESS,
56
PLAN_PREMIUM,
@@ -15,6 +16,7 @@ import { useMarketingMessage } from 'calypso/components/marketing-message/use-ma
1516
import { getDiscountByName } from 'calypso/lib/discounts';
1617
import { Purchase } from 'calypso/lib/purchases/types';
1718
import PlanNotice from 'calypso/my-sites/plans-features-main/components/plan-notice';
19+
import { useDomainToPlanCreditsApplicable } from 'calypso/my-sites/plans-features-main/hooks/use-domain-to-plan-credits-applicable';
1820
import { usePlanUpgradeCreditsApplicable } from 'calypso/my-sites/plans-features-main/hooks/use-plan-upgrade-credits-applicable';
1921
import { getCurrentUserCurrencyCode } from 'calypso/state/currency-code/selectors';
2022
import { getByPurchaseId } from 'calypso/state/purchases/selectors';
@@ -31,6 +33,7 @@ jest.mock( '@automattic/calypso-products', () => ( {
3133
} ) );
3234
jest.mock( 'calypso/state/purchases/selectors', () => ( {
3335
getByPurchaseId: jest.fn(),
36+
hasPurchasedDomain: jest.fn(),
3437
} ) );
3538
jest.mock( 'calypso/state/sites/plans/selectors', () => ( {
3639
isCurrentUserCurrentPlanOwner: jest.fn(),
@@ -53,12 +56,19 @@ jest.mock(
5356
usePlanUpgradeCreditsApplicable: jest.fn(),
5457
} )
5558
);
59+
jest.mock(
60+
'calypso/my-sites/plans-features-main/hooks/use-domain-to-plan-credits-applicable',
61+
() => ( {
62+
useDomainToPlanCreditsApplicable: jest.fn(),
63+
} )
64+
);
5665
jest.mock( 'calypso/my-sites/plans-features-main/hooks/use-max-plan-upgrade-credits', () => ( {
5766
useMaxPlanUpgradeCredits: jest.fn(),
5867
} ) );
5968
jest.mock( 'calypso/state/currency-code/selectors', () => ( {
6069
getCurrentUserCurrencyCode: jest.fn(),
6170
} ) );
71+
jest.mock( '@automattic/calypso-config' );
6272

6373
const mGetDiscountByName = getDiscountByName as jest.MockedFunction< typeof getDiscountByName >;
6474
const mUseMarketingMessage = useMarketingMessage as jest.MockedFunction<
@@ -74,11 +84,15 @@ const mIsRequestingSitePlans = isRequestingSitePlans as jest.MockedFunction<
7484
const mUsePlanUpgradeCreditsApplicable = usePlanUpgradeCreditsApplicable as jest.MockedFunction<
7585
typeof usePlanUpgradeCreditsApplicable
7686
>;
87+
const mUseDomainToPlanCreditsApplicable = useDomainToPlanCreditsApplicable as jest.MockedFunction<
88+
typeof useDomainToPlanCreditsApplicable
89+
>;
7790
const mGetCurrentUserCurrencyCode = getCurrentUserCurrencyCode as jest.MockedFunction<
7891
typeof getCurrentUserCurrencyCode
7992
>;
8093
const mGetByPurchaseId = getByPurchaseId as jest.MockedFunction< typeof getByPurchaseId >;
8194
const mIsProPlan = isProPlan as jest.MockedFunction< typeof isProPlan >;
95+
const mIsEnabled = isEnabled as jest.MockedFunction< typeof isEnabled >;
8296

8397
const plansList: PlanSlug[] = [
8498
PLAN_FREE,
@@ -105,8 +119,10 @@ describe( '<PlanNotice /> Tests', () => {
105119
mIsRequestingSitePlans.mockImplementation( () => true );
106120
mGetCurrentUserCurrencyCode.mockImplementation( () => 'USD' );
107121
mUsePlanUpgradeCreditsApplicable.mockImplementation( () => 100 );
122+
mUseDomainToPlanCreditsApplicable.mockImplementation( () => 100 );
108123
mGetByPurchaseId.mockImplementation( () => ( { isInAppPurchase: false } ) as Purchase );
109124
mIsProPlan.mockImplementation( () => false );
125+
mIsEnabled.mockImplementation( ( key ) => key !== 'domain-to-plan-credit' );
110126
} );
111127

112128
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', () => {
@@ -164,11 +180,30 @@ describe( '<PlanNotice /> Tests', () => {
164180
);
165181
} );
166182

183+
test( 'A domain-to-plan credit <PlanNotice /> should be shown in a site where a domain has been purchased without a paid plan', () => {
184+
mUsePlanUpgradeCreditsApplicable.mockImplementation( () => null );
185+
mUseDomainToPlanCreditsApplicable.mockImplementation( () => 1000 );
186+
mIsEnabled.mockImplementation( ( key ) => key === 'domain-to-plan-credit' );
187+
188+
renderWithProvider(
189+
<PlanNotice
190+
discountInformation={ { coupon: 'test', discountEndDate: new Date() } }
191+
visiblePlans={ plansList }
192+
isInSignup={ false }
193+
siteId={ 32234 }
194+
/>
195+
);
196+
expect( screen.getByRole( 'status' ).textContent ).toBe(
197+
'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!'
198+
);
199+
} );
200+
167201
test( 'A marketing message <PlanNotice /> when no other notices are available and marketing messages are available and the user is not in signup', () => {
168202
mIsCurrentUserCurrentPlanOwner.mockImplementation( () => true );
169203
mIsCurrentPlanPaid.mockImplementation( () => true );
170204
mGetDiscountByName.mockImplementation( () => false );
171205
mUsePlanUpgradeCreditsApplicable.mockImplementation( () => null );
206+
mUseDomainToPlanCreditsApplicable.mockImplementation( () => null );
172207
mUseMarketingMessage.mockImplementation( () => [
173208
false,
174209
[ { id: '12121', text: 'An important marketing message' } ],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
import { SitePlan, useSitePlans } from '@automattic/data-stores/src/plans';
6+
import { COST_OVERRIDE_REASONS } from '@automattic/data-stores/src/plans/constants';
7+
import { UseQueryResult } from '@tanstack/react-query';
8+
import { useDomainToPlanCreditsApplicable } from 'calypso/my-sites/plans-features-main/hooks/use-domain-to-plan-credits-applicable';
9+
import { useMaxPlanUpgradeCredits } from 'calypso/my-sites/plans-features-main/hooks/use-max-plan-upgrade-credits';
10+
import { hasPurchasedDomain } from 'calypso/state/purchases/selectors/has-purchased-domain';
11+
import { isCurrentPlanPaid } from 'calypso/state/sites/selectors';
12+
import { renderHookWithProvider } from 'calypso/test-helpers/testing-library';
13+
14+
jest.mock( 'calypso/my-sites/plans-features-main/hooks/use-max-plan-upgrade-credits', () => ( {
15+
useMaxPlanUpgradeCredits: jest.fn(),
16+
} ) );
17+
18+
jest.mock( 'calypso/state/purchases/selectors/has-purchased-domain', () => ( {
19+
hasPurchasedDomain: jest.fn(),
20+
} ) );
21+
22+
jest.mock( 'calypso/state/sites/selectors', () => ( {
23+
isCurrentPlanPaid: jest.fn(),
24+
} ) );
25+
26+
jest.mock( '@automattic/data-stores/src/plans/queries/use-site-plans', () => jest.fn() );
27+
28+
const mockUseMaxPlanUpgradeCredits = useMaxPlanUpgradeCredits as jest.MockedFunction<
29+
typeof useMaxPlanUpgradeCredits
30+
>;
31+
const mockHasPurchasedDomain = hasPurchasedDomain as jest.MockedFunction<
32+
typeof hasPurchasedDomain
33+
>;
34+
const mockIsCurrentPlanPaid = isCurrentPlanPaid as jest.MockedFunction< typeof isCurrentPlanPaid >;
35+
const mockUseSitePlans = useSitePlans as jest.MockedFunction< typeof useSitePlans >;
36+
const siteId = 1;
37+
const overrideCode = COST_OVERRIDE_REASONS.RECENT_DOMAIN_PRORATION;
38+
39+
describe( 'useDomainToPlanCreditsApplicable', () => {
40+
beforeEach( () => {
41+
jest.resetAllMocks();
42+
43+
mockUseMaxPlanUpgradeCredits.mockImplementation( () => 1000 );
44+
mockHasPurchasedDomain.mockImplementation( () => true );
45+
mockIsCurrentPlanPaid.mockImplementation( () => false );
46+
mockUseSitePlans.mockImplementation(
47+
() =>
48+
( {
49+
data: { free_plan: { pricing: { costOverrides: [ { overrideCode } ] } } },
50+
} ) as unknown as UseQueryResult< { [ planSlug: string ]: SitePlan } >
51+
);
52+
} );
53+
54+
test( 'Returns the credit value for a site that is eligible (has a domain and is on the free plan)', () => {
55+
const { result } = renderHookWithProvider( () => useDomainToPlanCreditsApplicable( siteId ) );
56+
expect( result.current ).toEqual( 1000 );
57+
} );
58+
59+
test( "Returns null when the site is not eligible because it doesn't have a domain)", () => {
60+
mockHasPurchasedDomain.mockImplementation( () => false );
61+
const { result } = renderHookWithProvider( () => useDomainToPlanCreditsApplicable( siteId ) );
62+
expect( result.current ).toEqual( null );
63+
} );
64+
65+
test( 'Returns null when the site is not eligible because it is on a paid plan', () => {
66+
mockIsCurrentPlanPaid.mockImplementation( () => true );
67+
const { result } = renderHookWithProvider( () => useDomainToPlanCreditsApplicable( siteId ) );
68+
expect( result.current ).toEqual( null );
69+
} );
70+
71+
test( 'Returns null when the site is not eligible because the upgrade credit is not for domain proration', () => {
72+
mockUseSitePlans.mockImplementation(
73+
() =>
74+
( {
75+
data: { free_plan: { pricing: { costOverrides: [] } } },
76+
} ) as unknown as UseQueryResult< { [ planSlug: string ]: SitePlan } >
77+
);
78+
const { result } = renderHookWithProvider( () => useDomainToPlanCreditsApplicable( siteId ) );
79+
expect( result.current ).toEqual( null );
80+
} );
81+
82+
test( 'Returns 0 (rather than null) for for a site that is eligible and has a credit value of 0', () => {
83+
// ie. distinguishes between a site having zero credits, and a site being ineligible for credits (returning null)
84+
mockUseMaxPlanUpgradeCredits.mockImplementation( () => 0 );
85+
const { result } = renderHookWithProvider( () => useDomainToPlanCreditsApplicable( siteId ) );
86+
expect( result.current ).toEqual( 0 );
87+
} );
88+
} );

0 commit comments

Comments
 (0)