Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | 1x 1x 1x 14x 39x 39x 13x 26x 26x 1x 25x 41x 41x 41x 41x 41x 41x 41x 2x 41x 41x 41x 41x 39x 30x 9x 9x 41x 4x 4x 41x 2x 41x 29x 12x 12x 12x 12x 12x 16x | /**
* External dependencies
*/
import { getScriptData, isSimpleSite } from '@automattic/jetpack-script-data';
import {
AnalyticsQueryClientProvider,
useStatsAppNoticeMutation,
useStatsAppNotices,
useStatsAppPlanUsage,
type StatsAppPlanUsage,
} from '@jetpack-premium-analytics/data';
import { Notice } from '@jetpack-premium-analytics/externals';
import { useTrackEvent } from '@jetpack-premium-analytics/widgets-toolkit';
import { useDispatch } from '@wordpress/data';
import { useCallback, useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { getPlanUpgradeUrl } from '../stats-settings/plan-usage';
import styles from './plan-limit-notice.module.scss';
import type { JSX } from 'react';
// The share of the limit at which Stats starts warning, matching Calypso.
const NEAR_LIMIT_USAGE_RATIO = 0.9;
// Stats postpones this notice for a week, and the state is shared with it.
const POSTPONE_SECONDS = 7 * 24 * 60 * 60;
type PlanLimitStatus = {
isOver: boolean;
};
// Once per page load, not per mount: every section tab mounts its own copy.
const viewedThisLoad = new Set< string >();
/**
* Reset the once-per-load view latch. Test-only.
*/
export function resetPlanLimitNoticeForTesting() {
viewedThisLoad.clear();
}
/**
* Where this cycle's views stand against the plan's limit. A missing or zero
* limit means there is nothing to meter against.
*
* @param usage - The plan usage report.
* @return The status once usage reaches 90% of the limit, otherwise `null`.
*/
function getPlanLimitStatus( usage?: StatsAppPlanUsage ): PlanLimitStatus | null {
const limit = usage?.views_limit;
if ( ! limit ) {
return null;
}
const used = usage.current_usage?.views_count ?? 0;
if ( used < limit * NEAR_LIMIT_USAGE_RATIO ) {
return null;
}
return { isOver: used >= limit };
}
/**
* Reads the plan usage and renders the notice when the site nears or passes its limit.
*
* @return The notice, or nothing.
*/
function ConnectedNotice(): JSX.Element | null {
// Stats shows this notice on Jetpack-connected sites only, and VIP sites are exempt from the limit.
const isEligible = ! isSimpleSite() && getScriptData()?.site?.host !== 'vip';
const { data: usage } = useStatsAppPlanUsage( { enabled: isEligible } );
const status = isEligible ? getPlanLimitStatus( usage ) : null;
// Only the near-limit notice can be put off; past the limit it stays for the cycle.
const isNear = !! status && ! status.isOver;
const { data: notices } = useStatsAppNotices( undefined, { enabled: isNear } );
const { createErrorNotice } = useDispatch( 'core/notices' );
const { mutate: updateNotice } = useStatsAppNoticeMutation( {
onError: () =>
createErrorNotice(
__( 'Couldn’t dismiss the notice. Please try again.', 'jetpack-premium-analytics-pkg' ),
{ type: 'snackbar' }
),
} );
const trackEvent = useTrackEvent();
const isHeld = isNear && ( ! notices || notices.tier_upgrade === false );
const statusName = status?.isOver ? 'over' : 'near';
useEffect( () => {
if ( ! status || isHeld || viewedThisLoad.has( statusName ) ) {
return;
}
viewedThisLoad.add( statusName );
trackEvent( 'jetpack_premium_analytics_plan_limit_notice_view', { status: statusName } );
}, [ status, isHeld, statusName, trackEvent ] );
const postpone = useCallback( () => {
updateNotice( { id: 'tier_upgrade', status: 'postponed', postponed_for: POSTPONE_SECONDS } );
trackEvent( 'jetpack_premium_analytics_plan_limit_notice_dismiss' );
}, [ updateNotice, trackEvent ] );
const recordUpgradeClick = useCallback( () => {
trackEvent( 'jetpack_premium_analytics_plan_limit_notice_upgrade_click', {
status: statusName,
} );
}, [ trackEvent, statusName ] );
if ( ! status || isHeld ) {
return null;
}
const upgradeHref = getPlanUpgradeUrl( 'jetpack-premium-analytics-plan-limit-notice' );
const { isOver } = status;
const title = isOver
? __( 'You have reached your views limit', 'jetpack-premium-analytics-pkg' )
: __( 'You are nearing your monthly limit', 'jetpack-premium-analytics-pkg' );
const description = isOver
? __(
'Your access to the traffic dashboard may be disrupted in the future without an upgrade.',
'jetpack-premium-analytics-pkg'
)
: __(
'Your site is receiving more attention and is close to the monthly view limit provided by your current plan. Consider increasing your tier limit to avoid potential service disruptions.',
'jetpack-premium-analytics-pkg'
);
return (
<Notice.Root intent={ isOver ? 'warning' : 'info' } className={ styles.notice }>
<Notice.Title>{ title }</Notice.Title>
<Notice.Description>{ description }</Notice.Description>
{ upgradeHref && (
<Notice.Actions>
<Notice.ActionLink href={ upgradeHref } onClick={ recordUpgradeClick }>
{ __( 'Upgrade now', 'jetpack-premium-analytics-pkg' ) }
</Notice.ActionLink>
</Notice.Actions>
) }
{ ! isOver && (
<Notice.CloseIcon
label={ __( 'Dismiss', 'jetpack-premium-analytics-pkg' ) }
onClick={ postpone }
/>
) }
</Notice.Root>
);
}
/**
* Tells the reader their site is close to, or past, the views its plan allows
* this billing cycle, and offers the upgrade. Brings its own query provider,
* since the dashboard stage has none.
*
* @return The notice, or nothing.
*/
export function PlanLimitNotice(): JSX.Element {
return (
<AnalyticsQueryClientProvider>
<ConnectedNotice />
</AnalyticsQueryClientProvider>
);
}
|