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',
});
}