License Gate + Download Security Demo (Ticket 1544414)

Support demo for ticket 1544414. This page is served by a demo plugin (sc-license-gate-demo) that bundles the official SureCart WordPress SDK and locks its premium features until a valid license is activated for this site. Everything below is live: the status table is calculated on every page load.

1. Live license status (from the demo plugin)

Premium features✅ UNLOCKED
Reasonlicense_valid_and_activated
Answer came fromcache (no API call this page load)
Last checked with API2026-10-10 13:32:53 GMT+0000
Cache valid until2026-10-11 01:32:53 GMT+0000
Stored license key60d4300d…7d84
Stored activation id952a642f-1039-49c2-bb2a-7e5f9387a8a7
Last known good2026-10-10 13:32:53 GMT+0000
Cache TTL / grace12 h / 3 days
SDK version1.0.1
SDK loaded from/wp-content/plugins/suremembers/licensing-sdk/src/Client.php

2. Premium feature (only renders while licensed)

✅ Premium feature unlocked. This content only renders while the SureCart license is valid and activated for this site.

3. Try it yourself (log in as webcare1 first)

  • License Gate Demo > Manage License: click Deactivate License, reload this page. Status flips to LOCKED (reason no_license_activated) with no API call. Re-activate with key 60d4300d-a7f7-4c51-8700-c929867a7d84 and it unlocks again. While unlocked, a premium submenu “Demo Import (premium)” exists under License Gate Demo; while locked it is not registered at all.
  • ?sclgd_refresh=1: bypasses the 12 hour cache and asks SureCart right now (“Answer came from: api”).
  • ?sclgd_simulate_outage=1&sclgd_refresh=1: points the SDK at an unreachable host for one request. Status stays UNLOCKED with reason api_unreachable_grace_period because the site had a good answer within the last 3 days. Run ?sclgd_refresh=1 afterwards to go back to a real answer.
  • SureCart > Customers > Theme Buyer 1544414: open the purchase and click Revoke Access, then load ?sclgd_refresh=1 here. Status becomes LOCKED with license_revoked_or_missing and the file disappears from the buyer’s dashboard. Restore Access puts both back.
  • Buyer login for the customer dashboard: user themebuyer1544414 / ThemeDemo!1544414 (log out of webcare1 first, or use a private window). Downloads tab and Licenses.
  • Demo product page (test mode, Test Processor). Product: SC License Gate Demo Theme (Ticket 1544414), Licensing enabled, 1 activation, release ZIP attached as Current Release.

4. Download link security, measured on this store

All results below were produced on this store on 21 Sep 2026 against the file attached to the demo product (Secure Storage). Click the links to see the behaviour for yourself.

#What was testedLink / callResult
ALink type used in the Product Access email (expose_for=86400)24 hour signed linkWorks until 22 Sep 2026 09:31 UTC, then returns HTTP 410. Not tied to the customer: anyone holding it can download during those 24 hours.
BLink type minted by the customer dashboard Download button (expose_for=60)60 second link (already expired)Downloaded the byte-identical ZIP at 0 s. Re-fetched at 79 s: HTTP 410 “This download link has expired or the file has been removed.”
CSame link with 2 characters of the signature changedtampered linkHTTP 410 immediately. The blob id and expiry are inside the signed token, so nothing can be guessed or extended.
DGuest (not logged in) calling the dashboard endpoint /wp-json/surecart/v1/customers/{id}/expose/{media}curl, no cookieHTTP 401.
ELogged-in buyer calling it for their own customer id / for another customer idcurl with the buyer’s login cookieOwn id: 200 and a fresh 60 s link. Other id: HTTP 403 rest_forbidden.
FPurchase revoked (Customers > purchase > Revoke Access)same endpoint as BHTTP 404 media.not_found; the Downloads tab lists 0 files; public license status = revoked. Restore Access reverses all three.
GSDK release download: GET /v1/public/licenses/{key}/expose_current_release?activation_id=...public token + real activation id200 with a 15 minute signed URL to the current release ZIP. With a wrong or missing activation id: HTTP 403 “You must pass a valid activation to expose the current release for this license.”

5. The gate code (PART 2 of the demo plugin, copy into functions.php)

if ( ! defined( 'SCLGD_CACHE_TTL' ) ) {
	define( 'SCLGD_CACHE_TTL', 12 * HOUR_IN_SECONDS ); // how long a fresh answer is trusted.
}
if ( ! defined( 'SCLGD_GRACE_PERIOD' ) ) {
	define( 'SCLGD_GRACE_PERIOD', 3 * DAY_IN_SECONDS ); // how long an outage may last before locking.
}

