diff --git a/packages/x-data-grid-premium/src/tests/license.DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/license.DataGridPremium.test.tsx index 62b51732d8c0..1773384ed9a0 100644 --- a/packages/x-data-grid-premium/src/tests/license.DataGridPremium.test.tsx +++ b/packages/x-data-grid-premium/src/tests/license.DataGridPremium.test.tsx @@ -15,6 +15,7 @@ describe(' - License', () => { orderNumber: 'Test', licensingModel: 'subscription', scope: 'pro', + planVersion: 'initial', }), ); expect(() => render()).toErrorDev([ diff --git a/packages/x-license/src/Watermark/Watermark.tsx b/packages/x-license/src/Watermark/Watermark.tsx index 92347c80bd6b..da41daf373fd 100644 --- a/packages/x-license/src/Watermark/Watermark.tsx +++ b/packages/x-license/src/Watermark/Watermark.tsx @@ -13,6 +13,8 @@ function getLicenseErrorMessage(licenseStatus: LicenseStatus) { return 'MUI X Invalid license key'; case LICENSE_STATUS.OutOfScope: return 'MUI X License key plan mismatch'; + case LICENSE_STATUS.ProductNotCovered: + return 'MUI X Product not covered by plan'; case LICENSE_STATUS.NotFound: return 'MUI X Missing license key'; default: diff --git a/packages/x-license/src/generateLicense/generateLicense.test.ts b/packages/x-license/src/generateLicense/generateLicense.test.ts index 5a27b7880364..90ea9768276d 100644 --- a/packages/x-license/src/generateLicense/generateLicense.test.ts +++ b/packages/x-license/src/generateLicense/generateLicense.test.ts @@ -9,9 +9,10 @@ describe('License: generateLicense', () => { orderNumber: 'MUI-123', scope: 'pro', licensingModel: 'subscription', + planVersion: 'initial', }), ).to.equal( - 'b2b2ea9c6fd846e11770da3c795d6f63Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1zdWJzY3JpcHRpb24sS1Y9Mg==', + 'e8fad422a82720084ec67dd693f08056Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1zdWJzY3JpcHRpb24sUFY9aW5pdGlhbCxLVj0y', ); }); @@ -22,9 +23,10 @@ describe('License: generateLicense', () => { orderNumber: 'MUI-123', scope: 'premium', licensingModel: 'subscription', + planVersion: 'initial', }), ).to.equal( - 'ac8d20b4ecd1f919157f3713f8ba1651Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLEtWPTI=', + '8ca0384bfb92ec214d4cd72483f5110bTz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLFBWPWluaXRpYWwsS1Y9Mg==', ); }); @@ -35,9 +37,10 @@ describe('License: generateLicense', () => { orderNumber: 'MUI-123', scope: 'pro', licensingModel: 'subscription', + planVersion: 'initial', }), ).to.equal( - 'b2b2ea9c6fd846e11770da3c795d6f63Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1zdWJzY3JpcHRpb24sS1Y9Mg==', + 'e8fad422a82720084ec67dd693f08056Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1zdWJzY3JpcHRpb24sUFY9aW5pdGlhbCxLVj0y', ); }); @@ -48,9 +51,38 @@ describe('License: generateLicense', () => { orderNumber: 'MUI-123', scope: 'pro', licensingModel: 'perpetual', + planVersion: 'initial', }), ).to.equal( - 'b16edd8e6bc83293a723779a259f520cTz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1wZXJwZXR1YWwsS1Y9Mg==', + 'aaf2e3c60b06199962fbbab985843d97Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1wZXJwZXR1YWwsUFY9aW5pdGlhbCxLVj0y', + ); + }); + + it('should generate subscription Pro license when `planVersion: "Q3-2024"`', () => { + expect( + generateLicense({ + expiryDate: new Date(1591723879062), + orderNumber: 'MUI-123', + scope: 'pro', + licensingModel: 'subscription', + planVersion: 'Q3-2024', + }), + ).to.equal( + '4adf08e54d606215809064d1d31b6b39Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1zdWJzY3JpcHRpb24sUFY9UTMtMjAyNCxLVj0y', + ); + }); + + it('should generate subscription Premium license when `planVersion: "Q3-2024"`', () => { + expect( + generateLicense({ + expiryDate: new Date(1591723879062), + orderNumber: 'MUI-123', + scope: 'premium', + licensingModel: 'subscription', + planVersion: 'Q3-2024', + }), + ).to.equal( + 'b76c2067275b3b566fcae1d28ad23c91Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLFBWPVEzLTIwMjQsS1Y9Mg==', ); }); }); diff --git a/packages/x-license/src/generateLicense/generateLicense.ts b/packages/x-license/src/generateLicense/generateLicense.ts index 9ff33495f713..badebb90d782 100644 --- a/packages/x-license/src/generateLicense/generateLicense.ts +++ b/packages/x-license/src/generateLicense/generateLicense.ts @@ -1,6 +1,6 @@ import { md5 } from '../encoding/md5'; import { base64Encode } from '../encoding/base64'; -import { LICENSE_SCOPES, LicenseScope } from '../utils/licenseScope'; +import { LICENSE_SCOPES, LicenseScope, PlanVersion } from '../utils/licenseScope'; import { LICENSING_MODELS, LicensingModel } from '../utils/licensingModel'; const licenseVersion = '2'; @@ -10,6 +10,7 @@ export interface LicenseDetails { expiryDate: Date; scope: LicenseScope; licensingModel: LicensingModel; + planVersion: PlanVersion; } function getClearLicenseString(details: LicenseDetails) { @@ -21,9 +22,16 @@ function getClearLicenseString(details: LicenseDetails) { throw new Error('MUI X: Invalid licensing model'); } - return `O=${details.orderNumber},E=${details.expiryDate.getTime()},S=${details.scope},LM=${ - details.licensingModel - },KV=${licenseVersion}`; + const keyParts = [ + `O=${details.orderNumber}`, + `E=${details.expiryDate.getTime()}`, + `S=${details.scope}`, + `LM=${details.licensingModel}`, + `PV=${details.planVersion}`, + `KV=${licenseVersion}`, + ]; + + return keyParts.join(','); } export function generateLicense(details: LicenseDetails) { diff --git a/packages/x-license/src/useLicenseVerifier/useLicenseVerifier.test.tsx b/packages/x-license/src/useLicenseVerifier/useLicenseVerifier.test.tsx index ece7b7567f65..bbb04d1804a7 100644 --- a/packages/x-license/src/useLicenseVerifier/useLicenseVerifier.test.tsx +++ b/packages/x-license/src/useLicenseVerifier/useLicenseVerifier.test.tsx @@ -6,6 +6,7 @@ import { LicenseInfo, generateLicense, Unstable_LicenseInfoProvider as LicenseInfoProvider, + MuiCommercialPackageName, } from '@mui/x-license'; import { sharedLicenseStatuses } from './useLicenseVerifier'; import { generateReleaseInfo } from '../verifyLicense'; @@ -14,9 +15,9 @@ const oneDayInMS = 1000 * 60 * 60 * 24; const releaseDate = new Date(3000, 0, 0, 0, 0, 0, 0); const RELEASE_INFO = generateReleaseInfo(releaseDate); -function TestComponent() { - const licesenStatus = useLicenseVerifier('x-date-pickers-pro', RELEASE_INFO); - return
Status: {licesenStatus.status}
; +function TestComponent(props: { packageName?: MuiCommercialPackageName }) { + const licenseStatus = useLicenseVerifier(props.packageName || 'x-date-pickers-pro', RELEASE_INFO); + return
Status: {licenseStatus.status}
; } describe('useLicenseVerifier', function test() { @@ -63,6 +64,7 @@ describe('useLicenseVerifier', function test() { licensingModel: 'perpetual', orderNumber: '12345', scope: 'pro', + planVersion: 'initial', }); LicenseInfo.setLicenseKey(''); @@ -88,6 +90,7 @@ describe('useLicenseVerifier', function test() { orderNumber: 'MUI-123', scope: 'pro', licensingModel: 'subscription', + planVersion: 'initial', }); LicenseInfo.setLicenseKey(expiredLicenseKey); @@ -105,5 +108,81 @@ describe('useLicenseVerifier', function test() { ]); expect(actualErrorMsg).to.match(/MUI X: Expired license key/); }); + + it('should throw if the license is not covering charts and tree-view', () => { + // Avoid Karma "Invalid left-hand side in assignment" SyntaxError + // eslint-disable-next-line no-useless-concat + process.env['NODE_' + 'ENV'] = 'development'; + + const licenseKey = generateLicense({ + expiryDate: new Date(3001, 0, 0, 0, 0, 0, 0), + orderNumber: 'MUI-123', + scope: 'pro', + licensingModel: 'subscription', + planVersion: 'initial', + }); + + LicenseInfo.setLicenseKey(licenseKey); + + expect(() => { + render(); + }).to.toErrorDev(['MUI X: Product not covered by plan.']); + + expect(() => { + render(); + }).to.toErrorDev(['MUI X: Product not covered by plan.']); + }); + + it('should not throw if the license is covering charts and tree-view', () => { + // Avoid Karma "Invalid left-hand side in assignment" SyntaxError + // eslint-disable-next-line no-useless-concat + process.env['NODE_' + 'ENV'] = 'development'; + + const licenseKey = generateLicense({ + expiryDate: new Date(3001, 0, 0, 0, 0, 0, 0), + orderNumber: 'MUI-123', + scope: 'pro', + licensingModel: 'subscription', + planVersion: 'Q3-2024', + }); + + LicenseInfo.setLicenseKey(licenseKey); + + expect(() => { + render(); + }).not.toErrorDev(); + + expect(() => { + render(); + }).not.toErrorDev(); + }); + + it('should not throw for existing pro and premium packages', () => { + // Avoid Karma "Invalid left-hand side in assignment" SyntaxError + // eslint-disable-next-line no-useless-concat + process.env['NODE_' + 'ENV'] = 'development'; + + const licenseKey = generateLicense({ + expiryDate: new Date(3001, 0, 0, 0, 0, 0, 0), + orderNumber: 'MUI-123', + scope: 'premium', + licensingModel: 'subscription', + planVersion: 'Q3-2024', + }); + + LicenseInfo.setLicenseKey(licenseKey); + + expect(() => { + render(); + }).not.toErrorDev(); + + expect(() => { + render(); + }).not.toErrorDev(); + + expect(() => { + render(); + }).not.toErrorDev(); + }); }); }); diff --git a/packages/x-license/src/useLicenseVerifier/useLicenseVerifier.ts b/packages/x-license/src/useLicenseVerifier/useLicenseVerifier.ts index 84151d4acc32..a79cc012b389 100644 --- a/packages/x-license/src/useLicenseVerifier/useLicenseVerifier.ts +++ b/packages/x-license/src/useLicenseVerifier/useLicenseVerifier.ts @@ -8,9 +8,10 @@ import { showMissingLicenseKeyError, showLicenseKeyPlanMismatchError, showExpiredPackageVersionError, + showProductNotCoveredError, } from '../utils/licenseErrorMessageUtils'; import { LICENSE_STATUS, LicenseStatus } from '../utils/licenseStatus'; -import { LicenseScope } from '../utils/licenseScope'; +import { extractAcceptedScopes, extractProductScope } from '../utils/licenseScope'; import MuiLicenseInfoContext from '../Unstable_LicenseInfoProvider/MuiLicenseInfoContext'; export type MuiCommercialPackageName = @@ -47,15 +48,15 @@ export function useLicenseVerifier( return sharedLicenseStatuses[packageName]!.licenseVerifier; } - const acceptedScopes: LicenseScope[] = packageName.includes('premium') - ? ['premium'] - : ['pro', 'premium']; + const acceptedScopes = extractAcceptedScopes(packageName); + const productScope = extractProductScope(packageName); const plan = packageName.includes('premium') ? 'Premium' : 'Pro'; const licenseStatus = verifyLicense({ releaseInfo, licenseKey, acceptedScopes, + productScope, }); const fullPackageName = `@mui/${packageName}`; @@ -64,6 +65,8 @@ export function useLicenseVerifier( // Skip } else if (licenseStatus.status === LICENSE_STATUS.Invalid) { showInvalidLicenseKeyError(); + } else if (licenseStatus.status === LICENSE_STATUS.ProductNotCovered) { + showProductNotCoveredError(); } else if (licenseStatus.status === LICENSE_STATUS.OutOfScope) { showLicenseKeyPlanMismatchError(); } else if (licenseStatus.status === LICENSE_STATUS.NotFound) { diff --git a/packages/x-license/src/utils/licenseErrorMessageUtils.ts b/packages/x-license/src/utils/licenseErrorMessageUtils.ts index 9924da8404b5..1c29bfb5f9e4 100644 --- a/packages/x-license/src/utils/licenseErrorMessageUtils.ts +++ b/packages/x-license/src/utils/licenseErrorMessageUtils.ts @@ -32,6 +32,16 @@ export function showLicenseKeyPlanMismatchError() { ]); } +export function showProductNotCoveredError() { + showError([ + 'MUI X: Product not covered by plan.', + '', + 'The component you are trying to use is not included in the Pro Plan your purchased. You are using a license that is only compatible with the `@mui/x-data-grid-pro` and `@mui/x-date-pickers-pro` commercial packages.', + '', + 'To start using another Pro package, please consider reaching to our sales team to upgrade your license or visit https://mui.com/r/x-get-license to get a new license key.', + ]); +} + export function showMissingLicenseKeyError({ plan, packageName, diff --git a/packages/x-license/src/utils/licenseScope.ts b/packages/x-license/src/utils/licenseScope.ts index 74e27de02161..d75a3b56607a 100644 --- a/packages/x-license/src/utils/licenseScope.ts +++ b/packages/x-license/src/utils/licenseScope.ts @@ -1,3 +1,20 @@ 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/utils/licenseStatus.ts b/packages/x-license/src/utils/licenseStatus.ts index 77a27e1d7357..ac6828c109e2 100644 --- a/packages/x-license/src/utils/licenseStatus.ts +++ b/packages/x-license/src/utils/licenseStatus.ts @@ -7,6 +7,7 @@ export enum LICENSE_STATUS { ExpiredVersion = 'ExpiredVersion', Valid = 'Valid', OutOfScope = 'OutOfScope', + ProductNotCovered = 'ProductNotCovered', } export type LicenseStatus = keyof typeof LICENSE_STATUS; diff --git a/packages/x-license/src/verifyLicense/verifyLicense.test.ts b/packages/x-license/src/verifyLicense/verifyLicense.test.ts index 7f916614ab14..1b2c2fe24eb3 100644 --- a/packages/x-license/src/verifyLicense/verifyLicense.test.ts +++ b/packages/x-license/src/verifyLicense/verifyLicense.test.ts @@ -31,6 +31,7 @@ describe('License: verifyLicense', () => { releaseInfo: '__RELEASE_INFO__', licenseKey, acceptedScopes: ['pro', 'premium'], + productScope: 'data-grid', }).status, ).to.throw('MUI X: The release information is invalid. Not able to validate license.'); }); @@ -42,6 +43,7 @@ describe('License: verifyLicense', () => { releaseInfo: RELEASE_INFO, licenseKey, acceptedScopes: ['pro', 'premium'], + productScope: 'data-grid', }).status, ).to.equal(LICENSE_STATUS.Valid); }); @@ -53,6 +55,7 @@ describe('License: verifyLicense', () => { scope: 'pro', licensingModel: 'perpetual', orderNumber: 'MUI-123', + planVersion: 'initial', }); expect( @@ -60,6 +63,7 @@ describe('License: verifyLicense', () => { releaseInfo: RELEASE_INFO, licenseKey: expiredLicenseKey, acceptedScopes: ['pro', 'premium'], + productScope: 'data-grid', }).status, ).to.equal(LICENSE_STATUS.ExpiredVersion); }); @@ -72,6 +76,7 @@ describe('License: verifyLicense', () => { licenseKey: 'b43ff5f9ac93f021855ff59ff0ba5220TkFNRTpNYC1VSSBTQVMsREVWRUxPUEVSX0NPVU5UPTEwLEVYUElSWT0xNTkxNzIzMDY3MDQyLFZFUlNJT049MS4yLjM', acceptedScopes: ['pro', 'premium'], + productScope: 'data-grid', }).status, ).to.equal(LICENSE_STATUS.Invalid); }); @@ -83,6 +88,7 @@ describe('License: verifyLicense', () => { orderNumber: 'MUI-123', scope: 'pro', licensingModel: 'subscription', + planVersion: 'initial', }); const licenseKeyPremium = generateLicense({ @@ -90,6 +96,7 @@ describe('License: verifyLicense', () => { orderNumber: 'MUI-123', scope: 'premium', licensingModel: 'subscription', + planVersion: 'initial', }); it('should log an error when ReleaseInfo is not valid', () => { @@ -100,6 +107,7 @@ describe('License: verifyLicense', () => { releaseInfo: '__RELEASE_INFO__', licenseKey: licenseKeyPro, acceptedScopes: ['pro', 'premium'], + productScope: 'data-grid', }).status, ).to.throw('MUI X: The release information is invalid. Not able to validate license.'); }); @@ -112,6 +120,7 @@ describe('License: verifyLicense', () => { releaseInfo: RELEASE_INFO, licenseKey: licenseKeyPro, acceptedScopes: ['pro', 'premium'], + productScope: 'data-grid', }).status, ).to.equal(LICENSE_STATUS.Valid); }); @@ -123,6 +132,7 @@ describe('License: verifyLicense', () => { releaseInfo: RELEASE_INFO, licenseKey: licenseKeyPremium, acceptedScopes: ['premium'], + productScope: 'data-grid', }).status, ).to.equal(LICENSE_STATUS.Valid); }); @@ -134,6 +144,7 @@ describe('License: verifyLicense', () => { releaseInfo: RELEASE_INFO, licenseKey: licenseKeyPro, acceptedScopes: ['premium'], + productScope: 'data-grid', }).status, ).to.equal(LICENSE_STATUS.OutOfScope); }); @@ -147,6 +158,7 @@ describe('License: verifyLicense', () => { orderNumber: 'MUI-123', scope: 'pro', licensingModel: 'subscription', + planVersion: 'initial', }); expect( @@ -154,6 +166,7 @@ describe('License: verifyLicense', () => { releaseInfo: RELEASE_INFO, licenseKey: expiredLicenseKey, acceptedScopes: ['pro', 'premium'], + productScope: 'data-grid', }).status, ).to.equal(LICENSE_STATUS.Valid); }); @@ -164,6 +177,7 @@ describe('License: verifyLicense', () => { orderNumber: 'MUI-123', scope: 'pro', licensingModel: 'subscription', + planVersion: 'initial', }); expect( @@ -171,6 +185,7 @@ describe('License: verifyLicense', () => { releaseInfo: RELEASE_INFO, licenseKey: expiredLicenseKey, acceptedScopes: ['pro', 'premium'], + productScope: 'data-grid', }).status, ).to.equal(LICENSE_STATUS.ExpiredAnnualGrace); }); @@ -182,6 +197,7 @@ describe('License: verifyLicense', () => { orderNumber: 'MUI-123', scope: 'pro', licensingModel: 'subscription', + planVersion: 'initial', }); expect( @@ -189,6 +205,7 @@ describe('License: verifyLicense', () => { releaseInfo: RELEASE_INFO, licenseKey: expiredLicenseKey, acceptedScopes: ['pro', 'premium'], + productScope: 'data-grid', }).status, ).to.equal(LICENSE_STATUS.ExpiredAnnual); }); @@ -199,6 +216,7 @@ describe('License: verifyLicense', () => { orderNumber: 'MUI-123', scope: 'pro', licensingModel: 'perpetual', + planVersion: 'initial', }); expect( @@ -206,6 +224,7 @@ describe('License: verifyLicense', () => { releaseInfo: RELEASE_INFO, licenseKey: expiredLicenseKey, acceptedScopes: ['pro', 'premium'], + productScope: 'data-grid', }).status, ).to.equal(LICENSE_STATUS.Valid); }); @@ -219,6 +238,7 @@ describe('License: verifyLicense', () => { licenseKey: 'b43ff5f9ac93f021855ff59ff0ba5220TkFNRTpNYC1VSSBTQVMsREVWRUxPUEVSX0NPVU5UPTEwLEVYUElSWT0xNTkxNzIzMDY3MDQyLFZFUlNJT049MS4yLjM', acceptedScopes: ['pro', 'premium'], + productScope: 'data-grid', }).status, ).to.equal(LICENSE_STATUS.Invalid); }); @@ -230,6 +250,7 @@ describe('License: verifyLicense', () => { orderNumber: 'MUI-123', scope: 'pro', licensingModel: 'annual', + planVersion: 'initial', }); it('should accept licensingModel="annual"', () => { @@ -239,6 +260,73 @@ describe('License: verifyLicense', () => { releaseInfo: RELEASE_INFO, licenseKey: licenseKeyPro, acceptedScopes: ['pro', 'premium'], + productScope: 'data-grid', + }).status, + ).to.equal(LICENSE_STATUS.Valid); + }); + }); + + describe('key version: 2.2', () => { + const licenseKeyInitial = generateLicense({ + expiryDate: new Date(releaseDate.getTime() + oneDayInMS), + orderNumber: 'MUI-123', + scope: 'pro', + licensingModel: 'annual', + planVersion: 'initial', + }); + + const licenseKey2 = generateLicense({ + expiryDate: new Date(releaseDate.getTime() + oneDayInMS), + orderNumber: 'MUI-123', + scope: 'pro', + licensingModel: 'annual', + planVersion: 'Q3-2024', + }); + + it('PlanVersion "initial" should not accept charts', () => { + process.env.NODE_ENV = 'production'; + expect( + verifyLicense({ + releaseInfo: RELEASE_INFO, + licenseKey: licenseKeyInitial, + acceptedScopes: ['pro', 'premium'], + productScope: 'charts', + }).status, + ).to.equal(LICENSE_STATUS.ProductNotCovered); + }); + + it('PlanVersion "initial" should not accept tree-view', () => { + process.env.NODE_ENV = 'production'; + expect( + verifyLicense({ + releaseInfo: RELEASE_INFO, + licenseKey: licenseKeyInitial, + acceptedScopes: ['pro', 'premium'], + productScope: 'tree-view', + }).status, + ).to.equal(LICENSE_STATUS.ProductNotCovered); + }); + + it('PlanVersion "Q3-2024" should accept charts', () => { + process.env.NODE_ENV = 'production'; + expect( + verifyLicense({ + releaseInfo: RELEASE_INFO, + licenseKey: licenseKey2, + acceptedScopes: ['pro', 'premium'], + productScope: 'charts', + }).status, + ).to.equal(LICENSE_STATUS.Valid); + }); + + it('PlanVersion "Q3-2024" should accept tree-view', () => { + process.env.NODE_ENV = 'production'; + expect( + verifyLicense({ + releaseInfo: RELEASE_INFO, + licenseKey: licenseKey2, + acceptedScopes: ['pro', 'premium'], + productScope: 'tree-view', }).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 b280b196dd7f..7d696490599d 100644 --- a/packages/x-license/src/verifyLicense/verifyLicense.ts +++ b/packages/x-license/src/verifyLicense/verifyLicense.ts @@ -1,7 +1,7 @@ import { base64Decode, base64Encode } from '../encoding/base64'; import { md5 } from '../encoding/md5'; import { LICENSE_STATUS, LicenseStatus } from '../utils/licenseStatus'; -import { LicenseScope, LICENSE_SCOPES } from '../utils/licenseScope'; +import { LicenseScope, LICENSE_SCOPES, ProductScope, PlanVersion } from '../utils/licenseScope'; import { LicensingModel, LICENSING_MODELS } from '../utils/licensingModel'; const getDefaultReleaseDate = () => { @@ -21,6 +21,7 @@ interface MuiLicense { licensingModel: LicensingModel | null; scope: LicenseScope | null; expiryTimestamp: number | null; + planVersion: PlanVersion; } /** @@ -41,17 +42,19 @@ const decodeLicenseVersion1 = (license: string): MuiLicense => { scope: 'pro', licensingModel: 'perpetual', expiryTimestamp, + planVersion: 'initial', }; }; /** - * Format: O=${orderNumber},E=${expiryTimestamp},S=${scope},LM=${licensingModel},KV=2`; + * Format: O=${orderNumber},E=${expiryTimestamp},S=${scope},LM=${licensingModel},PV=${planVersion},KV=2`; */ const decodeLicenseVersion2 = (license: string): MuiLicense => { const licenseInfo: MuiLicense = { scope: null, licensingModel: null, expiryTimestamp: null, + planVersion: 'initial', }; license @@ -73,6 +76,10 @@ const decodeLicenseVersion2 = (license: string): MuiLicense => { licenseInfo.expiryTimestamp = expiryTimestamp; } } + + if (key === 'PV') { + licenseInfo.planVersion = value as PlanVersion; + } }); return licenseInfo; @@ -99,10 +106,12 @@ export function verifyLicense({ releaseInfo, licenseKey, acceptedScopes, + productScope, }: { releaseInfo: string; - licenseKey: string | undefined; - acceptedScopes: LicenseScope[]; + licenseKey?: string; + acceptedScopes: readonly LicenseScope[]; + productScope: ProductScope; }): { status: LicenseStatus; meta?: any } { if (!releaseInfo) { throw new Error('MUI X: The release information is missing. Not able to validate license.'); @@ -165,10 +174,17 @@ export function verifyLicense({ } if (license.scope == null || !LICENSE_SCOPES.includes(license.scope)) { - console.error('Error checking license. scope not found or invalid!'); + console.error('MUI X: Error checking license. scope not found or invalid!'); 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.ProductNotCovered }; + } + } + if (!acceptedScopes.includes(license.scope)) { return { status: LICENSE_STATUS.OutOfScope }; } diff --git a/scripts/x-license.exports.json b/scripts/x-license.exports.json index 7de3fd6daabc..ba6c1bb8c50f 100644 --- a/scripts/x-license.exports.json +++ b/scripts/x-license.exports.json @@ -15,6 +15,7 @@ { "name": "showInvalidLicenseKeyError", "kind": "Function" }, { "name": "showLicenseKeyPlanMismatchError", "kind": "Function" }, { "name": "showMissingLicenseKeyError", "kind": "Function" }, + { "name": "showProductNotCoveredError", "kind": "Function" }, { "name": "Unstable_LicenseInfoProvider", "kind": "Function" }, { "name": "Unstable_LicenseInfoProviderProps", "kind": "Interface" }, { "name": "useLicenseVerifier", "kind": "Function" }, diff --git a/test/utils/testLicense.js b/test/utils/testLicense.js index 8a48c66f8b06..bf3064751157 100644 --- a/test/utils/testLicense.js +++ b/test/utils/testLicense.js @@ -10,6 +10,7 @@ export function generateTestLicenseKey() { scope: 'premium', orderNumber: 'MUI X tests', expiryDate, + planVersion: 'Q3-2024', }); }