Skip to content
New issue

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

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

Already on GitHub? # to your account

[license] Allow to limit some packages to a specific license plan #4651

Merged
merged 23 commits into from
May 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion docs/data/advanced-components/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ This key removes all watermarks and console warnings.
import { LicenseInfo } from '@mui/x-license-pro';

LicenseInfo.setLicenseKey(
'x0jTPl0USVkVZV0SsMjM1kDNyADM5cjM2ETPZJVSQhVRsIDN0YTM6IVREJ1T0b9586ef25c9853decfa7709eee27a1e',
'61628ce74db2c1b62783a6d438593bc5Tz1NVUktRG9jLEU9MTY4MzQ0NzgyMTI4NCxTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLEtWPTI=',
);
```

Expand Down
2 changes: 1 addition & 1 deletion docs/data/date-pickers/migration-lab/migration-lab.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ You must set the license key before rendering the first component.
import { LicenseInfo } from '@mui/x-license-pro';

LicenseInfo.setLicenseKey(
'x0jTPl0USVkVZV0SsMjM1kDNyADM5cjM2ETPZJVSQhVRsIDN0YTM6IVREJ1T0b9586ef25c9853decfa7709eee27a1e',
'61628ce74db2c1b62783a6d438593bc5Tz1NVUktRG9jLEU9MTY4MzQ0NzgyMTI4NCxTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLEtWPTI=',
);
```

Expand Down
2 changes: 1 addition & 1 deletion packages/storybook/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { configureActions } from '@storybook/addon-actions';

// Remove the license warning from demonstration purposes
LicenseInfo.setLicenseKey(
'0f94d8b65161817ca5d7f7af8ac2f042T1JERVI6TVVJLVN0b3J5Ym9vayxFWFBJUlk9MTY1NDg1ODc1MzU1MCxLRVlWRVJTSU9OPTE=',
'61628ce74db2c1b62783a6d438593bc5Tz1NVUktRG9jLEU9MTY4MzQ0NzgyMTI4NCxTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLEtWPTI=',
);

