Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
27 changes: 26 additions & 1 deletion assets/src/Components/CloudLibrary/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import apiFetch from '@wordpress/api-fetch';
import { stringifyUrl } from 'query-string';
import { v4 as uuidv4 } from 'uuid';
import { models, loadPromise } from '@wordpress/api';
import { cleanTemplateContent } from '../../../../shared/utils';
import { cleanTemplateContent, hasTemplatesCloudAccess } from '../../../../shared/utils';

export const changeOption = ( option, value ) => {
const model = new models.Settings( {
Expand Down Expand Up @@ -80,6 +80,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 || ! hasTemplatesCloudAccess( license ) ) {
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 );
43 changes: 33 additions & 10 deletions assets/src/Components/License.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* global tiobDash */
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { compose } from '@wordpress/compose';
Expand All @@ -11,16 +12,25 @@ import {
Icon,
} from '@wordpress/components';
import { models } from '@wordpress/api';
import { fetchLibrary as licenseCheck } from './CloudLibrary/common';
import { licenseCheck } from './CloudLibrary/common';
import {
hasTemplatesCloudAccess,
isLicenseValid,
} 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 isStored = isLicenseValid( license );
const hasAccess = hasTemplatesCloudAccess( license );

const isValid = 'valid' === license?.valid || 'valid' === license?.license;
const notEntitledMsg = __(
'Your license is valid, but Templates Cloud is not included in your plan.',
'templates-patterns-collection'
);

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

Expand Down Expand Up @@ -61,10 +71,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 All @@ -77,17 +87,17 @@ const License = ( { setLicense, license } ) => {

const toggleLicense = ( event ) => {
onSaveLicense( {
action: isValid ? 'deactivate' : 'activate',
action: isStored ? 'deactivate' : 'activate',
key: licenseKey,
} );

event.preventDefault();
};

const futureDate = new Date( new Date().setFullYear( new Date().getFullYear() + 10 ) );
const expiration = isValid && license?.expires === 'lifetime' ? futureDate.toDateString() : new Date( license.expires ).toDateString();
const expiration = isStored && license?.expires === 'lifetime' ? futureDate.toDateString() : new Date( license.expires ).toDateString();

const licenseStatusMsg = isValid ? (
const licenseStatusMsg = hasAccess ? (
<>
<Icon size={ 24 } className="verified" icon="yes-alt" />
<span>{ 'Verified - Expires at'} { expiration }</span>
Expand All @@ -97,6 +107,18 @@ const License = ( { setLicense, license } ) => {
''
);

const renderEntitlementMsg =
isStored && ! hasAccess ? (
<Notice isDismissible={ false } status="warning">
{ notEntitledMsg }{ ' ' }
<ExternalLink href={ tiobDash.upgradeURLTpc }>
{ __( 'Upgrade to PRO', 'templates-patterns-collection' ) }
</ExternalLink>
</Notice>
) : (
''
);

const renderResultMsg =
Object.keys( resultMsg ).length > 0 ? (
<Notice isDismissible={ false } status={ resultMsg.type }>
Expand All @@ -110,11 +132,11 @@ const License = ( { setLicense, license } ) => {
<>
<form className="license-form" onSubmit={ toggleLicense }>
<TextControl
disabled={ isValid }
disabled={ isStored }
onChange={ setLicenseKey }
label={ __( 'License Key', 'templates-patterns-collection' ) }
value={
isValid
isStored
? '******************************' +
licenseKey.slice( -5 )
: licenseKey
Expand All @@ -126,13 +148,14 @@ const License = ( { setLicense, license } ) => {
type="submit"
variant="primary"
>
{ isValid
{ isStored
? __( 'Deactivate', 'templates-patterns-collection' )
: __( 'Activate', 'templates-patterns-collection' ) }
</Button>
</form>

<div className="info">{ licenseStatusMsg }</div>
{ renderEntitlementMsg }
{ renderResultMsg }
</>
);
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
25 changes: 21 additions & 4 deletions e2e-tests/mu-plugins/tpc-e2e.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ function () {
'args' => array(
'mode' => array(
'type' => 'string',
'enum' => array( '', 'down', 'invalid' ),
'enum' => array( '', 'down', 'invalid', 'personal' ),
),
),
'callback' => function ( $request ) {
Expand Down Expand Up @@ -110,9 +110,11 @@ function ( $preempt, $args, $url ) {
$is_themeisle_api = false !== strpos( $url, 'api.themeisle.com' ) || false !== strpos( $url, 'ai.themeisle.com' );

// Scenario modes (Otter pattern), set per spec via tpc-e2e/v1/api-mode:
// 'down' => ThemeIsle APIs unreachable.
// 'invalid' => license check rejects the key (a code/message body is
// what License::check_license treats as invalid).
// 'down' => ThemeIsle APIs unreachable.
// 'invalid' => license check rejects the key (a code/message body is
// what License::check_license treats as invalid).
// 'personal' => the key is accepted, on a tier that does not include
// Templates Cloud.
$mode = get_option( 'tpc_e2e_api_mode', '' );

if ( 'down' === $mode && $is_themeisle_api ) {
Expand All @@ -130,6 +132,21 @@ function ( $preempt, $args, $url ) {
);
}

// A valid license on a tier that does not include Templates Cloud.
// Must match PERSONAL_LICENSE in config/mocks.js.
if ( 'personal' === $mode && false !== strpos( $url, 'api.themeisle.com/templates-cloud/' ) ) {
Comment thread
girishpanchal30 marked this conversation as resolved.
return tpc_e2e_response(
wp_json_encode(
array(
'license' => 'valid',
'key' => 'tpc-e2e-personal-key',
'tier' => 2,
'expires' => 'lifetime',
)
)
);
}

// Starter sites feed (Sites_Listing::API).
if ( false !== strpos( $url, 'api.themeisle.com/sites/wp-json/demosites-api/sites' ) ) {
return tpc_e2e_response( file_get_contents( TPC_E2E_FIXTURES_DIR . '/sites.json' ) );
Expand Down
Loading
Loading