Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[x-license] Add support for plan version #13459

Merged
merged 25 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
190a18e
license update proposal
michelengelen Jun 12, 2024
1706016
Revert "license update proposal"
michelengelen Jun 13, 2024
93a4e9a
second iteration on license update
michelengelen Jun 13, 2024
8b4369e
3rd iteration
michelengelen Jun 13, 2024
f815941
changed status return on charts/treeview check
michelengelen Jun 13, 2024
3ccba21
removed tests for now
michelengelen Jun 13, 2024
46cf0d5
adjusted things and fixed tests
michelengelen Jun 13, 2024
da18b3f
review remarks
michelengelen Jun 14, 2024
ac5e804
removed productScope optional marking
michelengelen Jun 14, 2024
300e0b5
Update verifyLicense.ts
michelengelen Jun 14, 2024
e62b985
added license generation tests
michelengelen Jun 17, 2024
f4b0ed5
fixed typo
michelengelen Jun 17, 2024
b5cec8b
Added 'Mui X:' prefix on error
michelengelen Jun 17, 2024
88056dd
Added Key Version 2.2 tests
michelengelen Jun 18, 2024
508c785
added product scope tests
michelengelen Jun 18, 2024
c6e7a17
added useLicenseVerifier test
michelengelen Jun 18, 2024
1a26ce8
Apply suggestions from code review
michelengelen Jun 19, 2024
e572ab4
added some tests requested from @cherniavskii
michelengelen Jun 19, 2024
c67d0f9
Update packages/x-license/src/utils/licenseErrorMessageUtils.ts
michelengelen Jun 19, 2024
001a411
changed error wording
michelengelen Jun 19, 2024
4fffe6a
added link to license page back in
michelengelen Jun 20, 2024
c04fa6b
Merge remote-tracking branch 'refs/remotes/upstream/master' into lice…
michelengelen Jun 20, 2024
f19b30e
added text for new license state to watermark
michelengelen Jun 20, 2024
a0da60e
fix tests
michelengelen Jun 20, 2024
a19da9d
fix tests
michelengelen Jun 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('<DataGridPremium /> - License', () => {
orderNumber: 'Test',
licensingModel: 'subscription',
scope: 'pro',
planVersion: 'initial',
}),
);
expect(() => render(<DataGridPremium columns={[]} rows={[]} autoHeight />)).toErrorDev([
Expand Down
2 changes: 2 additions & 0 deletions packages/x-license/src/Watermark/Watermark.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Comment on lines 14 to +17
Copy link
Member

@oliviertassinari oliviertassinari Jun 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like these two errors OutOfScope + ProductNotCovered are the same error. Should we merge them? As for the need to show different error messages, I think this should be done through the meta data.

If we want to keep them distinct, I would make their name match, e.g.

OutOfScope -> PlanScopeNotCovered
ProductNotCovered -> ProductScopeNotCovered

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I advocated to change ProductNotCovered into a more specific NotAvailableInInitialProPlan in #13568, so that we can display a more precise error in the doc.
We could indeed rename OutOfScope which is super broad right now.

Copy link
Member

@oliviertassinari oliviertassinari Jun 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NotAvailableInInitialProPlan so that we can display a more precise error in the doc.

There will be more cases like this in the future of having a paid for one of the MUI X plans, and trying to use a different plan. I don't see creating a new error type each time scaling.

So It feels like this should be one error type and use the metadata to have a precise error in the console.

For the docs, I agree, I can see why we need to separate the cases: (a.) buying pro and using pro without working is a different confusion source than (b.) buying pro and trying to use premium.

But anyway, no strong push on my end, more of a feeling that in the code, it feels strange.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see creating a new error type each time scaling.

The status is only used to do some sort of switch and display a warning message.
So I don't see how metadata would be more or less scalable than a different status message.
Even if we end up with 20 different statuses, the big part is the switch of conditions.

case LICENSE_STATUS.NotFound:
return 'MUI X Missing license key';
default:
Expand Down
40 changes: 36 additions & 4 deletions packages/x-license/src/generateLicense/generateLicense.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ describe('License: generateLicense', () => {
orderNumber: 'MUI-123',
scope: 'pro',
licensingModel: 'subscription',
planVersion: 'initial',
}),
).to.equal(
'b2b2ea9c6fd846e11770da3c795d6f63Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1zdWJzY3JpcHRpb24sS1Y9Mg==',
'e8fad422a82720084ec67dd693f08056Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1zdWJzY3JpcHRpb24sUFY9aW5pdGlhbCxLVj0y',
);
});

Expand All @@ -22,9 +23,10 @@ describe('License: generateLicense', () => {
orderNumber: 'MUI-123',
scope: 'premium',
licensingModel: 'subscription',
planVersion: 'initial',
}),
).to.equal(
'ac8d20b4ecd1f919157f3713f8ba1651Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLEtWPTI=',
'8ca0384bfb92ec214d4cd72483f5110bTz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLFBWPWluaXRpYWwsS1Y9Mg==',
);
});

