From 471fd52464b47d68ff34a9f9989cd0e4325bb575 Mon Sep 17 00:00:00 2001 From: Flavien DELANGLE Date: Wed, 26 Jun 2024 13:04:08 +0200 Subject: [PATCH] [license] Allow usage of charts an tree view pro package for old premium licenses (#13619) Signed-off-by: Flavien DELANGLE Co-authored-by: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com> --- .../x-license/src/Watermark/Watermark.tsx | 3 +- .../x-license/src/useLicenseVerifier/index.ts | 1 - .../useLicenseVerifier/useLicenseVerifier.ts | 15 +-- .../x-license/src/utils/commercialPackages.ts | 6 + packages/x-license/src/utils/index.ts | 1 + packages/x-license/src/utils/licenseScope.ts | 15 --- .../src/verifyLicense/verifyLicense.test.ts | 104 ++++++++++-------- .../src/verifyLicense/verifyLicense.ts | 46 ++++++-- 8 files changed, 103 insertions(+), 88 deletions(-) create mode 100644 packages/x-license/src/utils/commercialPackages.ts diff --git a/packages/x-license/src/Watermark/Watermark.tsx b/packages/x-license/src/Watermark/Watermark.tsx index b970d5a12fc49..ff1eafba92983 100644 --- a/packages/x-license/src/Watermark/Watermark.tsx +++ b/packages/x-license/src/Watermark/Watermark.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; -import { MuiCommercialPackageName, useLicenseVerifier } from '../useLicenseVerifier'; +import { useLicenseVerifier } from '../useLicenseVerifier'; import { LICENSE_STATUS, LicenseStatus } from '../utils/licenseStatus'; +import { MuiCommercialPackageName } from '../utils/commercialPackages'; function getLicenseErrorMessage(licenseStatus: LicenseStatus) { switch (licenseStatus) { diff --git a/packages/x-license/src/useLicenseVerifier/index.ts b/packages/x-license/src/useLicenseVerifier/index.ts index e8e4693717d89..ef9135804f698 100644 --- a/packages/x-license/src/useLicenseVerifier/index.ts +++ b/packages/x-license/src/useLicenseVerifier/index.ts @@ -1,2 +1 @@ export { useLicenseVerifier } from './useLicenseVerifier'; -export type { MuiCommercialPackageName } from './useLicenseVerifier'; diff --git a/packages/x-license/src/useLicenseVerifier/useLicenseVerifier.ts b/packages/x-license/src/useLicenseVerifier/useLicenseVerifier.ts index d85e4477ef618..72fb2bf48f100 100644 --- a/packages/x-license/src/useLicenseVerifier/useLicenseVerifier.ts +++ b/packages/x-license/src/useLicenseVerifier/useLicenseVerifier.ts @@ -11,15 +11,8 @@ import { showNotAvailableInInitialProPlanError, } from '../utils/licenseErrorMessageUtils'; import { LICENSE_STATUS, LicenseStatus } from '../utils/licenseStatus'; -import { extractAcceptedScopes, extractProductScope } from '../utils/licenseScope'; import MuiLicenseInfoContext from '../Unstable_LicenseInfoProvider/MuiLicenseInfoContext'; - -export type MuiCommercialPackageName = - | 'x-data-grid-pro' - | 'x-data-grid-premium' - | 'x-date-pickers-pro' - | 'x-tree-view-pro' - | 'x-charts-pro'; +import { MuiCommercialPackageName } from '../utils/commercialPackages'; export const sharedLicenseStatuses: { [packageName in MuiCommercialPackageName]?: { @@ -48,15 +41,11 @@ export function useLicenseVerifier( return sharedLicenseStatuses[packageName]!.licenseVerifier; } - const acceptedScopes = extractAcceptedScopes(packageName); - const productScope = extractProductScope(packageName); - const plan = packageName.includes('premium') ? 'Premium' : 'Pro'; const licenseStatus = verifyLicense({ releaseInfo, licenseKey, - acceptedScopes, - productScope, + packageName, }); const fullPackageName = `@mui/${packageName}`; diff --git a/packages/x-license/src/utils/commercialPackages.ts b/packages/x-license/src/utils/commercialPackages.ts new file mode 100644 index 0000000000000..ae4e676b57327 --- /dev/null +++ b/packages/x-license/src/utils/commercialPackages.ts @@ -0,0 +1,6 @@ +export type MuiCommercialPackageName = + | 'x-data-grid-pro' + | 'x-data-grid-premium' + | 'x-date-pickers-pro' + | 'x-tree-view-pro' + | 'x-charts-pro'; diff --git a/packages/x-license/src/utils/index.ts b/packages/x-license/src/utils/index.ts index 20a11f9c8cd89..7197239c1e953 100644 --- a/packages/x-license/src/utils/index.ts +++ b/packages/x-license/src/utils/index.ts @@ -3,3 +3,4 @@ export * from './licenseInfo'; export * from './licenseStatus'; export type { LicenseScope } from './licenseScope'; export type { LicensingModel } from './licensingModel'; +export type { MuiCommercialPackageName } from './commercialPackages'; diff --git a/packages/x-license/src/utils/licenseScope.ts b/packages/x-license/src/utils/licenseScope.ts index d75a3b56607aa..f998b354f7b0a 100644 --- a/packages/x-license/src/utils/licenseScope.ts +++ b/packages/x-license/src/utils/licenseScope.ts @@ -1,20 +1,5 @@ export const LICENSE_SCOPES = ['pro', 'premium'] as const; -export const PRODUCT_SCOPES = ['data-grid', 'date-pickers', 'charts', 'tree-view'] as const; export const PLAN_VERSIONS = ['initial', 'Q3-2024'] as const; export type LicenseScope = (typeof LICENSE_SCOPES)[number]; -export type ProductScope = (typeof PRODUCT_SCOPES)[number]; export type PlanVersion = (typeof PLAN_VERSIONS)[number]; - -export const extractProductScope = (packageName: string): ProductScope => { - // extract the part between "x-" and "-pro"/"-premium" - const regex = /x-(.*?)(-pro|-premium)?$/; - const match = packageName.match(regex); - return match![1] as ProductScope; -}; - -export const extractAcceptedScopes = (packageName: string): readonly LicenseScope[] => { - return packageName.includes('premium') - ? LICENSE_SCOPES.filter((scope) => scope.includes('premium')) - : LICENSE_SCOPES; -}; diff --git a/packages/x-license/src/verifyLicense/verifyLicense.test.ts b/packages/x-license/src/verifyLicense/verifyLicense.test.ts index 3e040c782a170..3b096e31abae1 100644 --- a/packages/x-license/src/verifyLicense/verifyLicense.test.ts +++ b/packages/x-license/src/verifyLicense/verifyLicense.test.ts @@ -30,8 +30,7 @@ describe('License: verifyLicense', () => { verifyLicense({ releaseInfo: '__RELEASE_INFO__', licenseKey, - acceptedScopes: ['pro', 'premium'], - productScope: 'data-grid', + packageName: 'x-data-grid-pro', }).status, ).to.throw('MUI X: The release information is invalid. Not able to validate license.'); }); @@ -42,8 +41,7 @@ describe('License: verifyLicense', () => { verifyLicense({ releaseInfo: RELEASE_INFO, licenseKey, - acceptedScopes: ['pro', 'premium'], - productScope: 'data-grid', + packageName: 'x-data-grid-pro', }).status, ).to.equal(LICENSE_STATUS.Valid); }); @@ -62,8 +60,7 @@ describe('License: verifyLicense', () => { verifyLicense({ releaseInfo: RELEASE_INFO, licenseKey: expiredLicenseKey, - acceptedScopes: ['pro', 'premium'], - productScope: 'data-grid', + packageName: 'x-data-grid-pro', }).status, ).to.equal(LICENSE_STATUS.ExpiredVersion); }); @@ -75,8 +72,7 @@ describe('License: verifyLicense', () => { releaseInfo: RELEASE_INFO, licenseKey: 'b43ff5f9ac93f021855ff59ff0ba5220TkFNRTpNYC1VSSBTQVMsREVWRUxPUEVSX0NPVU5UPTEwLEVYUElSWT0xNTkxNzIzMDY3MDQyLFZFUlNJT049MS4yLjM', - acceptedScopes: ['pro', 'premium'], - productScope: 'data-grid', + packageName: 'x-data-grid-pro', }).status, ).to.equal(LICENSE_STATUS.Invalid); }); @@ -106,8 +102,7 @@ describe('License: verifyLicense', () => { verifyLicense({ releaseInfo: '__RELEASE_INFO__', licenseKey: licenseKeyPro, - acceptedScopes: ['pro', 'premium'], - productScope: 'data-grid', + packageName: 'x-data-grid-pro', }).status, ).to.throw('MUI X: The release information is invalid. Not able to validate license.'); }); @@ -119,8 +114,7 @@ describe('License: verifyLicense', () => { verifyLicense({ releaseInfo: RELEASE_INFO, licenseKey: licenseKeyPro, - acceptedScopes: ['pro', 'premium'], - productScope: 'data-grid', + packageName: 'x-data-grid-pro', }).status, ).to.equal(LICENSE_STATUS.Valid); }); @@ -131,8 +125,7 @@ describe('License: verifyLicense', () => { verifyLicense({ releaseInfo: RELEASE_INFO, licenseKey: licenseKeyPremium, - acceptedScopes: ['premium'], - productScope: 'data-grid', + packageName: 'x-data-grid-premium', }).status, ).to.equal(LICENSE_STATUS.Valid); }); @@ -143,8 +136,7 @@ describe('License: verifyLicense', () => { verifyLicense({ releaseInfo: RELEASE_INFO, licenseKey: licenseKeyPro, - acceptedScopes: ['premium'], - productScope: 'data-grid', + packageName: 'x-data-grid-premium', }).status, ).to.equal(LICENSE_STATUS.OutOfScope); }); @@ -165,8 +157,7 @@ describe('License: verifyLicense', () => { verifyLicense({ releaseInfo: RELEASE_INFO, licenseKey: expiredLicenseKey, - acceptedScopes: ['pro', 'premium'], - productScope: 'data-grid', + packageName: 'x-data-grid-pro', }).status, ).to.equal(LICENSE_STATUS.Valid); }); @@ -184,8 +175,7 @@ describe('License: verifyLicense', () => { verifyLicense({ releaseInfo: RELEASE_INFO, licenseKey: expiredLicenseKey, - acceptedScopes: ['pro', 'premium'], - productScope: 'data-grid', + packageName: 'x-data-grid-pro', }).status, ).to.equal(LICENSE_STATUS.ExpiredAnnualGrace); }); @@ -204,8 +194,7 @@ describe('License: verifyLicense', () => { verifyLicense({ releaseInfo: RELEASE_INFO, licenseKey: expiredLicenseKey, - acceptedScopes: ['pro', 'premium'], - productScope: 'data-grid', + packageName: 'x-data-grid-pro', }).status, ).to.equal(LICENSE_STATUS.ExpiredAnnual); }); @@ -223,8 +212,7 @@ describe('License: verifyLicense', () => { verifyLicense({ releaseInfo: RELEASE_INFO, licenseKey: expiredLicenseKey, - acceptedScopes: ['pro', 'premium'], - productScope: 'data-grid', + packageName: 'x-data-grid-pro', }).status, ).to.equal(LICENSE_STATUS.Valid); }); @@ -237,8 +225,7 @@ describe('License: verifyLicense', () => { releaseInfo: RELEASE_INFO, licenseKey: 'b43ff5f9ac93f021855ff59ff0ba5220TkFNRTpNYC1VSSBTQVMsREVWRUxPUEVSX0NPVU5UPTEwLEVYUElSWT0xNTkxNzIzMDY3MDQyLFZFUlNJT049MS4yLjM', - acceptedScopes: ['pro', 'premium'], - productScope: 'data-grid', + packageName: 'x-data-grid-pro', }).status, ).to.equal(LICENSE_STATUS.Invalid); }); @@ -259,15 +246,14 @@ describe('License: verifyLicense', () => { verifyLicense({ releaseInfo: RELEASE_INFO, licenseKey: licenseKeyPro, - acceptedScopes: ['pro', 'premium'], - productScope: 'data-grid', + packageName: 'x-data-grid-pro', }).status, ).to.equal(LICENSE_STATUS.Valid); }); }); describe('key version: 2.2', () => { - const licenseKeyInitial = generateLicense({ + const proLicenseKeyInitial = generateLicense({ expiryDate: new Date(releaseDate.getTime() + oneDayInMS), orderNumber: 'MUI-123', scope: 'pro', @@ -275,7 +261,15 @@ describe('License: verifyLicense', () => { planVersion: 'initial', }); - const licenseKey2 = generateLicense({ + const premiumLicenseKeyInitial = generateLicense({ + expiryDate: new Date(releaseDate.getTime() + oneDayInMS), + orderNumber: 'MUI-123', + scope: 'premium', + licensingModel: 'annual', + planVersion: 'initial', + }); + + const proLicenseKeyQ32024 = generateLicense({ expiryDate: new Date(releaseDate.getTime() + oneDayInMS), orderNumber: 'MUI-123', scope: 'pro', @@ -283,50 +277,68 @@ describe('License: verifyLicense', () => { planVersion: 'Q3-2024', }); - it('PlanVersion "initial" should not accept charts', () => { + it('PlanVersion "initial" should not accept x-charts-pro', () => { process.env.NODE_ENV = 'production'; expect( verifyLicense({ releaseInfo: RELEASE_INFO, - licenseKey: licenseKeyInitial, - acceptedScopes: ['pro', 'premium'], - productScope: 'charts', + licenseKey: proLicenseKeyInitial, + packageName: 'x-charts-pro', }).status, ).to.equal(LICENSE_STATUS.NotAvailableInInitialProPlan); }); - it('PlanVersion "initial" should not accept tree-view', () => { + it('PlanVersion "initial" should not accept x-tree-view-pro', () => { process.env.NODE_ENV = 'production'; expect( verifyLicense({ releaseInfo: RELEASE_INFO, - licenseKey: licenseKeyInitial, - acceptedScopes: ['pro', 'premium'], - productScope: 'tree-view', + licenseKey: proLicenseKeyInitial, + packageName: 'x-tree-view-pro', }).status, ).to.equal(LICENSE_STATUS.NotAvailableInInitialProPlan); }); - it('PlanVersion "Q3-2024" should accept charts', () => { + it('PlanVersion "Q3-2024" should accept x-charts-pro', () => { + process.env.NODE_ENV = 'production'; + expect( + verifyLicense({ + releaseInfo: RELEASE_INFO, + licenseKey: proLicenseKeyQ32024, + packageName: 'x-charts-pro', + }).status, + ).to.equal(LICENSE_STATUS.Valid); + }); + + it('PlanVersion "Q3-2024" should accept x-tree-view-pro', () => { + process.env.NODE_ENV = 'production'; + expect( + verifyLicense({ + releaseInfo: RELEASE_INFO, + licenseKey: proLicenseKeyQ32024, + packageName: 'x-tree-view-pro', + }).status, + ).to.equal(LICENSE_STATUS.Valid); + }); + + it('Premium with planVersion "initial" should accept x-tree-view-pro', () => { process.env.NODE_ENV = 'production'; expect( verifyLicense({ releaseInfo: RELEASE_INFO, - licenseKey: licenseKey2, - acceptedScopes: ['pro', 'premium'], - productScope: 'charts', + licenseKey: premiumLicenseKeyInitial, + packageName: 'x-tree-view-pro', }).status, ).to.equal(LICENSE_STATUS.Valid); }); - it('PlanVersion "Q3-2024" should accept tree-view', () => { + it('Premium with planVersion "initial" should accept x-charts-pro', () => { process.env.NODE_ENV = 'production'; expect( verifyLicense({ releaseInfo: RELEASE_INFO, - licenseKey: licenseKey2, - acceptedScopes: ['pro', 'premium'], - productScope: 'tree-view', + licenseKey: premiumLicenseKeyInitial, + packageName: 'x-charts-pro', }).status, ).to.equal(LICENSE_STATUS.Valid); }); diff --git a/packages/x-license/src/verifyLicense/verifyLicense.ts b/packages/x-license/src/verifyLicense/verifyLicense.ts index ed9b936357cba..8aa5d8915d5ac 100644 --- a/packages/x-license/src/verifyLicense/verifyLicense.ts +++ b/packages/x-license/src/verifyLicense/verifyLicense.ts @@ -1,8 +1,9 @@ import { base64Decode, base64Encode } from '../encoding/base64'; import { md5 } from '../encoding/md5'; import { LICENSE_STATUS, LicenseStatus } from '../utils/licenseStatus'; -import { LicenseScope, LICENSE_SCOPES, ProductScope, PlanVersion } from '../utils/licenseScope'; +import { LicenseScope, LICENSE_SCOPES, PlanVersion } from '../utils/licenseScope'; import { LicensingModel, LICENSING_MODELS } from '../utils/licensingModel'; +import { MuiCommercialPackageName } from '../utils/commercialPackages'; const getDefaultReleaseDate = () => { const today = new Date(); @@ -15,6 +16,22 @@ export function generateReleaseInfo(releaseDate = getDefaultReleaseDate()) { return base64Encode(releaseDate.getTime().toString()); } +function isLicenseScopeSufficient( + packageName: MuiCommercialPackageName, + licenseScope: LicenseScope, +) { + let acceptedScopes: LicenseScope[]; + if (packageName.includes('-pro')) { + acceptedScopes = ['pro', 'premium']; + } else if (packageName.includes('-premium')) { + acceptedScopes = ['premium']; + } else { + acceptedScopes = []; + } + + return acceptedScopes.includes(licenseScope); +} + const expiryReg = /^.*EXPIRY=([0-9]+),.*$/; interface MuiLicense { @@ -24,6 +41,11 @@ interface MuiLicense { planVersion: PlanVersion; } +const PRO_PACKAGES_AVAILABLE_IN_INITIAL_PRO_PLAN: MuiCommercialPackageName[] = [ + 'x-data-grid-pro', + 'x-date-pickers-pro', +]; + /** * Format: ORDER:${orderNumber},EXPIRY=${expiryTimestamp},KEYVERSION=1 */ @@ -105,13 +127,11 @@ const decodeLicense = (encodedLicense: string): MuiLicense | null => { export function verifyLicense({ releaseInfo, licenseKey, - acceptedScopes, - productScope, + packageName, }: { releaseInfo: string; licenseKey?: string; - acceptedScopes: readonly LicenseScope[]; - productScope: ProductScope; + packageName: MuiCommercialPackageName; }): { status: LicenseStatus; meta?: any } { if (!releaseInfo) { throw new Error('MUI X: The release information is missing. Not able to validate license.'); @@ -178,15 +198,17 @@ export function verifyLicense({ return { status: LICENSE_STATUS.Invalid }; } - if (license.planVersion === 'initial') { - // 'charts-pro' or 'tree-view-pro' can only be used with a newer license - if (productScope === 'charts' || productScope === 'tree-view') { - return { status: LICENSE_STATUS.NotAvailableInInitialProPlan }; - } + if (!isLicenseScopeSufficient(packageName, license.scope)) { + return { status: LICENSE_STATUS.OutOfScope }; } - if (!acceptedScopes.includes(license.scope)) { - return { status: LICENSE_STATUS.OutOfScope }; + // 'charts-pro' or 'tree-view-pro' can only be used with a newer Pro license + if ( + license.planVersion === 'initial' && + license.scope === 'pro' && + !PRO_PACKAGES_AVAILABLE_IN_INITIAL_PRO_PLAN.includes(packageName) + ) { + return { status: LICENSE_STATUS.NotAvailableInInitialProPlan }; } return { status: LICENSE_STATUS.Valid };