Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
29 changes: 29 additions & 0 deletions assets/src/Components/CloudLibrary/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { stringifyUrl } from 'query-string';
import { v4 as uuidv4 } from 'uuid';
import { models, loadPromise } from '@wordpress/api';
import { cleanTemplateContent } from '../../../../shared/utils';
import {
isLicenseValid,
isTemplatesCloudTier,
} from '../../../../shared/utils';
Comment thread
girishpanchal30 marked this conversation as resolved.
Outdated

export const changeOption = ( option, value ) => {
const model = new models.Settings( {
Expand Down Expand Up @@ -80,6 +84,31 @@ export const fetchLibrary = async (
}
};

/**
* Validate a license key against the licensing API.
*
* @param {string} licenseKey The license key to check.
* @return {Promise<Object>} `{ success, status, license, message }`.
*/
export const licenseCheck = async ( licenseKey ) => {
const { success, templates: license, message } =
( await fetchLibrary( false, {
license_id: licenseKey,
license_check: 1,
} ) ) || {};

if ( ! success || ! isLicenseValid( license ) || ! isTemplatesCloudTier( license?.tier ) ) {
return {
success: false,
status: 'invalid',
license,
message,
};
}

return { success: true, status: 'valid', license };
};

export const updateTemplate = async ( id, name ) => {
const url = stringifyUrl( {
url: tiobDash.endpoint + 'templates/' + id,
Expand Down
6 changes: 0 additions & 6 deletions assets/src/Components/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const TabNavigation = ( {
setCurrentTab,
currentTab,
isFetching,
license,
} ) => {
const buttons = {};

Expand All @@ -33,7 +32,6 @@ const TabNavigation = ( {

const [ isSyncing, setSyncing ] = useState( false );
const { isLicenseOpen, setLicenseOpen } = useContext( LicensePanelContext );
const isValid = 'valid' === license?.valid || 'valid' === license?.license;

const sync = () => {
setSyncing( true );
Expand Down Expand Up @@ -158,7 +156,6 @@ const Header = ( {
cancelOnboarding,
setCurrentTab,
currentTab,
license,
} ) => {
return (
<div className="ob-head">
Expand All @@ -169,7 +166,6 @@ const Header = ( {
<TabNavigation
setCurrentTab={ setCurrentTab }
currentTab={ currentTab }
license={ license }
/>
</div>
</>
Expand Down Expand Up @@ -204,13 +200,11 @@ export default compose(
getOnboardingStatus,
getCurrentTab,
getFetching,
getLicense,
} = select( 'neve-onboarding' );
return {
isOnboarding: getOnboardingStatus(),
currentTab: getCurrentTab(),
isFetching: getFetching(),
license: getLicense(),
};
} )
)( Header );
10 changes: 5 additions & 5 deletions assets/src/Components/License.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ import {
Icon,
} from '@wordpress/components';
import { models } from '@wordpress/api';
import { fetchLibrary as licenseCheck } from './CloudLibrary/common';
import { licenseCheck } from './CloudLibrary/common';
import { isLicenseValid, isTemplatesCloudTier } from '../../../shared/utils';

const License = ( { setLicense, license } ) => {
const keyValue = license?.key !== '' && license?.key !== 'free' ? license?.key : '';
const [ licenseKey, setLicenseKey ] = useState( keyValue );
const [ loading, setLoading ] = useState( false );
const [ resultMsg, setResultMsg ] = useState( {} );


const isValid = 'valid' === license?.valid || 'valid' === license?.license;
const isValid = isLicenseValid( license ) && isTemplatesCloudTier( license?.tier );
Comment thread
girishpanchal30 marked this conversation as resolved.
Outdated

const delay = (time) => new Promise(resolve => setTimeout(resolve, time));

Expand Down Expand Up @@ -61,10 +61,10 @@ const License = ( { setLicense, license } ) => {
return;
}

const { success, templates } = await licenseCheck( false, { license_id: data.key, license_check: 1 } );
const { success, license: licenseData } = await licenseCheck( data.key );

if ( success ) {
setLicense( templates );
setLicense( licenseData );
await updateKey( data.key );
} else {
createNotice(
Expand Down
22 changes: 3 additions & 19 deletions assets/src/store/selectors.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isTemplatesCloudTier } from '../../../shared/utils';

export default {
getSites: ( state ) => state.sites,
getMigrationData: ( state ) => state.migrationData,
Expand All @@ -15,28 +17,10 @@ export default {
getTemplateModal: ( state ) => state.templateModal,
getSearchQuery: ( state ) => state.searchQuery,
getUserStatus: ( state ) => {
const acceptedTiers = [
6,
17,
23,
5,
9,
14,
20,
1,
7,
12,
18,
3,
8,
13,
19,
];

return (
state.license &&
state.license.tier &&
acceptedTiers.includes( state.license.tier )
isTemplatesCloudTier( state.license.tier )
);
},
getLicense: ( state ) => {
Expand Down
29 changes: 28 additions & 1 deletion e2e-tests/config/mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,23 @@ export const MOCK_TEMPLATES = [
},
];

// License payloads returned for a browser-side license check (license_check=1).
// AGENCY_LICENSE matches the license the mu-plugin seeds; PERSONAL_LICENSE is a
// valid license on a tier that does not include Templates Cloud.
export const AGENCY_LICENSE = {
license: 'valid',
key: 'tpc-e2e-key',
tier: 3,
expires: 'lifetime',
};

export const PERSONAL_LICENSE = {
license: 'valid',
key: 'tpc-e2e-personal-key',
tier: 2,
expires: 'lifetime',
};

// apiFetch sends credentialed requests, so the mocked cross-origin responses
// must echo the exact origin (a wildcard is rejected by the browser).
const corsHeaders = (route) => ({
Expand Down Expand Up @@ -142,10 +159,20 @@ export async function mockOnboardingRoutes(page) {
export const TEMPLATE_CONTENT_TEXT = 'TPC E2E imported content';
export const TEMPLATE_CONTENT = `<!-- wp:paragraph --><p>${TEMPLATE_CONTENT_TEXT}</p><!-- /wp:paragraph -->`;

export async function mockTemplatesCloudRoutes(page, templates = MOCK_TEMPLATES) {
export async function mockTemplatesCloudRoutes(
page,
templates = MOCK_TEMPLATES,
license = AGENCY_LICENSE,
) {
await page.route('**/api.themeisle.com/templates-cloud/**', (route) => {
const url = new URL(route.request().url());

// license_check=1 returns the license data instead of templates - this
// is what the license panel activates against.
if (url.searchParams.get('license_check')) {
return fulfillJson(route, license);
}

// GET templates/{id}/import returns the template's block content.
if (url.pathname.endsWith('/import')) {
return fulfillJson(route, {
Expand Down
73 changes: 73 additions & 0 deletions e2e-tests/specs/license.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* WordPress dependencies
*/
import { test, expect } from '@wordpress/e2e-test-utils-playwright';

/**
* Internal dependencies
*/
import {
AGENCY_LICENSE,
MOCK_TEMPLATES,
PERSONAL_LICENSE,
mockTemplatesCloudRoutes,
} from '../config/mocks';

const setApiMode = (requestUtils, mode) =>
requestUtils.rest({
path: '/tpc-e2e/v1/api-mode',
method: 'POST',
data: { mode },
});

const setLegacyTc = (requestUtils, enabled) =>
requestUtils.rest({
path: '/tpc-e2e/v1/legacy-tc',
method: 'POST',
data: { enabled },
});

// The license panel lives in the tiob-plugin dashboard settings, which only
// exists for "legacy Templates Cloud" installs. The 'invalid' API mode drops the
// license the mu-plugin seeds, so the activation form is the starting state.
test.describe('License activation', () => {
const SETTINGS_URL = 'admin.php?page=tiob-plugin#settings';

test.beforeEach(async ({ requestUtils }) => {
await setLegacyTc(requestUtils, true);
await setApiMode(requestUtils, 'invalid');
});

test.afterEach(async ({ requestUtils }) => {
await setApiMode(requestUtils, '');
await setLegacyTc(requestUtils, false);
});

test('not activating personal plan', async ({
page,
admin,
}) => {
await mockTemplatesCloudRoutes(page, MOCK_TEMPLATES, PERSONAL_LICENSE);
await admin.visitAdminPage(SETTINGS_URL);

await page.getByLabel('License Key').fill(PERSONAL_LICENSE.key);
await page.getByRole('button', { name: 'Activate' }).click();

await expect(
page.locator('#tpc-app').getByText('Can not activate this license!', { exact: true })
).toBeVisible();
Comment thread
girishpanchal30 marked this conversation as resolved.
});

test('activating an agency plan', async ({
page,
admin,
}) => {
await mockTemplatesCloudRoutes(page, MOCK_TEMPLATES, AGENCY_LICENSE);
await admin.visitAdminPage(SETTINGS_URL);

await page.getByLabel('License Key').fill(AGENCY_LICENSE.key);
await page.getByRole('button', { name: 'Activate' }).click();

await expect(page.getByRole('button', { name: 'Deactivate' })).toBeVisible();
});
});
8 changes: 3 additions & 5 deletions onboarding/src/store/selectors.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isTemplatesCloudTier } from '../../../shared/utils';

export default {
getThemeAction: ( state ) => state.themeAction,
getCurrentStep: ( state ) => state.onboardingStep,
Expand All @@ -11,14 +13,10 @@ export default {
getError: ( state ) => state.error,
getPluginOptions: ( state ) => state.pluginOptions,
getUserStatus: ( state ) => {
const acceptedTiers = [
6, 17, 23, 5, 9, 14, 20, 1, 7, 12, 18, 3, 8, 13, 19,
];

return (
state.license &&
state.license.tier &&
acceptedTiers.includes( state.license.tier )
isTemplatesCloudTier( state.license.tier )
);
},
getUserCustomSettings: ( state ) => state.userCustomSettings,
Expand Down
42 changes: 42 additions & 0 deletions shared/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,45 @@ export const cleanTemplateContent = ( templateContent, cleanFunc ) => {
loopElementorElement( item, cleanFunc );
} );
};

/**
* License tiers entitled to Templates Cloud.
*/
export const TEMPLATES_CLOUD_TIERS = [
6,
17,
23,
5,
9,
14,
20,
1,
7,
12,
18,
3,
8,
13,
19,
];

/**
* Check if a license tier is entitled to Templates Cloud.
*
* @param {number|string} tier The tier key returned by the licensing API.
* @return {boolean} Whether the tier includes Templates Cloud.
*/
export const isTemplatesCloudTier = ( tier ) => {
const parsed = Number( tier );

return Number.isInteger( parsed ) && TEMPLATES_CLOUD_TIERS.includes( parsed );
};

/**
* Check if the license itself is valid, regardless of its tier.
*
* @param {Object} license The license data.
* @return {boolean} Whether the license is valid.
*/
export const isLicenseValid = ( license ) =>
'valid' === license?.valid || 'valid' === license?.license;
Loading