Expand All @@ -35,9 +37,10 @@ describe('License: generateLicense', () => {
orderNumber: 'MUI-123',
scope: 'pro',
licensingModel: 'subscription',
planVersion: 'initial',
}),
).to.equal(
'b2b2ea9c6fd846e11770da3c795d6f63Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1zdWJzY3JpcHRpb24sS1Y9Mg==',
'e8fad422a82720084ec67dd693f08056Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1zdWJzY3JpcHRpb24sUFY9aW5pdGlhbCxLVj0y',
);
});

Expand All @@ -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',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm used to:

Suggested change
planVersion: 'Q3-2024',
planVersion: '2024-Q3',

I wonder if we should change this. But I guess it's up to the license key generator logic.

}),
).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==',
);
});
});
16 changes: 12 additions & 4 deletions packages/x-license/src/generateLicense/generateLicense.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,6 +10,7 @@ export interface LicenseDetails {
expiryDate: Date;
scope: LicenseScope;
licensingModel: LicensingModel;
planVersion: PlanVersion;
}

function getClearLicenseString(details: LicenseDetails) {
Expand All @@ -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 = [
michelengelen marked this conversation as resolved.
Show resolved Hide resolved
`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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
LicenseInfo,
generateLicense,
Unstable_LicenseInfoProvider as LicenseInfoProvider,
MuiCommercialPackageName,
} from '@mui/x-license';
import { sharedLicenseStatuses } from './useLicenseVerifier';
import { generateReleaseInfo } from '../verifyLicense';
Expand All @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I wrote licesen 🤪

return <div data-testid="status">Status: {licesenStatus.status}</div>;
function TestComponent(props: { packageName?: MuiCommercialPackageName }) {
const licenseStatus = useLicenseVerifier(props.packageName || 'x-date-pickers-pro', RELEASE_INFO);
return <div data-testid="status">Status: {licenseStatus.status}</div>;
}

describe('useLicenseVerifier', function test() {
Expand Down Expand Up @@ -63,6 +64,7 @@ describe('useLicenseVerifier', function test() {
licensingModel: 'perpetual',
orderNumber: '12345',
scope: 'pro',
planVersion: 'initial',
});

LicenseInfo.setLicenseKey('');
Expand All @@ -88,6 +90,7 @@ describe('useLicenseVerifier', function test() {
orderNumber: 'MUI-123',
scope: 'pro',
licensingModel: 'subscription',
planVersion: 'initial',
});
LicenseInfo.setLicenseKey(expiredLicenseKey);

Expand All @@ -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(<TestComponent packageName={'x-charts-pro'} />);
}).to.toErrorDev(['MUI X: Product not covered by plan.']);

expect(() => {
render(<TestComponent packageName={'x-tree-view-pro'} />);
}).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(<TestComponent packageName={'x-charts-pro'} />);
michelengelen marked this conversation as resolved.
Show resolved Hide resolved
}).not.toErrorDev();

expect(() => {
render(<TestComponent packageName={'x-tree-view-pro'} />);
}).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(<TestComponent packageName={'x-data-grid-pro'} />);
}).not.toErrorDev();

expect(() => {
render(<TestComponent packageName={'x-data-grid-premium'} />);
}).not.toErrorDev();

expect(() => {
render(<TestComponent packageName={'x-date-pickers-pro'} />);
}).not.toErrorDev();
});
});
});
11 changes: 7 additions & 4 deletions packages/x-license/src/useLicenseVerifier/useLicenseVerifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -47,15 +48,15 @@ export function useLicenseVerifier(
return sharedLicenseStatuses[packageName]!.licenseVerifier;
}

const acceptedScopes: LicenseScope[] = packageName.includes('premium')
? ['premium']
: ['pro', 'premium'];
const acceptedScopes = extractAcceptedScopes(packageName);
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved
const productScope = extractProductScope(packageName);
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved

const plan = packageName.includes('premium') ? 'Premium' : 'Pro';
const licenseStatus = verifyLicense({
releaseInfo,
licenseKey,
acceptedScopes,
productScope,
});

const fullPackageName = `@mui/${packageName}`;
Expand All @@ -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) {
Expand Down
10 changes: 10 additions & 0 deletions packages/x-license/src/utils/licenseErrorMessageUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions packages/x-license/src/utils/licenseScope.ts
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced about "initial". To me, what we have today is

Suggested change
export const PLAN_VERSIONS = ['initial', 'Q3-2024'] as const;
export const PLAN_VERSIONS = ['Q3-2022', 'Q3-2024'] as const;

the last time we changed the plan.


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 => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small detail, default function convention we use is function for top level scope and arrow function for lower scope; applied here would be:

Suggested change
export const extractProductScope = (packageName: string): ProductScope => {
export function 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;
michelengelen marked this conversation as resolved.
Show resolved Hide resolved
};

export const extractAcceptedScopes = (packageName: string): readonly LicenseScope[] => {
return packageName.includes('premium')
? LICENSE_SCOPES.filter((scope) => scope.includes('premium'))
: LICENSE_SCOPES;
};
1 change: 1 addition & 0 deletions packages/x-license/src/utils/licenseStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export enum LICENSE_STATUS {
ExpiredVersion = 'ExpiredVersion',
Valid = 'Valid',
OutOfScope = 'OutOfScope',
ProductNotCovered = 'ProductNotCovered',
}

export type LicenseStatus = keyof typeof LICENSE_STATUS;
Loading
Loading