PluginProbe ʕ •ᴥ•ʔ
Advanced Ads – Ad Manager & AdSense / 2.0.26
Advanced Ads – Ad Manager & AdSense v2.0.26
2.0.26 2.0.25 2.0.24 2.0.23 2.0.22 2.0.21 1.38.0 1.39.0 1.39.1 1.39.2 1.39.3 1.39.4 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.40.0 1.40.1 1.40.2 1.41.0 1.42.0 1.42.1 1.42.2 1.43.0 1.44.0 1.44.1 1.45.0 1.45.1 1.46.0 1.47.0 1.47.1 1.47.2 1.47.3 1.47.4 1.47.5 1.48.0 1.48.1 1.49.0 1.5.0 1.5.0.1 1.5.1 1.5.2 1.5.2.1 1.5.4 1.5.4.1 1.5.5 1.50.0 1.51.0 1.51.1 1.51.2 1.51.3 1.52.0 1.52.1 1.52.2 1.52.3 1.52.4 1.53.0 1.53.1 1.53.2 1.54.0 1.54.1 1.55.0 1.56.0 1.56.1 1.56.2 1.56.3 1.56.4 1.6 1.6.1 1.6.10 1.6.10.1 1.6.10.2 1.6.11 1.6.11.1 1.6.12 1.6.13 1.6.14 1.6.15 1.6.16 1.6.17 1.6.17.1 1.6.17.2 1.6.2 1.6.2.1 1.6.3 1.6.4 1.6.4.1 1.6.5 1.6.6 1.6.6.1 1.6.7 1.6.7.1 1.6.8 1.6.8.1 1.6.8.2 1.6.8.3 1.6.9 1.6.9.1 1.6.9.2 1.6.9.3 1.6.9.4 1.7 1.7.0.1 1.7.0.2 1.7.0.3 1.7.1 1.7.1.1 1.7.1.2 1.7.1.3 1.7.1.4 1.7.1.5 1.7.10 trunk 1.7.11 1.0.1 1.7.12 1.0.2 1.7.13 1.0.3 1.7.14 1.1.0 1.7.15 1.1.1 1.7.16 1.1.2 1.7.17 1.1.3 1.7.18 1.10 1.7.19 1.10.1 1.7.2 1.10.10 1.7.2.1 1.10.11 1.7.20 1.10.12 1.7.21 1.10.2 1.7.22 1.10.3 1.7.23 1.10.4 1.7.24 1.10.5 1.7.25 1.10.6 1.7.3 1.10.7 1.7.4 1.10.8 1.7.4.1 1.10.9 1.7.4.2 1.11 1.7.4.3 1.11.1 1.7.4.4 1.11.2 1.7.4.5 1.12 1.7.5 1.13 1.7.5.1 1.13.1 1.7.6 1.13.2 1.7.7 1.13.3 1.7.8 1.13.4 1.7.9 1.13.5 1.7.9.1 1.13.6 1.7.9.2 1.13.7 1.7.9.3 1.13.8 1.8 1.14 1.8.1 1.14.1 1.8.10 1.14.10 1.8.11 1.14.11 1.8.12 1.14.2 1.8.13 1.14.3 1.8.14 1.14.4 1.8.15 1.14.5 1.8.16 1.14.6 1.8.17 1.14.7 1.8.18 1.14.8 1.8.19 1.14.9 1.8.2 1.15 1.8.20 1.16 1.8.21 1.16.1 1.8.22 1.17 1.8.23 1.17.1 1.8.24 1.17.10 1.8.25 1.17.10-rc.1 1.8.26 1.17.11 1.8.27 1.17.12 1.8.28 1.17.12-rc.1 1.8.29 1.17.2 1.8.3 1.17.3 1.8.30 1.17.4 1.8.4 1.17.5 1.8.5 1.17.6 1.8.6 1.17.7 1.8.7 1.17.8 1.8.8 1.17.9 1.8.9 1.17.9-beta.1 1.9 1.18.0 2.0.0 1.19.0 2.0.1 1.19.1 2.0.10 1.2 2.0.11 1.2.1 2.0.12 1.2.2 2.0.13 1.2.3 2.0.14 1.2.4 2.0.15 1.2.5 2.0.16 1.2.6 2.0.17 1.2.7 2.0.18 1.20.0 2.0.19 1.20.0-rc.1 2.0.2 1.20.0-rc.2 2.0.20 1.20.1 2.0.3 1.20.2 2.0.4 1.20.3 2.0.5 1.21.0 2.0.6 1.21.1 2.0.7 1.22.0 2.0.8 1.22.1 2.0.9 1.22.2 1.23.0 1.23.1 1.23.2 1.24.0 1.24.1 1.24.2 1.25.0 1.25.1 1.26.0 1.27.0 1.28.0 1.29.0 1.29.1 1.3 1.3.1 1.3.10 1.3.11 1.3.12 1.3.13 1.3.14 1.3.15 1.3.16 1.3.17 1.3.18 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.30.0 1.30.1 1.30.2 1.30.2-rc.1 1.30.3 1.30.4 1.30.4-rc.1 1.30.5 1.31.0 1.31.1 1.32.0 1.32.0-rc.1 1.33.0 1.33.1 1.33.2 1.34.0 1.35.0 1.35.1 1.36.0 1.36.1 1.36.2 1.36.3 1.37.0 1.37.1 1.37.2
advanced-ads / src / admin / screen-licenses / utils.js
advanced-ads / src / admin / screen-licenses Last commit date
components 4 weeks ago hooks 4 weeks ago License.jsx 4 weeks ago addon-catalog.js 1 month ago route.jsx 4 weeks ago utils.js 4 weeks ago
utils.js
1683 lines
1 /**
2 * External Dependencies
3 */
4 import apiFetch from '@wordpress/api-fetch';
5 import { __, sprintf } from '@wordpress/i18n';
6
7 import { ALL_ACCESS_ADDON_IDS, LICENSE_ADDON_CATALOG } from './addon-catalog';
8 import { endpoints } from '@advancedAds';
9
10 export const LICENSE_PATH = '/license';
11
12 let licensesApiFetchConfigured = false;
13
14 /**
15 * Configure apiFetch for any WordPress permalink structure (Plain, Post name, etc.).
16 *
17 * Uses rest_url() from PHP when wpApiSettings is missing or incomplete.
18 */
19 function configureLicensesApiFetch() {
20 if ( licensesApiFetchConfigured ) {
21 return;
22 }
23
24 const restRoot = window.wpApiSettings?.root || endpoints?.restUrl;
25 const restNonce = window.wpApiSettings?.nonce || endpoints?.restNonce;
26
27 if ( restRoot && typeof apiFetch.createRootURLMiddleware === 'function' ) {
28 apiFetch.use( apiFetch.createRootURLMiddleware( restRoot ) );
29 }
30
31 if ( restNonce && typeof apiFetch.createNonceMiddleware === 'function' ) {
32 apiFetch.use( apiFetch.createNonceMiddleware( restNonce ) );
33 }
34
35 licensesApiFetchConfigured = true;
36 }
37
38 /**
39 * Permalink-safe REST request for licenses-screen endpoints only.
40 *
41 * @param {string} path REST route path (e.g. /advanced-ads/v1/licenses).
42 * @param {Object} options apiFetch options without path/url.
43 * @return {Promise<*>} apiFetch response.
44 */
45 export function licensesApiRequest( path, options = {} ) {
46 configureLicensesApiFetch();
47 return apiFetch( { path, ...options } );
48 }
49
50 const MS_PER_DAY = 24 * 60 * 60 * 1000;
51
52 /** @type {Array<[string, string[]]>} Addon id => normalized product name labels (PHP manifest + shop aliases). */
53 const LICENSE_PRODUCT_ADDON_LABELS = [
54 [ 'pro', [ 'pro' ] ],
55 [ 'tracking', [ 'tracking' ] ],
56 [ 'responsive', [ 'responsive', 'amp ads' ] ],
57 [ 'gam', [ 'gam', 'google ad manager integration' ] ],
58 [ 'layer', [ 'layer', 'popup and layer ads' ] ],
59 [ 'selling', [ 'selling', 'selling ads' ] ],
60 [ 'sticky', [ 'sticky', 'sticky ads' ] ],
61 ];
62
63 function matchesLicenseProductLabel( normalized, label ) {
64 return (
65 normalized === label ||
66 normalized === `advanced ads ${ label }` ||
67 normalized.endsWith( ` ${ label }` )
68 );
69 }
70
71 function normalizeLicenseStatus( status ) {
72 return String( status ?? '' ).toLowerCase();
73 }
74
75 function toLicenseKey( licenseKey ) {
76 return String( licenseKey ?? '' );
77 }
78
79 function getLicenseKeyFromRow( license ) {
80 return toLicenseKey( license?.licenseKey );
81 }
82
83 function getAddonMapKey( appliedAddonKeyMap, addonId ) {
84 return String( appliedAddonKeyMap?.[ addonId ] ?? '' );
85 }
86
87 function hasFutureExpiry( expiryDate ) {
88 const parsed = parseLicenseExpiryDate( expiryDate );
89 return parsed !== null && parsed.getTime() > Date.now();
90 }
91
92 export function getAllAccessSiteContext(
93 allLicenses,
94 currentHostname,
95 appliedAddonKeyMap
96 ) {
97 return {
98 aaOnSite: isAllAccessActiveOnThisSite( allLicenses, currentHostname ),
99 aaGoverns: isAllAccessGoverningAppliedMap(
100 appliedAddonKeyMap,
101 allLicenses
102 ),
103 };
104 }
105
106 function buildShopSsoUrl( intent, params = {} ) {
107 const url = new URL( `${ endpoints.shopUrl }/sso-login` );
108 url.searchParams.set( 'site', endpoints.siteUrl );
109 url.searchParams.set( 'intent', intent );
110
111 for ( const [ key, value ] of Object.entries( params ) ) {
112 if ( value === undefined || value === null || value === '' ) {
113 continue;
114 }
115 if ( key === 'pricing_id' && Number( value ) <= 0 ) {
116 continue;
117 }
118 url.searchParams.set( key, String( value ) );
119 }
120
121 return url.toString();
122 }
123
124 /**
125 * Hostname from a site URL (no scheme or path).
126 *
127 * @param {string} [maybeUrl] Full or partial site URL.
128 * @return {string} Hostname, or empty string.
129 */
130 export function getHostnameFromUrl( maybeUrl ) {
131 if ( ! maybeUrl || typeof maybeUrl !== 'string' ) {
132 return '';
133 }
134
135 try {
136 return new URL( maybeUrl ).hostname;
137 } catch {
138 return maybeUrl
139 .replace( /^https?:\/\//i, '' )
140 .replace( /\/.*$/, '' )
141 .replace( /\/$/, '' );
142 }
143 }
144
145 /**
146 * Whether the license status allows activation (active/valid only).
147 *
148 * @param {string} [status] License status from API.
149 * @return {boolean} True when the license is active or valid.
150 */
151 export function isRichLicenseActive( status ) {
152 const normalized = normalizeLicenseStatus( status );
153
154 return normalized === 'active' || normalized === 'valid';
155 }
156
157 /**
158 * Parse shop expiry strings (DD-MM-YYYY or ISO) for entitlement checks.
159 *
160 * @param {string} [raw] Expiry from API.
161 * @return {Date|null} Parsed date or null when invalid.
162 */
163 export function parseLicenseExpiryDate( raw ) {
164 if ( ! raw ) {
165 return null;
166 }
167
168 const value = String( raw ).trim();
169 const dmy = /^(\d{1,2})-(\d{1,2})-(\d{4})$/.exec( value );
170 if ( dmy ) {
171 const parsed = new Date(
172 Number( dmy[ 3 ] ),
173 Number( dmy[ 2 ] ) - 1,
174 Number( dmy[ 1 ] )
175 );
176 return Number.isNaN( parsed.getTime() ) ? null : parsed;
177 }
178
179 const parsed = new Date( value );
180 return Number.isNaN( parsed.getTime() ) ? null : parsed;
181 }
182
183 /**
184 * Expiry date as Unix milliseconds.
185 *
186 * @param {string} [raw] Expiry from API.
187 * @return {number} Timestamp in ms, or 0 when invalid.
188 */
189 export function getLicenseExpiryTimestamp( raw ) {
190 const parsed = parseLicenseExpiryDate( raw );
191 return parsed ? parsed.getTime() : 0;
192 }
193
194 /**
195 * Whether expiryDate is in the past.
196 *
197 * @param {string} [expiryDate] Expiry from API.
198 * @return {boolean} True when the expiry date is in the past.
199 */
200 export function isLicenseDateExpired( expiryDate ) {
201 const ts = getLicenseExpiryTimestamp( expiryDate );
202 return ts > 0 && ts <= Date.now();
203 }
204
205 /**
206 * Whether the license should display as expired (shop status or past date).
207 *
208 * @param {string} [status] License status from API.
209 * @param {string} [expiryDate] Expiry from API.
210 * @return {boolean} True when shop status or expiry date indicates expired.
211 */
212 export function isLicenseExpiredForDisplay( status, expiryDate ) {
213 if ( isShopLicenseExpired( status ) ) {
214 return true;
215 }
216
217 return isLicenseDateExpired( expiryDate );
218 }
219
220 /**
221 * Whether the license expires within the given number of days.
222 *
223 * @param {string} [expiryDate] Expiry from API.
224 * @param {number} [days] Day threshold.
225 * @return {boolean} True when the license expires within the threshold.
226 */
227 export function isLicenseExpiringSoon( expiryDate, days = 30 ) {
228 const ts = getLicenseExpiryTimestamp( expiryDate );
229 const now = Date.now();
230 if ( ! ts || ts <= now ) {
231 return false;
232 }
233
234 return ts - now <= days * MS_PER_DAY;
235 }
236
237 /**
238 * Whole days until expiry (0 when expired or invalid).
239 *
240 * @param {string} [expiryDate] Expiry from API.
241 * @return {number} Whole days until expiry, or 0 when expired or invalid.
242 */
243 export function getDaysUntilLicenseExpiry( expiryDate ) {
244 const ts = getLicenseExpiryTimestamp( expiryDate );
245 const now = Date.now();
246 if ( ! ts || ts <= now ) {
247 return 0;
248 }
249
250 return Math.ceil( ( ts - now ) / MS_PER_DAY );
251 }
252
253 /**
254 * Whether the license grants access (active, or inactive with a valid subscription).
255 *
256 * @param {string} [status] License status from API.
257 * @param {string} [expiryDate] Expiry from API.
258 * @return {boolean} True when the license is entitled for use.
259 */
260 export function isRichLicenseEntitled( status, expiryDate ) {
261 if ( isRichLicenseActive( status ) ) {
262 return true;
263 }
264
265 const normalized = normalizeLicenseStatus( status );
266
267 if ( normalized === 'inactive' ) {
268 return ! expiryDate || hasFutureExpiry( expiryDate );
269 }
270
271 if (
272 normalized === 'expired' ||
273 normalized === 'invalid' ||
274 normalized === 'disabled'
275 ) {
276 return !! expiryDate && hasFutureExpiry( expiryDate );
277 }
278
279 return false;
280 }
281
282 /**
283 * Shop row status for display (active|expired contract) before All Access / map overrides.
284 *
285 * @param {Object} license
286 * @param {string} currentHostname
287 * @return {string} Normalized shop status.
288 */
289 export function normalizeShopRowStatus( license, currentHostname ) {
290 const status = license?.status ?? '';
291
292 if ( ! isRichLicenseEntitled( status, license?.expiryDate ) ) {
293 return status;
294 }
295
296 if ( isAllAccessBundleName( license?.name ) ) {
297 return isCurrentSiteActivated(
298 license?.sitesActivated,
299 currentHostname
300 )
301 ? 'active'
302 : status;
303 }
304
305 if ( isCurrentSiteActivatedOnLicense( license, currentHostname ) ) {
306 return 'active';
307 }
308
309 return 'inactive';
310 }
311
312 /**
313 * Whether the current site hostname appears in activated sites.
314 *
315 * @param {Array<{domain?: string}>} sitesActivated Sites from license API.
316 * @param {string} currentHostname Current site hostname.
317 * @return {boolean} True when the current hostname is activated.
318 */
319 export function isCurrentSiteActivated( sitesActivated, currentHostname ) {
320 if ( ! currentHostname ) {
321 return false;
322 }
323 return ( sitesActivated ?? [] ).some( ( row ) =>
324 String( row?.domain ?? '' ).includes( currentHostname )
325 );
326 }
327
328 /**
329 * Number of sites consuming a license slot (list length or EDD activation count).
330 *
331 * @param {Array<{domain?: string}>} sitesActivated Sites from license API.
332 * @param {number} activationCount EDD activation count when provided.
333 * @return {number} Sites used, from list length or EDD activation count.
334 */
335 export function getLicenseSitesUsedCount( sitesActivated, activationCount ) {
336 const listCount = ( sitesActivated ?? [] ).length;
337 const count =
338 typeof activationCount === 'number' && activationCount > 0
339 ? activationCount
340 : 0;
341
342 return Math.max( listCount, count );
343 }
344
345 /**
346 * Whether this site is activated on this license row (active licenses only).
347 *
348 * @param {Object} license
349 * @param {string} license.status
350 * @param {Array<{domain?: string}>} license.sitesActivated
351 * @param {string} currentHostname
352 * @return {boolean} True when this license is activated on the current site.
353 */
354 export function isCurrentSiteActivatedOnLicense( license, currentHostname ) {
355 if ( ! isRichLicenseEntitled( license?.status, license?.expiryDate ) ) {
356 return false;
357 }
358
359 return isCurrentSiteActivated( license?.sitesActivated, currentHostname );
360 }
361
362 /**
363 * Whether activation is allowed for this site (slot or already on account).
364 *
365 * @param {Object} args
366 * @param {string} args.status
367 * @param {number} args.activationCount
368 * @param {number} args.availableSites
369 * @param {Array<{domain?: string}>} args.sitesActivated
370 * @param {string} args.currentHostname
371 * @param {string} [args.expiryDate] License expiry date.
372 * @return {boolean} True when activation is allowed on this site.
373 */
374 export function canActivateOnThisSite( {
375 status,
376 activationCount,
377 availableSites,
378 sitesActivated,
379 currentHostname,
380 expiryDate,
381 } ) {
382 if ( ! isRichLicenseEntitled( status, expiryDate ) ) {
383 return false;
384 }
385
386 if ( isCurrentSiteActivated( sitesActivated, currentHostname ) ) {
387 return true;
388 }
389
390 return (
391 getLicenseSitesUsedCount( sitesActivated, activationCount ) <
392 availableSites
393 );
394 }
395
396 /**
397 * Format the license date.
398 *
399 * @param {string} [raw] Date string from API.
400 * @return {string} Formatted date, em dash if missing, or original if invalid.
401 */
402 export function formatLicenseDate( raw ) {
403 if ( ! raw ) {
404 return '';
405 }
406 const parsed = parseLicenseExpiryDate( raw );
407 if ( ! parsed ) {
408 return raw;
409 }
410 return parsed.toLocaleDateString( undefined, {
411 year: 'numeric',
412 month: 'short',
413 day: 'numeric',
414 } );
415 }
416
417 /**
418 * Get the type label for the license.
419 *
420 * @param {string} name License name.
421 * @param {number} total Total sites allowed by the license.
422 * @return {string} Display string for license type / tier.
423 */
424 export function getTypeLabel( name, total = 0 ) {
425 name = name ?? __( 'License', 'advanced-ads' );
426
427 if ( total > 1 ) {
428 return `${ name } / ${ total } ${ __( 'sites', 'advanced-ads' ) }`;
429 }
430
431 return name;
432 }
433
434 /**
435 * Normalize a license product name for comparisons.
436 *
437 * @param {string} [name] License product name from API.
438 * @return {string} Normalized lowercase license name.
439 */
440 export function normalizeLicenseName( name ) {
441 return String( name ?? '' )
442 .toLowerCase()
443 .trim()
444 .replace( /\s+/g, ' ' );
445 }
446
447 /**
448 * Whether the license name represents an All Access bundle.
449 *
450 * @param {string} [name] License product name from API.
451 * @return {boolean} True when the name matches an All Access bundle.
452 */
453 export function isAllAccessBundleName( name ) {
454 const normalized = normalizeLicenseName( name );
455 return normalized !== '' && normalized.startsWith( 'all access' );
456 }
457
458 /**
459 * Strip site-tier suffixes from a normalized license product name (e.g. "/ 2 sites").
460 *
461 * @param {string} normalized Already normalized license name.
462 * @return {string} Product name without tier suffix.
463 */
464 export function stripLicenseProductTierSuffix( normalized ) {
465 return String( normalized ?? '' )
466 .replace( /\s*\/\s*\d+\s*sites?.*$/i, '' )
467 .replace( /\s*\(\s*\d+\s*sites?\s*\).*$/i, '' )
468 .trim();
469 }
470
471 /**
472 * Map a shop license product name to a short add-on id (pro, tracking, …).
473 * Aligns with PHP License_Product_Map (manifest labels + tier stripping).
474 *
475 * @param {string} [name] License product name from API.
476 * @return {string|null} Add-on id or null when unknown / All Access row.
477 */
478 export function addonIdForLicenseProductName( name ) {
479 let normalized = normalizeLicenseName( name );
480 if ( '' === normalized || normalized.startsWith( 'all access' ) ) {
481 return null;
482 }
483
484 normalized = stripLicenseProductTierSuffix( normalized );
485
486 for ( const [ id, labels ] of LICENSE_PRODUCT_ADDON_LABELS ) {
487 for ( const label of labels ) {
488 if ( matchesLicenseProductLabel( normalized, label ) ) {
489 return id;
490 }
491 }
492 }
493
494 return null;
495 }
496
497 /**
498 * Find add-on id assigned to a license key in the applied map.
499 *
500 * @param {string} licenseKey
501 * @param {Object<string, string>} appliedAddonKeyMap
502 * @return {string|null} Addon ID
503 */
504 export function addonIdForLicenseKey( licenseKey, appliedAddonKeyMap ) {
505 const key = toLicenseKey( licenseKey );
506 if ( ! key ) {
507 return null;
508 }
509
510 for ( const [ addonId, mappedKey ] of Object.entries(
511 appliedAddonKeyMap ?? {}
512 ) ) {
513 if ( String( mappedKey ) === key ) {
514 return addonId;
515 }
516 }
517
518 return null;
519 }
520
521 /**
522 * Resolve add-on id for a license row (product name, then applied map).
523 *
524 * @param {Object} license
525 * @param {Object<string, string>} [appliedAddonKeyMap]
526 * @return {string|null} Addon ID
527 */
528 export function resolveAddonIdForLicense( license, appliedAddonKeyMap = {} ) {
529 return (
530 addonIdForLicenseProductName( license?.name ) ??
531 addonIdForLicenseKey( license?.licenseKey, appliedAddonKeyMap )
532 );
533 }
534
535 /**
536 * Best entitled All Access row (same rule as PHP License::find_all_access_row( …, 'entitled' )).
537 *
538 * @param {Array<{name?: string, status?: string, expiryDate?: string}>} [licenses]
539 * @return {object|null} Best matching entitled All Access license or null.
540 */
541 export function findEntitledAllAccessLicense( licenses ) {
542 let best = null;
543
544 for ( const row of licenses ?? [] ) {
545 if ( ! isAllAccessBundleName( row?.name ) ) {
546 continue;
547 }
548 if ( ! isRichLicenseEntitled( row?.status, row?.expiryDate ) ) {
549 continue;
550 }
551 if ( ! best ) {
552 best = row;
553 continue;
554 }
555 const bestActive = isRichLicenseActive( best?.status );
556 const rowActive = isRichLicenseActive( row?.status );
557 if ( rowActive && ! bestActive ) {
558 best = row;
559 }
560 }
561
562 return best;
563 }
564
565 /**
566 * License keys currently stored in advanced-ads-licenses (addon id => key).
567 *
568 * @param {Object<string, string>} [appliedAddonKeyMap]
569 * @return {Set<string>} Set of applied license keys.
570 */
571 export function getAppliedLicenseKeys( appliedAddonKeyMap ) {
572 const keys = new Set();
573 for ( const value of Object.values( appliedAddonKeyMap ?? {} ) ) {
574 if ( value ) {
575 keys.add( String( value ) );
576 }
577 }
578 return keys;
579 }
580
581 /**
582 * Whether the persisted map uses one All Access key for every addon (AA governs UI).
583 *
584 * @param {Object<string, string>} appliedAddonKeyMap Applied addon-to-license map.
585 * @param {Array<object>} allLicenses All available licenses.
586 * @return {boolean} True when a single All Access key governs all addons.
587 */
588 export function isAllAccessGoverningAppliedMap(
589 appliedAddonKeyMap,
590 allLicenses
591 ) {
592 const values = [ ...getAppliedLicenseKeys( appliedAddonKeyMap ) ];
593 if ( values.length < 2 ) {
594 return false;
595 }
596 const unique = new Set( values );
597 if ( unique.size !== 1 ) {
598 return false;
599 }
600 const entitledAllAccess = findEntitledAllAccessLicense( allLicenses );
601 return (
602 !! entitledAllAccess &&
603 String( entitledAllAccess.licenseKey ?? '' ) === values[ 0 ]
604 );
605 }
606
607 /**
608 * Whether this license key is in advanced-ads-licenses (source of truth for "active on site").
609 *
610 * @param {string} licenseKey License key to check.
611 * @param {Object<string, string>} appliedAddonKeyMap Applied addon-to-license map.
612 * @return {boolean} True when the license key exists in the applied map.
613 */
614 export function isLicenseKeyInAppliedMap( licenseKey, appliedAddonKeyMap ) {
615 const key = toLicenseKey( licenseKey );
616 if ( ! key ) {
617 return false;
618 }
619 return getAppliedLicenseKeys( appliedAddonKeyMap ).has( key );
620 }
621
622 /**
623 * Whether All Access is activated for this site in rich license data.
624 *
625 * @param {Array<object>} allLicenses All available licenses.
626 * @param {string} currentHostname Current site hostname.
627 * @return {boolean} True when All Access is active on the current site.
628 */
629 export function isAllAccessActiveOnThisSite( allLicenses, currentHostname ) {
630 const entitledAllAccess = findEntitledAllAccessLicense( allLicenses );
631 if ( ! entitledAllAccess ) {
632 return false;
633 }
634 return isCurrentSiteActivatedOnLicense(
635 entitledAllAccess,
636 currentHostname
637 );
638 }
639
640 /**
641 * Disk / plugin state for one add-on from REST addonInstallStates.
642 *
643 * @param {string} addonId
644 * @param {Object<string, object>} addonInstallStates
645 * @return {Object} Installation and activation state of the add-on.
646 */
647 export function getAddonInstallState( addonId, addonInstallStates ) {
648 const row = addonInstallStates?.[ addonId ];
649 return {
650 installed: Boolean( row?.installed ),
651 active: Boolean( row?.active ),
652 };
653 }
654
655 /**
656 * Whether at least one add-on is running and assigned to this All Access key on site.
657 *
658 * @param {string} licenseKey
659 * @param {Object<string, string>} appliedAddonKeyMap
660 * @param {Object<string, object>} addonInstallStates
661 * @return {boolean} Whether at least one assigned add-on is currently running.
662 */
663 export function hasActiveAddonUnderAllAccessLicense(
664 licenseKey,
665 appliedAddonKeyMap,
666 addonInstallStates
667 ) {
668 const key = toLicenseKey( licenseKey );
669 if ( ! key ) {
670 return false;
671 }
672
673 for ( const [ addonId, mappedKey ] of Object.entries(
674 appliedAddonKeyMap ?? {}
675 ) ) {
676 if ( String( mappedKey ) !== key ) {
677 continue;
678 }
679
680 if ( getAddonInstallState( addonId, addonInstallStates ).active ) {
681 return true;
682 }
683 }
684
685 return false;
686 }
687
688 export function getDisplayStatusForAllAccessLicense(
689 license,
690 licenseKey,
691 raw,
692 currentHostname,
693 appliedAddonKeyMap = {},
694 addonInstallStates = {}
695 ) {
696 if ( ! isRichLicenseEntitled( license?.status, license?.expiryDate ) ) {
697 return raw;
698 }
699
700 const mapKeys = getAppliedLicenseKeys( appliedAddonKeyMap );
701 if ( mapKeys.size === 0 ) {
702 return 'inactive';
703 }
704
705 if ( ! isLicenseKeyInAppliedMap( licenseKey, appliedAddonKeyMap ) ) {
706 return 'inactive';
707 }
708
709 if ( isCurrentSiteActivated( license?.sitesActivated, currentHostname ) ) {
710 return 'active';
711 }
712
713 if (
714 hasActiveAddonUnderAllAccessLicense(
715 licenseKey,
716 appliedAddonKeyMap,
717 addonInstallStates
718 )
719 ) {
720 return 'active';
721 }
722
723 return 'inactive';
724 }
725
726 export function getDisplayStatusWithAppliedMap(
727 license,
728 licenseKey,
729 raw,
730 allLicenses,
731 appliedAddonKeyMap,
732 addonInstallStates,
733 aaOnSite
734 ) {
735 const addonId = resolveAddonIdForLicense( license, appliedAddonKeyMap );
736 const inMap =
737 addonId &&
738 isAddonLicensedByKey( addonId, licenseKey, appliedAddonKeyMap );
739
740 if ( ! isRichLicenseEntitled( license?.status, license?.expiryDate ) ) {
741 return raw;
742 }
743
744 if ( ! inMap && isRichLicenseActive( raw ) ) {
745 return 'inactive';
746 }
747
748 if (
749 inMap &&
750 addonId &&
751 ! getAddonInstallState( addonId, addonInstallStates ).active
752 ) {
753 return 'inactive';
754 }
755
756 if (
757 inMap &&
758 addonId &&
759 getAddonInstallState( addonId, addonInstallStates ).active &&
760 raw === 'active'
761 ) {
762 return 'active';
763 }
764
765 if (
766 aaOnSite ||
767 isAllAccessGoverningAppliedMap( appliedAddonKeyMap, allLicenses )
768 ) {
769 return 'inactive';
770 }
771
772 return raw;
773 }
774
775 export function getDisplayStatusWithoutAppliedMap(
776 license,
777 raw,
778 currentHostname,
779 appliedAddonKeyMap,
780 addonInstallStates,
781 aaOnSite
782 ) {
783 if ( aaOnSite && ! isAllAccessBundleName( license?.name ) ) {
784 return 'inactive';
785 }
786
787 const addonId = resolveAddonIdForLicense( license, appliedAddonKeyMap );
788 if (
789 addonId &&
790 isCurrentSiteActivatedOnLicense( license, currentHostname ) &&
791 isRichLicenseActive( raw ) &&
792 ! getAddonInstallState( addonId, addonInstallStates ).active
793 ) {
794 return 'inactive';
795 }
796
797 return raw;
798 }
799
800 export function isLicenseAppliedOnThisSite(
801 license,
802 _allLicenses,
803 _currentHostname,
804 currentActiveLicenses = ''
805 ) {
806 const licenseKey = getLicenseKeyFromRow( license );
807 if ( ! licenseKey ) {
808 return false;
809 }
810
811 const active = String( currentActiveLicenses ?? '' )
812 .split( ',' )
813 .map( ( key ) => key.trim() )
814 .filter( Boolean );
815
816 return active.includes( licenseKey );
817 }
818
819 /**
820 * Whether this add-on uses the given license key on this site (map only).
821 *
822 * @param {string} addonId
823 * @param {string} licenseKey
824 * @param {Object<string, string>} appliedAddonKeyMap
825 * @return {boolean} Whether the add-on is assigned to the given license key.
826 */
827 export function isAddonLicensedByKey(
828 addonId,
829 licenseKey,
830 appliedAddonKeyMap
831 ) {
832 if ( ! addonId || ! licenseKey ) {
833 return false;
834 }
835
836 return (
837 getAddonMapKey( appliedAddonKeyMap, addonId ) ===
838 toLicenseKey( licenseKey )
839 );
840 }
841
842 /**
843 * Whether this add-on is licensed on site by a different license than the All Access card key.
844 *
845 * @param {string} addonId
846 * @param {string} allAccessLicenseKey
847 * @param {Object<string, string>} appliedAddonKeyMap
848 * @return {boolean} Whether the add-on is assigned to a different license key.
849 */
850 export function isAddonManagedByOtherLicense(
851 addonId,
852 allAccessLicenseKey,
853 appliedAddonKeyMap
854 ) {
855 if ( ! addonId || ! allAccessLicenseKey ) {
856 return false;
857 }
858
859 const assignedKey = getAddonMapKey( appliedAddonKeyMap, addonId );
860 return (
861 assignedKey !== '' &&
862 assignedKey !== toLicenseKey( allAccessLicenseKey )
863 );
864 }
865
866 /**
867 * Whether a license key belongs to an All Access bundle row.
868 *
869 * @param {string} licenseKey
870 * @param {Array<object>} allLicenses
871 * @return {boolean} All access key
872 */
873 export function isAllAccessLicenseKey( licenseKey, allLicenses ) {
874 const key = toLicenseKey( licenseKey );
875 if ( ! key ) {
876 return false;
877 }
878
879 const row = ( allLicenses ?? [] ).find(
880 ( license ) => getLicenseKeyFromRow( license ) === key
881 );
882
883 return row ? isAllAccessBundleName( row?.name ) : false;
884 }
885
886 /**
887 * Whether bulk Activate all should skip an add-on mapped to another license key.
888 *
889 * When switching to an inactive All Access license, take over add-ons from another
890 * All Access key. Always skip add-ons licensed by a single-product key.
891 *
892 * @param {string} addonId
893 * @param {string} licenseKey
894 * @param {Object<string, string>} appliedAddonKeyMap
895 * @param {Array<object>} allLicenses
896 * @param {boolean} isLicenseActivatedOnSite
897 * @return {boolean} Bulk activate addon check
898 */
899 export function shouldSkipAddonForBulkActivate(
900 addonId,
901 licenseKey,
902 appliedAddonKeyMap,
903 allLicenses,
904 isLicenseActivatedOnSite
905 ) {
906 if (
907 ! isAddonManagedByOtherLicense(
908 addonId,
909 licenseKey,
910 appliedAddonKeyMap
911 )
912 ) {
913 return false;
914 }
915
916 if ( isLicenseActivatedOnSite ) {
917 return true;
918 }
919
920 const managingKey = getAddonMapKey( appliedAddonKeyMap, addonId );
921
922 return ! isAllAccessLicenseKey( managingKey, allLicenses );
923 }
924
925 /**
926 * Display name for the license row that owns an add-on key on this site.
927 *
928 * @param {string} licenseKey
929 * @param {Array<object>} allLicenses
930 * @return {string} The display name of the license that owns the add-on key.
931 */
932 export function getLicenseNameForKey( licenseKey, allLicenses ) {
933 const key = toLicenseKey( licenseKey );
934 if ( ! key ) {
935 return '';
936 }
937
938 const row = ( allLicenses ?? [] ).find(
939 ( license ) => getLicenseKeyFromRow( license ) === key
940 );
941
942 return String( row?.name ?? '' );
943 }
944
945 /**
946 * Row status for Included add-ons UI.
947 *
948 * @param {string} addonId
949 * @param {Object<string, object>} addonInstallStates
950 * @param {boolean} isApplied License key applied for this add-on.
951 * @return {string} UI status of the add-on row.
952 */
953 export function getAddonRowStatus( addonId, addonInstallStates, isApplied ) {
954 const { installed, active } = getAddonInstallState(
955 addonId,
956 addonInstallStates
957 );
958
959 if ( active && isApplied ) {
960 return 'installed';
961 }
962
963 if ( active ) {
964 return 'running';
965 }
966
967 if ( installed ) {
968 return 'ready';
969 }
970
971 return 'needs_install';
972 }
973
974 /**
975 * Derive bulk Activate all / Deactivate all mode and target add-on ids.
976 * Skips add-ons managed by another license. Matches AddonRowActions Active UI.
977 *
978 * @param {Object} args
979 * @param {string[]} args.addonIds
980 * @param {Object<string, object>} args.addonInstallStates
981 * @param {Object<string, string>} args.appliedAddonKeyMap
982 * @param {string} args.licenseKey
983 * @param {boolean} args.isLicenseActivatedOnSite
984 * @param {Array<object>} args.allLicenses
985 * @return {{ mode: 'activate'|'deactivate'|'none', targetIds: string[] }} Bulk action mode and eligible add-on ids.
986 */
987 export function getBulkAddonActionPlan( {
988 addonIds,
989 addonInstallStates,
990 appliedAddonKeyMap,
991 licenseKey,
992 isLicenseActivatedOnSite,
993 allLicenses = [],
994 } ) {
995 const activateIds = [];
996 const deactivateIds = [];
997
998 for ( const addonId of addonIds ?? [] ) {
999 const isApplied = isAddonLicensedByKey(
1000 addonId,
1001 licenseKey,
1002 appliedAddonKeyMap
1003 );
1004 const rowStatus = getAddonRowStatus(
1005 addonId,
1006 addonInstallStates,
1007 isApplied
1008 );
1009 const isActiveRow =
1010 ( rowStatus === 'installed' && isLicenseActivatedOnSite ) ||
1011 ( rowStatus === 'running' && isLicenseActivatedOnSite );
1012
1013 if ( isActiveRow ) {
1014 if (
1015 isAddonManagedByOtherLicense(
1016 addonId,
1017 licenseKey,
1018 appliedAddonKeyMap
1019 )
1020 ) {
1021 continue;
1022 }
1023 deactivateIds.push( addonId );
1024 } else if (
1025 ! shouldSkipAddonForBulkActivate(
1026 addonId,
1027 licenseKey,
1028 appliedAddonKeyMap,
1029 allLicenses,
1030 isLicenseActivatedOnSite
1031 )
1032 ) {
1033 activateIds.push( addonId );
1034 }
1035 }
1036
1037 if ( activateIds.length > 0 ) {
1038 return { mode: 'activate', targetIds: activateIds };
1039 }
1040
1041 if ( deactivateIds.length > 0 ) {
1042 return { mode: 'deactivate', targetIds: deactivateIds };
1043 }
1044
1045 return { mode: 'none', targetIds: [] };
1046 }
1047
1048 /**
1049 * Counts per add-on row state for the All Access license card summary.
1050 *
1051 * @param {string[]} addonIds
1052 * @param {Object<string, object>} addonInstallStates
1053 * @param {Object<string, string>} appliedAddonKeyMap
1054 * @param {string} licenseKey
1055 * @return {{
1056 * total: number,
1057 * installed: number,
1058 * ready: number,
1059 * pending: number,
1060 * state: 'none'|'partial'|'complete'
1061 * }} Summary counts and overall license state across all add-ons.
1062 */
1063 export function getAllAccessAddonsInstallSummary(
1064 addonIds,
1065 addonInstallStates,
1066 appliedAddonKeyMap,
1067 licenseKey
1068 ) {
1069 let installed = 0;
1070 let ready = 0;
1071 let pending = 0;
1072
1073 for ( const addonId of addonIds ) {
1074 const isLicensedByThisKey = isAddonLicensedByKey(
1075 addonId,
1076 licenseKey,
1077 appliedAddonKeyMap
1078 );
1079 const rowStatus = getAddonRowStatus(
1080 addonId,
1081 addonInstallStates,
1082 isLicensedByThisKey
1083 );
1084
1085 if ( rowStatus === 'installed' ) {
1086 installed++;
1087 } else if ( rowStatus === 'ready' ) {
1088 ready++;
1089 } else {
1090 pending++;
1091 }
1092 }
1093
1094 const total = addonIds.length;
1095 let state = 'partial';
1096 if ( installed === total && total > 0 ) {
1097 state = 'complete';
1098 } else if ( installed === 0 && ready === 0 ) {
1099 state = 'none';
1100 }
1101
1102 return {
1103 total,
1104 installed,
1105 ready,
1106 pending,
1107 state,
1108 };
1109 }
1110
1111 /**
1112 * Human-readable All Access add-on setup label for the license card.
1113 *
1114 * @param {{ total: number, installed: number, ready: number, pending: number, state: string }} summary
1115 * @return {string} Human-readable setup label describing the All Access add-on state.
1116 */
1117 export function getAllAccessAddonsSummaryLabel( summary ) {
1118 const { total, installed, ready, state } = summary;
1119
1120 if ( state === 'complete' ) {
1121 return __( 'All installed', 'advanced-ads' );
1122 }
1123
1124 if ( state === 'none' ) {
1125 return sprintf(
1126 /* translators: %d: Total number of included add-ons. */
1127 __( 'None installed (%d add-ons)', 'advanced-ads' ),
1128 total
1129 );
1130 }
1131
1132 if ( ready > 0 ) {
1133 return sprintf(
1134 /* translators: 1: Number installed, 2: Total add-ons, 3: Number downloaded but not finished. */
1135 __( '%1$d of %2$d installed (%3$d ready)', 'advanced-ads' ),
1136 installed,
1137 total,
1138 ready
1139 );
1140 }
1141
1142 return sprintf(
1143 /* translators: 1: Number installed, 2: Total add-ons. */
1144 __( '%1$d of %2$d installed', 'advanced-ads' ),
1145 installed,
1146 total
1147 );
1148 }
1149
1150 /**
1151 * Whether the user can activate this license row on the current site.
1152 *
1153 * @param {Object} license License row data.
1154 * @param {Array<object>} allLicenses All available licenses.
1155 * @param {Object} args Same fields as canActivateOnThisSite.
1156 * @param {Object} [appliedAddonKeyMap] Applied addon-to-license map.
1157 * @return {boolean} True when the license can be activated on this site.
1158 */
1159 export function canActivateLicenseRowOnSite(
1160 license,
1161 allLicenses,
1162 args,
1163 appliedAddonKeyMap = {}
1164 ) {
1165 const { status, expiryDate } = args;
1166
1167 if ( ! isRichLicenseEntitled( status, expiryDate ) ) {
1168 return false;
1169 }
1170
1171 const licenseKey = getLicenseKeyFromRow( license );
1172 const addonId = resolveAddonIdForLicense( license, appliedAddonKeyMap );
1173
1174 // This license already owns its add-on on site (Deactivate path, not activate).
1175 if (
1176 addonId &&
1177 licenseKey &&
1178 isAddonLicensedByKey( addonId, licenseKey, appliedAddonKeyMap )
1179 ) {
1180 return false;
1181 }
1182
1183 if ( isAllAccessBundleName( license?.name ) ) {
1184 return canActivateOnThisSite( args );
1185 }
1186
1187 // Single-product: always allow shop activate when entitled (slot limits enforced by shop).
1188 return true;
1189 }
1190
1191 /**
1192 * Whether auto-updates are enabled for one add-on id (or main).
1193 *
1194 * @param {string} addonId Short id or "main".
1195 * @param {Object<string, string>} autoUpdateStates Map from REST.
1196 * @return {boolean} True if auto-updates are enabled.
1197 */
1198 export function isPluginAutoUpdateEnabled( addonId, autoUpdateStates ) {
1199 const key = addonId || 'main';
1200 return autoUpdateStates?.[ key ] === 'on';
1201 }
1202
1203 /**
1204 * Human-readable auto-update summary for a license row.
1205 *
1206 * @param {string} licenseName License product name.
1207 * @param {Object<string, string>} autoUpdateStates Map from REST.
1208 * @return {string} Localized On, Off, or Mixed.
1209 */
1210 export function getAutoUpdateDisplayLabel( licenseName, autoUpdateStates ) {
1211 if ( isAllAccessBundleName( licenseName ) ) {
1212 const states = ALL_ACCESS_ADDON_IDS.map(
1213 ( id ) => autoUpdateStates?.[ id ] ?? 'off'
1214 );
1215 const allOn = states.every( ( state ) => state === 'on' );
1216 const allOff = states.every( ( state ) => state === 'off' );
1217
1218 if ( allOn ) {
1219 return __( 'On', 'advanced-ads' );
1220 }
1221 if ( allOff ) {
1222 return __( 'Off', 'advanced-ads' );
1223 }
1224
1225 return __( 'Mixed', 'advanced-ads' );
1226 }
1227
1228 const addonId = addonIdForLicenseProductName( licenseName );
1229 const key = addonId || 'main';
1230
1231 return isPluginAutoUpdateEnabled( key, autoUpdateStates )
1232 ? __( 'On', 'advanced-ads' )
1233 : __( 'Off', 'advanced-ads' );
1234 }
1235
1236 /**
1237 * Add-on ids whose auto-update toggles apply to this license row.
1238 *
1239 * @param {string} licenseName License product name.
1240 * @return {string[]} Short add-on ids or ["main"].
1241 */
1242 export function getAutoUpdateAddonIdsForLicense( licenseName ) {
1243 if ( isAllAccessBundleName( licenseName ) ) {
1244 return [ ...ALL_ACCESS_ADDON_IDS ];
1245 }
1246
1247 const addonId = addonIdForLicenseProductName( licenseName );
1248 return [ addonId || 'main' ];
1249 }
1250
1251 /**
1252 * Short label for which plugin(s) an auto-update row controls.
1253 *
1254 * @param {string} licenseName License product name.
1255 * @return {string} Label for the auto-update row.
1256 */
1257 export function getAutoUpdateScopeLabel( licenseName ) {
1258 if ( isAllAccessBundleName( licenseName ) ) {
1259 return __( 'All add-ons', 'advanced-ads' );
1260 }
1261
1262 const addonIds = getAutoUpdateAddonIdsForLicense( licenseName );
1263 const titles = addonIds.map( ( id ) => {
1264 if ( id === 'main' ) {
1265 return __( 'Advanced Ads', 'advanced-ads' );
1266 }
1267 return LICENSE_ADDON_CATALOG[ id ]?.title ?? id;
1268 } );
1269
1270 return titles.join( ', ' );
1271 }
1272
1273 /**
1274 * Shop checkout URL that adds a plan to cart for in-plugin purchase.
1275 *
1276 * @param {Object} params
1277 * @param {number} params.downloadId EDD download post ID.
1278 * @param {number} params.pricingId EDD variable price ID.
1279 * @param {string} [params.utmCampaign] Optional utm_campaign query param.
1280 * @return {string} Shop checkout add-to-cart URL with site return param.
1281 */
1282 export function buildShopCheckoutUrl( { downloadId, pricingId, utmCampaign } ) {
1283 const url = new URL( `${ endpoints.shopUrl }/checkout/` );
1284 url.searchParams.set( 'edd_action', 'add_to_cart' );
1285 url.searchParams.set( 'download_id', String( downloadId ) );
1286 if ( Number( pricingId ) > 0 ) {
1287 url.searchParams.set( 'edd_options[price_id]', String( pricingId ) );
1288 }
1289 url.searchParams.set( 'site', endpoints.siteUrl );
1290 if ( utmCampaign ) {
1291 url.searchParams.set( 'utm_source', 'advancedads' );
1292 url.searchParams.set( 'utm_medium', 'in-plugin' );
1293 url.searchParams.set( 'utm_campaign', utmCampaign );
1294 }
1295 return url.toString();
1296 }
1297
1298 /**
1299 * Direct EDD checkout URL for license upgrade (sl_license_upgrade).
1300 *
1301 * @param {Object} params
1302 * @param {number} params.licenseId Current EDD license post ID.
1303 * @param {number} params.upgradeId EDD upgrade path id (1 = AA single, 2 = AA 5-site).
1304 * @param {string} [params.utmCampaign] Optional utm_campaign query param.
1305 * @return {string} Shop checkout upgrade URL.
1306 */
1307 export function buildShopEddUpgradeUrl( {
1308 licenseId,
1309 upgradeId,
1310 utmCampaign,
1311 } ) {
1312 const url = new URL( `${ endpoints.shopUrl }/checkout/` );
1313 url.searchParams.set( 'edd_action', 'sl_license_upgrade' );
1314 url.searchParams.set( 'license_id', String( licenseId ) );
1315 url.searchParams.set( 'upgrade_id', String( upgradeId ) );
1316 url.searchParams.set( 'site', endpoints.siteUrl );
1317
1318 if ( utmCampaign ) {
1319 url.searchParams.set( 'utm_source', 'advancedads' );
1320 url.searchParams.set( 'utm_medium', 'in-plugin' );
1321 url.searchParams.set( 'utm_campaign', utmCampaign );
1322 }
1323
1324 return url.toString();
1325 }
1326
1327 /**
1328 * Shop checkout bridge URL for license renewal (replaces sso-login).
1329 *
1330 * @param {Object} params
1331 * @param {number} params.licenseId EDD license post ID.
1332 * @param {string} [params.utmCampaign] Optional utm_campaign query param.
1333 * @return {string} Checkout bridge URL.
1334 */
1335 export function buildShopRenewalCheckoutUrl( { licenseId, utmCampaign } ) {
1336 const url = new URL( `${ endpoints.shopUrl }/checkout/` );
1337 url.searchParams.set( 'site', endpoints.siteUrl );
1338 url.searchParams.set( 'intent', 'renew' );
1339 url.searchParams.set( 'license_id', String( licenseId ) );
1340
1341 if ( utmCampaign ) {
1342 url.searchParams.set( 'utm_source', 'advancedads' );
1343 url.searchParams.set( 'utm_medium', 'in-plugin' );
1344 url.searchParams.set( 'utm_campaign', utmCampaign );
1345 }
1346
1347 return url.toString();
1348 }
1349
1350 /**
1351 * Resolve checkout IDs for a modal plan key.
1352 *
1353 * @param {string} planId Plan id from PricingTable PLANS.
1354 * @param {Array<{id: string, downloadId: number, pricingId: number}>} [plans] Plan list.
1355 * @return {{ downloadId: number, pricingId: number }|null} EDD download and price IDs, or null when unknown.
1356 */
1357 export function getCheckoutIdsForPlan( planId, plans ) {
1358 const plan = ( plans ?? [] ).find( ( entry ) => entry.id === planId );
1359 if ( ! plan?.downloadId ) {
1360 return null;
1361 }
1362 if ( 'all-access-five' === plan.id && 2 !== plan.pricingId ) {
1363 return null;
1364 }
1365 return {
1366 downloadId: Number( plan.downloadId ),
1367 pricingId: Number( plan.pricingId ) || 0,
1368 };
1369 }
1370
1371 /**
1372 * Navigate to shop checkout for the given plan.
1373 *
1374 * @param {string} planId Plan id from PricingTable.
1375 * @param {Array<{id: string, downloadId: number, pricingId: number}>} plans Plan list from PLANS.
1376 * @return {boolean} False when plan IDs are missing.
1377 */
1378 export function startShopCheckoutForPlan( planId, plans ) {
1379 const ids = getCheckoutIdsForPlan( planId, plans );
1380 if ( ! ids ) {
1381 return false;
1382 }
1383 const plan = ( plans ?? [] ).find( ( entry ) => entry.id === planId );
1384 globalThis.location.href = buildShopCheckoutUrl( {
1385 ...ids,
1386 utmCampaign: plan?.utmCampaign ?? '',
1387 } );
1388 return true;
1389 }
1390
1391 /**
1392 * Navigate to shop EDD upgrade checkout for the given plan and license.
1393 *
1394 * @param {string} planId Plan id from PricingTable.
1395 * @param {number} licenseId EDD license post ID.
1396 * @param {Array<{id: string, upgradeId?: number}>} plans Plan list from PLANS.
1397 * @return {boolean} False when plan or licenseId are missing.
1398 */
1399 export function startShopUpgradeForPlan( planId, licenseId, plans ) {
1400 const plan = ( plans ?? [] ).find( ( entry ) => entry.id === planId );
1401 const upgradeId = plan?.upgradeId;
1402 if ( ! upgradeId || ! licenseId ) {
1403 return false;
1404 }
1405 globalThis.location.href = buildShopEddUpgradeUrl( {
1406 licenseId,
1407 upgradeId,
1408 utmCampaign: plan?.utmCampaign ?? '',
1409 } );
1410 return true;
1411 }
1412
1413 /**
1414 * Whether the shop license row is expired (EDD renewal eligibility).
1415 *
1416 * @param {string} [status] License status from exchange payload.
1417 * @return {boolean} True when status is expired on the shop.
1418 */
1419 export function isShopLicenseExpired( status ) {
1420 return normalizeLicenseStatus( status ) === 'expired';
1421 }
1422
1423 /**
1424 * Shop /sso-login URL that starts license renewal intent for an existing license row.
1425 *
1426 * @param {Object} params
1427 * @param {number} params.licenseId EDD license post ID from exchange payload.
1428 * @return {string} Shop sso-login URL with renew intent query args.
1429 */
1430 export function buildShopRenewalUrl( { licenseId } ) {
1431 return buildShopSsoUrl( 'renew', {
1432 license_id: licenseId,
1433 } );
1434 }
1435
1436 /**
1437 * Navigate to shop renewal checkout for the given license.
1438 *
1439 * @param {number} licenseId EDD license post ID.
1440 * @return {boolean} False when licenseId is missing.
1441 */
1442 export function startShopRenewalForLicense( licenseId ) {
1443 if ( ! licenseId ) {
1444 return false;
1445 }
1446 globalThis.location.href = buildShopRenewalCheckoutUrl( {
1447 licenseId,
1448 utmCampaign: 'a2-in_plugin-licenses_addons-renew',
1449 } );
1450 return true;
1451 }
1452
1453 /**
1454 * Target plan ids allowed when upgrading from the current plan.
1455 *
1456 * @param {'pro'|'all-access-single'|'all-access-five'|null} currentPlanId
1457 * @return {string[]} Allowed PricingTable plan ids (empty = no upgrade).
1458 */
1459 export function getAllowedUpgradePlanIds( currentPlanId ) {
1460 switch ( currentPlanId ) {
1461 case 'pro':
1462 return [ 'all-access-single', 'all-access-five' ];
1463 case 'all-access-single':
1464 return [ 'all-access-five' ];
1465 default:
1466 return [];
1467 }
1468 }
1469
1470 /**
1471 * Whether the license row has any valid upgrade target.
1472 *
1473 * @param {'pro'|'all-access-single'|'all-access-five'|null} currentPlanId
1474 * @return {boolean} return plan ids
1475 */
1476 export function canUpgradeLicensePlan( currentPlanId ) {
1477 return getAllowedUpgradePlanIds( currentPlanId ).length > 0;
1478 }
1479
1480 /**
1481 * Display label for the current plan in the upgrade pricing modal header.
1482 *
1483 * @param {Object} license License row from exchange payload.
1484 * @return {string} Human-readable plan name.
1485 */
1486 export function resolvePlanLabelForLicense( license ) {
1487 const name = license?.name ?? '';
1488 if ( isAllAccessBundleName( name ) ) {
1489 return ( license?.availableSites ?? 0 ) > 1
1490 ? __( 'All Access (5 sites)', 'advanced-ads' )
1491 : __( 'All Access', 'advanced-ads' );
1492 }
1493 return name || __( 'License', 'advanced-ads' );
1494 }
1495
1496 /**
1497 * Pricing table plan id for the license row (upgrade modal current-plan match).
1498 *
1499 * @param {Object} license License row from exchange payload.
1500 * @return {Object} object of license
1501 */
1502 export function resolvePlanIdForLicense( license ) {
1503 if ( ! license ) {
1504 return null;
1505 }
1506
1507 const name = license.name ?? '';
1508
1509 if ( isAllAccessBundleName( name ) ) {
1510 return ( license.availableSites ?? 0 ) > 1
1511 ? 'all-access-five'
1512 : 'all-access-single';
1513 }
1514
1515 if ( addonIdForLicenseProductName( name ) === 'pro' ) {
1516 return 'pro';
1517 }
1518
1519 return null;
1520 }
1521
1522 /**
1523 * Normalize checkout_intent from the post-checkout return URL.
1524 *
1525 * @param {string} [queryIntent] Raw query value.
1526 * @return {'buy'|'upgrade'|'renew'} Normalized checkout intent.
1527 */
1528 export function normalizeCheckoutIntent( queryIntent ) {
1529 const intent = String( queryIntent ?? '' ).toLowerCase();
1530
1531 if ( intent === 'upgrade' || intent === 'renew' ) {
1532 return intent;
1533 }
1534
1535 return 'buy';
1536 }
1537
1538 /**
1539 * License row to evaluate for post-checkout setup subtitle copy.
1540 *
1541 * @param {Array<object>} [licenses] Rich license list.
1542 * @param {string|number} [licenseIdFromUrl] EDD SL license post ID from return URL.
1543 * @return {object|null} Matching license row or null.
1544 */
1545 export function resolvePostCheckoutLicenseRow( licenses, licenseIdFromUrl ) {
1546 const list = licenses ?? [];
1547 const licenseId = parseInt( String( licenseIdFromUrl ?? '' ), 10 );
1548
1549 if ( licenseId > 0 ) {
1550 const match = list.find(
1551 ( row ) => Number( row?.licenseId ) === licenseId
1552 );
1553 if ( match ) {
1554 return match;
1555 }
1556 }
1557
1558 const allAccess = findEntitledAllAccessLicense( list );
1559 if ( allAccess ) {
1560 return allAccess;
1561 }
1562
1563 let best = null;
1564 let bestId = -1;
1565
1566 for ( const row of list ) {
1567 if ( isAllAccessBundleName( row?.name ) ) {
1568 continue;
1569 }
1570 if ( ! isRichLicenseEntitled( row?.status, row?.expiryDate ) ) {
1571 continue;
1572 }
1573
1574 const id = Number( row?.licenseId ?? 0 );
1575 if ( id > bestId ) {
1576 bestId = id;
1577 best = row;
1578 }
1579 }
1580
1581 return best;
1582 }
1583
1584 /**
1585 * Post-checkout subtitle bucket (license-level setup on this site).
1586 *
1587 * @param {object|null} row License row from exchange.
1588 * @param {Array<object>} licenses Full license list.
1589 * @param {string} hostname Current site hostname.
1590 * @param {Object<string, string>} appliedAddonKeyMap Addon id => license key.
1591 * @param {Object<string, object>} addonInstallStates Per-addon install flags.
1592 * @return {'ready'|'activate'|'download'} Setup state for notice subtitle.
1593 */
1594 export function getPostCheckoutSetupState(
1595 row,
1596 licenses,
1597 hostname,
1598 appliedAddonKeyMap = {},
1599 addonInstallStates = {}
1600 ) {
1601 if ( ! row || ! hostname ) {
1602 return 'download';
1603 }
1604
1605 if ( isAllAccessBundleName( row?.name ) ) {
1606 return isAllAccessActiveOnThisSite( licenses, hostname )
1607 ? 'ready'
1608 : 'download';
1609 }
1610
1611 if ( isCurrentSiteActivatedOnLicense( row, hostname ) ) {
1612 return 'ready';
1613 }
1614
1615 const addonId = resolveAddonIdForLicense( row, appliedAddonKeyMap );
1616 if ( addonId ) {
1617 const { installed, active } = getAddonInstallState(
1618 addonId,
1619 addonInstallStates
1620 );
1621 if ( installed && ! active ) {
1622 return 'activate';
1623 }
1624 }
1625
1626 return 'download';
1627 }
1628
1629 /**
1630 * Post-checkout success notice title and body (copy only).
1631 *
1632 * @param {Object} args
1633 * @param {string} [args.checkoutIntent] URL checkout_intent.
1634 * @param {Array<object>} [args.licenses] Rich license list.
1635 * @param {string|number} [args.licenseId] URL license_id.
1636 * @param {string} [args.hostname] Current site hostname.
1637 * @param {Object<string, string>} [args.appliedAddonKeyMap]
1638 * @param {Object<string, object>} [args.addonInstallStates]
1639 * @return {{ title: string, message: string }} Notice title and message.
1640 */
1641 export function buildPostCheckoutNoticeCopy( {
1642 checkoutIntent,
1643 licenses,
1644 licenseId,
1645 hostname,
1646 appliedAddonKeyMap = {},
1647 addonInstallStates = {},
1648 } ) {
1649 const intent = normalizeCheckoutIntent( checkoutIntent );
1650 const titles = {
1651 buy: __( 'License purchased successfully', 'advanced-ads' ),
1652 upgrade: __( 'License upgraded successfully', 'advanced-ads' ),
1653 renew: __( 'License renewed successfully', 'advanced-ads' ),
1654 };
1655 const messages = {
1656 ready: __( 'Your license is ready.', 'advanced-ads' ),
1657 activate: __(
1658 "Your license is ready. Use 'activate' to set it up on this site.",
1659 'advanced-ads'
1660 ),
1661 download: __(
1662 "Your license is ready. Use 'Download and activate' to set it up on this site.",
1663 'advanced-ads'
1664 ),
1665 };
1666
1667 const row = resolvePostCheckoutLicenseRow( licenses, licenseId );
1668 const setupState = row
1669 ? getPostCheckoutSetupState(
1670 row,
1671 licenses,
1672 hostname,
1673 appliedAddonKeyMap,
1674 addonInstallStates
1675 )
1676 : 'download';
1677
1678 return {
1679 title: titles[ intent ] ?? titles.buy,
1680 message: messages[ setupState ] ?? messages.download,
1681 };
1682 }
1683