configureActions({
Expand Down
14 changes: 12 additions & 2 deletions packages/x-license-pro/src/cli/license-cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/* eslint-disable no-console */
import * as yargs from 'yargs';
import { generateLicense, LicenseScope } from '../generateLicense/generateLicense';
import { generateLicense } from '../generateLicense/generateLicense';
import { base64Decode } from '../encoding/base64';
import { LicenseScope } from '../utils/licenseScope';
import { LicensingModel } from '../utils/licensingModel';

const oneDayInMs = 1000 * 60 * 60 * 24;

Expand Down Expand Up @@ -64,8 +66,15 @@ export function licenseGenCli() {
})
.option('scope', {
default: 'pro',
alias: 's',
describe: 'The license scope.',
type: 'string',
})
.option('licensingModel', {
default: 'subscription',
alias: 'l',
describe: 'The license sales model.',
type: 'string',
});
},
handler: (argv: yargs.ArgumentsCamelCase<LicenseGenArgv>) => {
Expand All @@ -76,7 +85,8 @@ export function licenseGenCli() {
const licenseDetails = {
expiryDate: new Date(new Date().getTime() + parseInt(argv.expiry, 10) * oneDayInMs),
orderNumber: argv.order,
scope: argv.scope as LicenseScope,
scope: argv.scope as LicenseScope | undefined,
licensingModel: argv.licensingModel as LicensingModel | undefined,
};

console.log(
Expand Down
60 changes: 53 additions & 7 deletions packages/x-license-pro/src/generateLicense/generateLicense.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,81 @@ import { expect } from 'chai';
import { generateLicense } from './generateLicense';

describe('License: generateLicense', () => {
it('should generate DataGridPro License properly when "scope" is not provided', () => {
// TODO: Remove
it('should generate pro license properly when "scope" is not provided', () => {
expect(
generateLicense({ expiryDate: new Date(1591723879062), orderNumber: 'MUI-123' }),
generateLicense({
expiryDate: new Date(1591723879062),
orderNumber: 'MUI-123',
licensingModel: 'subscription',
}),
).to.equal(
'90b5a76151089447618ede1917227a1bT1JERVI6TVVJLTEyMyxFWFBJUlk9MTU5MTcyMzg3OTA2MixLRVlWRVJTSU9OPTIsU0NPUEU9cHJv',
'b2b2ea9c6fd846e11770da3c795d6f63Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1zdWJzY3JpcHRpb24sS1Y9Mg==',
);
});

it('should generate DataGridPro License properly when `scope: "pro"`', () => {
it('should generate pro license properly when `scope: "pro"`', () => {
expect(
generateLicense({
expiryDate: new Date(1591723879062),
orderNumber: 'MUI-123',
scope: 'pro',
licensingModel: 'subscription',
}),
).to.equal(
'90b5a76151089447618ede1917227a1bT1JERVI6TVVJLTEyMyxFWFBJUlk9MTU5MTcyMzg3OTA2MixLRVlWRVJTSU9OPTIsU0NPUEU9cHJv',
'b2b2ea9c6fd846e11770da3c795d6f63Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1zdWJzY3JpcHRpb24sS1Y9Mg==',
);
});

it('should generate DataGridPremium License properly when `scope: "premium"`', () => {
it('should generate premium license when `scope: "premium"`', () => {
expect(
generateLicense({
expiryDate: new Date(1591723879062),
orderNumber: 'MUI-123',
scope: 'premium',
licensingModel: 'subscription',
}),
).to.equal(
'ac8d20b4ecd1f919157f3713f8ba1651Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLEtWPTI=',
);
});

// TODO: Remove
it('should generate perpetual license when "licensingModel" is not provided', () => {
expect(
generateLicense({
expiryDate: new Date(1591723879062),
orderNumber: 'MUI-123',
scope: 'pro',
}),
).to.equal(
'b16edd8e6bc83293a723779a259f520cTz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1wZXJwZXR1YWwsS1Y9Mg==',
);
});

it('should generate subscription license when `licensingModel: "subscription"`', () => {
expect(
generateLicense({
expiryDate: new Date(1591723879062),
orderNumber: 'MUI-123',
scope: 'pro',
licensingModel: 'subscription',
}),
).to.equal(
'b2b2ea9c6fd846e11770da3c795d6f63Tz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1zdWJzY3JpcHRpb24sS1Y9Mg==',
);
});

it('should generate perpetual license when `licensingModel: "perpetual"`', () => {
expect(
generateLicense({
expiryDate: new Date(1591723879062),
orderNumber: 'MUI-123',
scope: 'pro',
licensingModel: 'perpetual',
}),
).to.equal(
'0d79eeaf5facce7184422f22eeeb369aT1JERVI6TVVJLTEyMyxFWFBJUlk9MTU5MTcyMzg3OTA2MixLRVlWRVJTSU9OPTIsU0NPUEU9cHJlbWl1bQ==',
'b16edd8e6bc83293a723779a259f520cTz1NVUktMTIzLEU9MTU5MTcyMzg3OTA2MixTPXBybyxMTT1wZXJwZXR1YWwsS1Y9Mg==',
);
});
});
29 changes: 17 additions & 12 deletions packages/x-license-pro/src/generateLicense/generateLicense.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
import { md5 } from '../encoding/md5';
import { base64Encode } from '../encoding/base64';
import { LICENSE_SCOPES, LicenseScope } from '../utils/licenseScope';
import { LICENSING_MODELS, LicensingModel } from '../utils/licensingModel';

const licenseVersion = '2';

export type LicenseScope = 'pro' | 'premium';

export interface LicenseDetails {
orderNumber: string;
expiryDate: Date;
// TODO: to be made required once the store is updated
scope?: LicenseScope;
// TODO: to be made required once the store is updated
licensingModel?: LicensingModel;
}

function getClearLicenseString(details: LicenseDetails) {
return `ORDER:${
details.orderNumber
},EXPIRY=${details.expiryDate.getTime()},KEYVERSION=${licenseVersion},SCOPE=${details.scope}`;
if (details.scope && !LICENSE_SCOPES.includes(details.scope)) {
throw new Error('MUI: Invalid scope');
}

if (details.licensingModel && !LICENSING_MODELS.includes(details.licensingModel)) {
throw new Error('MUI: Invalid sales model');
}

return `O=${details.orderNumber},E=${details.expiryDate.getTime()},S=${
Copy link
Member

@oliviertassinari oliviertassinari May 15, 2022

Choose a reason for hiding this comment

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

Without this change, a v2 license key is not usable by < v5.11.0 of the software. It's not backward compatible. It surfaced in https://groups.google.com/a/mui.com/g/x/c/Nkq2fQbGkn0/. Time will tell if this causes trouble with more users, I suspect it will. If the intent is to shorten the string key, I doubt it's worth the breaking change. If a breaking change needs to happen, then I think bundling with #4892 could be great.

How about?

  1. We revert the breaking change:
Suggested change
return `O=${details.orderNumber},E=${details.expiryDate.getTime()},S=${
return `O=${details.orderNumber},EXPIRY=${details.expiryDate.getTime()},S=${
  1. We add support for both E and EXPIRY as license key prefix
  2. We add support for [x-license] Use a simpler checksum for the license key #4892
  3. We release and wait 6 months.
  4. We replace EXPIRY -> E, md5 -> simpler checksum.

Copy link
Member

Choose a reason for hiding this comment

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

@joserodolfofreitas we are one week in, how are the upgrades support cases going? I haven't seen new ones, so maybe it's fine.

details.scope ?? 'pro'
},LM=${details.licensingModel ?? 'perpetual'},KV=${licenseVersion}`;
}

export function generateLicense(details: LicenseDetails) {
let clearLicense;
if (details.scope) {
clearLicense = getClearLicenseString(details);
} else {
clearLicense = getClearLicenseString({ ...details, scope: 'pro' });
}
const licenseStr = getClearLicenseString(details);

return `${md5(base64Encode(clearLicense))}${base64Encode(clearLicense)}`;
return `${md5(base64Encode(licenseStr))}${base64Encode(licenseStr)}`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
showNotFoundLicenseError,
} from '../utils/licenseErrorMessageUtils';
import { LicenseStatus } from '../utils/licenseStatus';
import { LicenseScope } from '../utils/licenseScope';

export type MuiCommercialPackageName =
| 'x-data-grid-pro'
Expand All @@ -27,7 +28,16 @@ export function useLicenseVerifier(
return sharedLicenseStatuses[packageName]!.status;
}

const licenseStatus = verifyLicense(releaseInfo, licenseKey);
const acceptedScopes: LicenseScope[] = packageName.includes('premium')
Copy link
Member Author

Choose a reason for hiding this comment

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

This logic would have to become smarter if we release other plans

Copy link
Member

Choose a reason for hiding this comment

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

So to be sure that I understand it - the goal here is to if you have premium you also get pro right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes
I first did a scale const scopes = ['community', 'pro', 'premium'] and then compare the index of the current plan with the one of the license
But it would not work with more granular scopes if we have some in the future
Right now it's a very basic check so both options work fine.

? ['premium']
: ['pro', 'premium'];

const licenseStatus = verifyLicense({
releaseInfo,
licenseKey,
acceptedScopes,
isProduction: process.env.NODE_ENV === 'production',
});

sharedLicenseStatuses[packageName] = { key: licenseStatus, status: licenseStatus };

Expand Down
2 changes: 2 additions & 0 deletions packages/x-license-pro/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './licenseErrorMessageUtils';
export * from './licenseInfo';
export * from './licenseStatus';
export type { LicenseScope } from './licenseScope';
export type { LicensingModel } from './licensingModel';
3 changes: 3 additions & 0 deletions packages/x-license-pro/src/utils/licenseScope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const LICENSE_SCOPES = ['pro', 'premium'] as const;

export type LicenseScope = typeof LICENSE_SCOPES[number];
14 changes: 14 additions & 0 deletions packages/x-license-pro/src/utils/licensingModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const LICENSING_MODELS = [
/**
* A license is outdated if the current version of the software was released after the expiry date of the license.
* But the license can be used indefinitely with an older version of the software.
*/
'perpetual',
/**
* On development, a license is outdated if the expiry date has been reached
* On production, a license is outdated if the current version of the software was released after the expiry date of the license (see "perpetual")
*/
'subscription',
] as const;

export type LicensingModel = typeof LICENSING_MODELS[number];
Loading