/**
 * Get the current license state for this site.
 *
 * @param bool $force Ignore the cache and ask SureCart now.
 * @return array{unlocked:bool,reason:string,source:string,checked_at:int}
 */
function sclgd_license_state( $force = false ) {
	static $memo = null;
	if ( null !== $memo && ! $force ) {
		return $memo;
	}

	$client = $GLOBALS['sclgd_client'] ?? null;
	if ( ! $client ) {
		return $memo = array( 'unlocked' => false, 'reason' => 'sdk_not_loaded', 'source' => 'local', 'checked_at' => time() );
	}

	$settings      = $client->settings();
	$license_key   = (string) $settings->license_key;
	$activation_id = (string) $settings->activation_id;

	// 1. Nothing activated on this site -> locked, no network call.
	if ( '' === $license_key || '' === $activation_id ) {
		delete_transient( 'sclgd_license_state' );
		return $memo = array( 'unlocked' => false, 'reason' => 'no_license_activated', 'source' => 'local', 'checked_at' => time() );
	}

	// 2. Cached answer, bound to the key + activation so a (de)activation invalidates it at once.
	$cached = get_transient( 'sclgd_license_state' );
	if ( ! $force && is_array( $cached ) && ( $cached['key'] ?? '' ) === $license_key && ( $cached['activation'] ?? '' ) === $activation_id ) {
		$cached['source'] = 'cache';
		return $memo = $cached;
	}

	// 3. Ask SureCart (two small public API calls, authenticated with the store's public token).
	$valid      = $client->license()->is_valid( $license_key );          // GET /v1/public/licenses/{key}   -> true, false (revoked) or WP_Error
	$activation = $client->activation()->get( $activation_id );          // GET /v1/public/activations/{id} -> object or WP_Error(not_found)
	$active     = ! is_wp_error( $activation ) && ! empty( $activation->id ); // same test the SDK's is_active() makes

	// not_found = a definite "no" (key or activation really is gone). Any other error = could not reach the API.
	$outage = false;
	if ( is_wp_error( $valid ) ) {
		$outage = 'not_found' !== $valid->get_error_code();
		$valid  = false;
	}
	if ( is_wp_error( $activation ) && 'not_found' !== $activation->get_error_code() ) {
		$outage = true;
	}

	$state = array(
		'key'        => $license_key,
		'activation' => $activation_id,
		'checked_at' => time(),
		'source'     => 'api',
	);

	if ( $outage ) {
		$last_good = (int) get_option( 'sclgd_license_last_good', 0 );
		if ( $last_good && ( time() - $last_good ) < SCLGD_GRACE_PERIOD ) {
			$state['unlocked'] = true;
			$state['reason']   = 'api_unreachable_grace_period';
			$state['source']   = 'grace';
			set_transient( 'sclgd_license_state', $state, HOUR_IN_SECONDS ); // retry sooner during an outage.
			return $memo = $state;
		}
		$state['unlocked'] = false;
		$state['reason']   = 'api_unreachable_grace_expired';
		set_transient( 'sclgd_license_state', $state, HOUR_IN_SECONDS );
		return $memo = $state;
	}

	if ( true === $valid && $active ) {
		$state['unlocked'] = true;
		$state['reason']   = 'license_valid_and_activated';
		update_option( 'sclgd_license_last_good', time(), false );
	} else {
		$state['unlocked'] = false;
		$state['reason']   = ! $valid ? 'license_revoked_or_missing' : 'activation_removed_for_this_site';
	}

	set_transient( 'sclgd_license_state', $state, SCLGD_CACHE_TTL );
	return $memo = $state;
}

/**
 * Convenience helper for templates: true when premium features may run.
 */
function sclgd_is_unlocked() {
	return (bool) ( sclgd_license_state()['unlocked'] ?? false );
}

Usage anywhere in the theme: if ( sclgd_is_unlocked() ) { /* premium code */ }. The demo plugin also shows three gates in PART 3: an admin notice while locked, an admin page that is only registered while unlocked, and a shortcode / enqueued script that only load while unlocked. Full source: /wp-content/plugins/sc-license-gate-demo/sc-license-gate-demo.php on this site.

Notes for support (cleanup)

Created for ticket 1544414: plugin sc-license-gate-demo (active), product f9e4f5b3 + price 3b505de8 + media ac46983a + download 4752d25c (current release), test order 3d3fb4fd / purchase 028d6ae4 / license 5bf9be92 (key 60d4300d…), WP user 88 themebuyer1544414 linked to test customer 80e570c4, this page (722). Option sclgd_license_last_good and transient sclgd_license_state.

Scroll to Top