| 1 |
/* global wp */ |
| 2 |
/** |
| 3 |
* StoreEngine SDK License + Updates panel. |
| 4 |
* |
| 5 |
* Boots once per host plugin from SE_License_SDK_License::render_license_page(). |
| 6 |
* Reads its config from a `window.seSdkLicenseAppConfig_{slug}` object the |
| 7 |
* PHP side injects via wp_localize_script. |
| 8 |
* |
| 9 |
* Uses wp.element (React + ReactDOM that ships with WordPress) — no build |
| 10 |
* step, runs as plain JS in every WP admin from 5.3+. |
| 11 |
* |
| 12 |
* Visual language mirrors the legacy license-form.php: centered cards with |
| 13 |
* soft shadow, slate palette, 4px radius, the host's --se-sdk-primary-color. |
| 14 |
*/ |
| 15 |
( function ( wp ) { |
| 16 |
if ( ! wp || ! wp.element ) { |
| 17 |
return; |
| 18 |
} |
| 19 |
|
| 20 |
const { createElement: h, useState, useEffect, useCallback, Fragment, render } = wp.element; |
| 21 |
const { __, sprintf } = ( wp.i18n || { __: ( s ) => s, sprintf: ( s ) => s } ); |
| 22 |
|
| 23 |
function api( config, path, opts ) { |
| 24 |
opts = opts || {}; |
| 25 |
const url = config.restUrl + path.replace( /^\//, '' ); |
| 26 |
|
| 27 |
return fetch( url, { |
| 28 |
method: opts.method || 'GET', |
| 29 |
headers: { |
| 30 |
'Content-Type': 'application/json', |
| 31 |
'X-WP-Nonce': config.nonce, |
| 32 |
Accept: 'application/json', |
| 33 |
}, |
| 34 |
credentials: 'same-origin', |
| 35 |
body: opts.body ? JSON.stringify( opts.body ) : undefined, |
| 36 |
} ).then( async ( res ) => { |
| 37 |
const data = await res.json().catch( () => ( {} ) ); |
| 38 |
if ( ! res.ok ) { |
| 39 |
const err = new Error( data.message || res.statusText ); |
| 40 |
err.code = data.code; |
| 41 |
err.data = data.data || {}; |
| 42 |
err.status = res.status; |
| 43 |
err.log = ( data.data && data.data.log ) || null; |
| 44 |
throw err; |
| 45 |
} |
| 46 |
return data; |
| 47 |
} ); |
| 48 |
} |
| 49 |
|
| 50 |
function timeAgo( unix ) { |
| 51 |
if ( ! unix ) { |
| 52 |
return __( 'never', 'storeengine-sdk' ); |
| 53 |
} |
| 54 |
const seconds = Math.max( 0, Math.floor( Date.now() / 1000 - unix ) ); |
| 55 |
if ( seconds < 60 ) return __( 'just now', 'storeengine-sdk' ); |
| 56 |
if ( seconds < 3600 ) return sprintf( /* translators: %d: minutes */ __( '%d min ago', 'storeengine-sdk' ), Math.floor( seconds / 60 ) ); |
| 57 |
if ( seconds < 86400 ) return sprintf( __( '%d hr ago', 'storeengine-sdk' ), Math.floor( seconds / 3600 ) ); |
| 58 |
return sprintf( __( '%d days ago', 'storeengine-sdk' ), Math.floor( seconds / 86400 ) ); |
| 59 |
} |
| 60 |
|
| 61 |
function compareVersions( a, b ) { |
| 62 |
const pa = String( a ).split( '.' ).map( ( n ) => parseInt( n, 10 ) || 0 ); |
| 63 |
const pb = String( b ).split( '.' ).map( ( n ) => parseInt( n, 10 ) || 0 ); |
| 64 |
const len = Math.max( pa.length, pb.length ); |
| 65 |
for ( let i = 0; i < len; i++ ) { |
| 66 |
const ai = pa[ i ] || 0; |
| 67 |
const bi = pb[ i ] || 0; |
| 68 |
if ( ai !== bi ) return ai < bi ? -1 : 1; |
| 69 |
} |
| 70 |
return 0; |
| 71 |
} |
| 72 |
|
| 73 |
function refreshIcon() { |
| 74 |
return h( 'svg', { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }, |
| 75 |
h( 'polyline', { points: '23 4 23 10 17 10' } ), |
| 76 |
h( 'polyline', { points: '1 20 1 14 7 14' } ), |
| 77 |
h( 'path', { d: 'M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15' } ) |
| 78 |
); |
| 79 |
} |
| 80 |
|
| 81 |
function checkIcon() { |
| 82 |
return h( 'svg', { className: 'se-sdk-btn-icon', viewBox: '0 0 20 20', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', 'aria-hidden': true }, |
| 83 |
h( 'path', { fill: 'currentColor', d: 'M17.5 9.99992C17.5 5.85778 14.1421 2.49992 10 2.49992C5.85787 2.49992 2.50001 5.85778 2.50001 9.99992C2.50001 14.1421 5.85787 17.4999 10 17.4999C14.1421 17.4999 17.5 14.1421 17.5 9.99992ZM11.9108 7.74406C12.2363 7.41862 12.7638 7.41862 13.0892 7.74406C13.4146 8.0695 13.4146 8.59701 13.0892 8.92244L9.75587 12.2558C9.43043 12.5812 8.90292 12.5812 8.57748 12.2558L6.91082 10.5891C6.58538 10.2637 6.58538 9.73616 6.91082 9.41072C7.23625 9.08529 7.76377 9.08529 8.0892 9.41072L9.16668 10.4882L11.9108 7.74406Z' } ) |
| 84 |
); |
| 85 |
} |
| 86 |
|
| 87 |
/* ========================================================= |
| 88 |
Update status hero — top card, "Installed → Latest" |
| 89 |
========================================================= */ |
| 90 |
function StatusHero( props ) { |
| 91 |
const { state, onCheckNow, checking } = props; |
| 92 |
const updateAvailable = !! state.update_available; |
| 93 |
|
| 94 |
return h( 'div', { className: 'se-sdk-card' }, |
| 95 |
h( 'div', { className: 'se-sdk-hero' }, |
| 96 |
h( 'div', { className: 'se-sdk-hero-versions' }, |
| 97 |
h( 'div', { className: 'se-sdk-version-block' }, |
| 98 |
h( 'span', { className: 'se-sdk-version-label' }, __( 'Installed', 'storeengine-sdk' ) ), |
| 99 |
h( 'span', { className: 'se-sdk-version-value' }, state.current_version || '—' ) |
| 100 |
), |
| 101 |
updateAvailable && h( 'span', { className: 'se-sdk-hero-arrow', 'aria-hidden': true }, '→' ), |
| 102 |
updateAvailable && h( 'div', { className: 'se-sdk-version-block' }, |
| 103 |
h( 'span', { className: 'se-sdk-version-label' }, __( 'Latest', 'storeengine-sdk' ) ), |
| 104 |
h( 'span', { className: 'se-sdk-version-value is-latest' }, state.latest_version || '—' ) |
| 105 |
) |
| 106 |
), |
| 107 |
h( 'div', { className: 'se-sdk-hero-actions' }, |
| 108 |
h( 'span', { className: 'se-sdk-checked-at' }, |
| 109 |
sprintf( __( 'Checked %s', 'storeengine-sdk' ), timeAgo( state.last_checked_at ) ) |
| 110 |
), |
| 111 |
h( 'button', { |
| 112 |
type: 'button', |
| 113 |
className: 'se-sdk-btn se-sdk-btn-secondary', |
| 114 |
onClick: onCheckNow, |
| 115 |
disabled: checking, |
| 116 |
}, checking ? __( 'Checking…', 'storeengine-sdk' ) : __( 'Check for updates', 'storeengine-sdk' ) ) |
| 117 |
) |
| 118 |
) |
| 119 |
); |
| 120 |
} |
| 121 |
|
| 122 |
/* ========================================================= |
| 123 |
Update available banner (above license card when relevant) |
| 124 |
========================================================= */ |
| 125 |
function UpdateBanner( props ) { |
| 126 |
const { state, installing, installLog, onInstall } = props; |
| 127 |
|
| 128 |
if ( ! state.update_available ) { |
| 129 |
return null; |
| 130 |
} |
| 131 |
|
| 132 |
return h( 'div', { className: 'se-sdk-banner se-sdk-banner-update' }, |
| 133 |
h( 'div', { className: 'se-sdk-banner-message' }, |
| 134 |
h( 'div', { className: 'se-sdk-banner-title' }, |
| 135 |
sprintf( __( 'Version %s is available', 'storeengine-sdk' ), state.latest_version ) |
| 136 |
), |
| 137 |
state.changelog |
| 138 |
? h( 'p', { className: 'se-sdk-banner-sub' }, state.changelog ) |
| 139 |
: h( 'p', { className: 'se-sdk-banner-sub' }, |
| 140 |
__( 'Click update to install the latest release. Your settings and data stay intact.', 'storeengine-sdk' ) ) |
| 141 |
), |
| 142 |
h( 'div', { className: 'se-sdk-banner-actions' }, |
| 143 |
h( 'button', { |
| 144 |
type: 'button', |
| 145 |
className: 'se-sdk-btn', |
| 146 |
onClick: () => onInstall( null ), |
| 147 |
disabled: installing, |
| 148 |
}, installing |
| 149 |
? __( 'Installing…', 'storeengine-sdk' ) |
| 150 |
: sprintf( __( 'Update to %s', 'storeengine-sdk' ), state.latest_version ) |
| 151 |
) |
| 152 |
), |
| 153 |
installing && installLog.length > 0 && h( 'div', { className: 'se-sdk-install-progress' }, |
| 154 |
h( 'div', { className: 'se-sdk-progress-track' }, h( 'div', { className: 'se-sdk-progress-fill' } ) ), |
| 155 |
h( InstallLog, { messages: installLog } ) |
| 156 |
) |
| 157 |
); |
| 158 |
} |
| 159 |
|
| 160 |
function InstallLog( props ) { |
| 161 |
const { messages } = props; |
| 162 |
if ( ! messages || messages.length === 0 ) { |
| 163 |
return null; |
| 164 |
} |
| 165 |
return h( 'details', { className: 'se-sdk-log-drawer' }, |
| 166 |
h( 'summary', null, sprintf( __( 'Install log (%d entries)', 'storeengine-sdk' ), messages.length ) ), |
| 167 |
h( 'ul', { className: 'se-sdk-log-list' }, |
| 168 |
messages.map( ( m, i ) => |
| 169 |
h( 'li', { key: i, className: 'se-sdk-log-' + m.level }, |
| 170 |
h( 'span', { className: 'se-sdk-log-level' }, m.level ), |
| 171 |
h( 'span', { className: 'se-sdk-log-msg' }, m.message ) |
| 172 |
) |
| 173 |
) |
| 174 |
) |
| 175 |
); |
| 176 |
} |
| 177 |
|
| 178 |
/* ========================================================= |
| 179 |
Manual-update fallback — shown when an automatic install fails. |
| 180 |
Instructions only (no direct download link): the user downloads |
| 181 |
the package from their store account and uploads it by hand. |
| 182 |
========================================================= */ |
| 183 |
function ManualUpdateNotice( props ) { |
| 184 |
const { fallback, onDismiss } = props; |
| 185 |
if ( ! fallback ) { |
| 186 |
return null; |
| 187 |
} |
| 188 |
|
| 189 |
const isTheme = config.packageType === 'theme'; |
| 190 |
const uploadLabel = isTheme |
| 191 |
? __( 'Appearance → Themes → Add New → Upload Theme', 'storeengine-sdk' ) |
| 192 |
: __( 'Plugins → Add New → Upload Plugin', 'storeengine-sdk' ); |
| 193 |
|
| 194 |
const steps = [ |
| 195 |
config.storeDashboardUrl |
| 196 |
? sprintf( __( 'Download the latest %s package (.zip) from your account dashboard.', 'storeengine-sdk' ), config.packageName ) |
| 197 |
: sprintf( __( 'Download the latest %s package (.zip) from where you purchased it.', 'storeengine-sdk' ), config.packageName ), |
| 198 |
sprintf( __( 'In another tab, go to %s.', 'storeengine-sdk' ), uploadLabel ), |
| 199 |
__( 'Upload the .zip you downloaded and choose “Replace current with uploaded” if asked.', 'storeengine-sdk' ), |
| 200 |
__( 'Your settings and data are preserved — this only replaces the files.', 'storeengine-sdk' ), |
| 201 |
]; |
| 202 |
|
| 203 |
return h( 'div', { className: 'se-sdk-manual-update' }, |
| 204 |
h( 'div', { className: 'se-sdk-manual-update-head' }, |
| 205 |
h( 'strong', null, __( 'Automatic update didn’t complete — update manually', 'storeengine-sdk' ) ), |
| 206 |
h( 'button', { |
| 207 |
type: 'button', |
| 208 |
className: 'se-sdk-manual-dismiss', |
| 209 |
'aria-label': __( 'Dismiss', 'storeengine-sdk' ), |
| 210 |
onClick: onDismiss, |
| 211 |
}, '×' ) |
| 212 |
), |
| 213 |
fallback.message && h( 'p', { className: 'se-sdk-manual-reason' }, fallback.message ), |
| 214 |
h( 'ol', { className: 'se-sdk-manual-steps' }, |
| 215 |
steps.map( ( s, i ) => h( 'li', { key: i }, s ) ) |
| 216 |
), |
| 217 |
h( 'div', { className: 'se-sdk-manual-actions' }, |
| 218 |
config.storeDashboardUrl && h( 'a', { |
| 219 |
className: 'se-sdk-btn', |
| 220 |
href: config.storeDashboardUrl, |
| 221 |
target: '_blank', |
| 222 |
rel: 'noopener noreferrer', |
| 223 |
}, __( 'Open account dashboard', 'storeengine-sdk' ) ), |
| 224 |
h( 'a', { |
| 225 |
className: 'se-sdk-btn se-sdk-btn-secondary', |
| 226 |
href: config.uploadUrl, |
| 227 |
target: '_blank', |
| 228 |
rel: 'noopener noreferrer', |
| 229 |
}, isTheme ? __( 'Go to Upload Theme', 'storeengine-sdk' ) : __( 'Go to Upload Plugin', 'storeengine-sdk' ) ) |
| 230 |
) |
| 231 |
); |
| 232 |
} |
| 233 |
|
| 234 |
/* ========================================================= |
| 235 |
Rollback shortcut banner (only when previous_version known) |
| 236 |
========================================================= */ |
| 237 |
function RollbackBanner( props ) { |
| 238 |
const { state, installing, onRollback } = props; |
| 239 |
if ( ! state.previous_version ) return null; |
| 240 |
|
| 241 |
return h( 'div', { className: 'se-sdk-banner se-sdk-banner-rollback' }, |
| 242 |
h( 'div', { className: 'se-sdk-banner-message' }, |
| 243 |
h( 'div', { className: 'se-sdk-banner-title' }, |
| 244 |
sprintf( __( 'Quick rollback to v%s', 'storeengine-sdk' ), state.previous_version ) |
| 245 |
), |
| 246 |
h( 'p', { className: 'se-sdk-banner-sub' }, |
| 247 |
__( "If the latest update isn't working out, you can return to your previously installed version with one click.", 'storeengine-sdk' ) |
| 248 |
) |
| 249 |
), |
| 250 |
h( 'div', { className: 'se-sdk-banner-actions' }, |
| 251 |
h( 'button', { |
| 252 |
type: 'button', |
| 253 |
className: 'se-sdk-btn se-sdk-btn-secondary', |
| 254 |
onClick: () => onRollback( state.previous_version ), |
| 255 |
disabled: installing, |
| 256 |
}, sprintf( __( 'Roll back to v%s', 'storeengine-sdk' ), state.previous_version ) ) |
| 257 |
) |
| 258 |
); |
| 259 |
} |
| 260 |
|
| 261 |
/* ========================================================= |
| 262 |
License card — mirrors the legacy license-form.php layout |
| 263 |
========================================================= */ |
| 264 |
function LicenseCard( props ) { |
| 265 |
const { config, license, state, onActivate, onDeactivate, onCheckLicense, busy, error } = props; |
| 266 |
const [ keyDraft, setKeyDraft ] = useState( '' ); |
| 267 |
|
| 268 |
const active = license && license.status === 'active'; |
| 269 |
// Server already masks via get_public_data() → mask_string(). Render as-is. |
| 270 |
const maskedKey = license && license.license ? license.license : ''; |
| 271 |
|
| 272 |
const heroTitle = active |
| 273 |
? sprintf( __( '%s is active on this site.', 'storeengine-sdk' ), config.packageName ) |
| 274 |
: sprintf( __( 'Activate %s', 'storeengine-sdk' ), config.packageName ); |
| 275 |
|
| 276 |
const heroSub = active |
| 277 |
? __( 'You\'re receiving updates, security fixes and priority support.', 'storeengine-sdk' ) |
| 278 |
: __( 'Activate your license to unlock automatic updates, priority support, and all premium features.', 'storeengine-sdk' ); |
| 279 |
|
| 280 |
return h( 'div', { className: 'se-sdk-card' }, |
| 281 |
h( 'div', { className: 'se-sdk-license-hero' }, |
| 282 |
h( 'h2', { className: 'se-sdk-license-hero-title' }, heroTitle ), |
| 283 |
h( 'p', { className: 'se-sdk-license-hero-sub' }, heroSub ), |
| 284 |
h( 'form', { |
| 285 |
className: 'se-sdk-license-form', |
| 286 |
onSubmit: ( e ) => { |
| 287 |
e.preventDefault(); |
| 288 |
if ( active ) return; |
| 289 |
if ( keyDraft.trim() ) onActivate( keyDraft.trim() ); |
| 290 |
}, |
| 291 |
}, |
| 292 |
h( 'div', { className: 'se-sdk-license-row' }, |
| 293 |
h( 'input', { |
| 294 |
className: 'se-sdk-license-input', |
| 295 |
type: 'text', |
| 296 |
placeholder: __( 'Enter your license key to activate', 'storeengine-sdk' ), |
| 297 |
value: active ? maskedKey : keyDraft, |
| 298 |
readOnly: active, |
| 299 |
onChange: ( e ) => setKeyDraft( e.target.value ), |
| 300 |
disabled: busy, |
| 301 |
spellCheck: false, |
| 302 |
autoComplete: 'off', |
| 303 |
} ), |
| 304 |
active |
| 305 |
? h( Fragment, null, |
| 306 |
h( 'button', { |
| 307 |
type: 'button', |
| 308 |
className: 'se-sdk-btn se-sdk-btn-danger', |
| 309 |
onClick: onDeactivate, |
| 310 |
disabled: busy, |
| 311 |
}, busy ? __( 'Working…', 'storeengine-sdk' ) : __( 'Deactivate License', 'storeengine-sdk' ) ), |
| 312 |
config.storeDashboardUrl && h( 'a', { |
| 313 |
className: 'se-sdk-btn', |
| 314 |
href: config.storeDashboardUrl, |
| 315 |
target: '_blank', |
| 316 |
rel: 'noopener noreferrer', |
| 317 |
}, __( 'Manage License', 'storeengine-sdk' ) ) |
| 318 |
) |
| 319 |
: h( 'button', { |
| 320 |
type: 'submit', |
| 321 |
className: 'se-sdk-btn', |
| 322 |
disabled: busy || ! keyDraft.trim(), |
| 323 |
}, busy ? __( 'Activating…', 'storeengine-sdk' ) : h( Fragment, null, |
| 324 |
checkIcon(), |
| 325 |
h( 'span', null, __( 'Activate License', 'storeengine-sdk' ) ) |
| 326 |
) ) |
| 327 |
), |
| 328 |
! active && config.purchaseUrl && h( 'p', { className: 'se-sdk-purchase-prompt' }, |
| 329 |
__( "Don't have a license key? ", 'storeengine-sdk' ), |
| 330 |
h( 'a', { href: config.purchaseUrl, target: '_blank', rel: 'noopener noreferrer' }, |
| 331 |
__( 'Purchase one here', 'storeengine-sdk' ) |
| 332 |
) |
| 333 |
), |
| 334 |
error && h( 'div', { className: 'se-sdk-error' }, error ) |
| 335 |
) |
| 336 |
), |
| 337 |
active && h( StatsRow, { license, state, onCheckLicense, checking: busy } ) |
| 338 |
); |
| 339 |
} |
| 340 |
|
| 341 |
function StatsRow( props ) { |
| 342 |
const { license, state, onCheckLicense, checking } = props; |
| 343 |
const remaining = license.unlimited |
| 344 |
? '∞' |
| 345 |
: ( license.remaining !== undefined ? license.remaining : ( license.limit - license.activations ) ); |
| 346 |
const usage = license.unlimited |
| 347 |
? __( 'Unlimited', 'storeengine-sdk' ) |
| 348 |
: sprintf( __( '%1$s of %2$s', 'storeengine-sdk' ), remaining, license.limit ); |
| 349 |
|
| 350 |
// License "updated_at" comes back as an ISO/MySQL string from |
| 351 |
// get_public_data(); turn it into a unix timestamp for timeAgo(). |
| 352 |
const licenseCheckedUnix = license.updated_at |
| 353 |
? Math.floor( Date.parse( license.updated_at.replace( ' ', 'T' ) + 'Z' ) / 1000 ) |
| 354 |
: null; |
| 355 |
|
| 356 |
const autoUpdateOn = !! state.auto_update_enabled; |
| 357 |
|
| 358 |
return h( 'div', { className: 'se-sdk-stats' }, |
| 359 |
h( 'div', { className: 'se-sdk-stat' }, |
| 360 |
h( 'span', { className: 'se-sdk-stat-label' }, __( 'Status', 'storeengine-sdk' ) ), |
| 361 |
h( 'span', { className: 'se-sdk-stat-value' }, |
| 362 |
h( 'span', { className: 'se-sdk-pill se-sdk-pill-ok' }, __( 'Active', 'storeengine-sdk' ) ) |
| 363 |
) |
| 364 |
), |
| 365 |
h( 'div', { className: 'se-sdk-stat' }, |
| 366 |
h( 'span', { className: 'se-sdk-stat-label' }, __( 'Last Checked', 'storeengine-sdk' ) ), |
| 367 |
h( 'span', { className: 'se-sdk-stat-value' }, |
| 368 |
timeAgo( licenseCheckedUnix ), |
| 369 |
h( 'button', { |
| 370 |
type: 'button', |
| 371 |
className: 'se-sdk-refresh-link', |
| 372 |
onClick: onCheckLicense, |
| 373 |
disabled: checking, |
| 374 |
title: __( 'Check license status now', 'storeengine-sdk' ), |
| 375 |
'aria-label': __( 'Check license status now', 'storeengine-sdk' ), |
| 376 |
}, refreshIcon() ) |
| 377 |
) |
| 378 |
), |
| 379 |
h( 'div', { className: 'se-sdk-stat' }, |
| 380 |
h( 'span', { className: 'se-sdk-stat-label' }, __( 'Expires', 'storeengine-sdk' ) ), |
| 381 |
h( 'span', { className: 'se-sdk-stat-value' }, license.expires || __( 'N/A', 'storeengine-sdk' ) ) |
| 382 |
), |
| 383 |
h( 'div', { className: 'se-sdk-stat' }, |
| 384 |
h( 'span', { className: 'se-sdk-stat-label' }, __( 'Activations Remaining', 'storeengine-sdk' ) ), |
| 385 |
h( 'span', { className: 'se-sdk-stat-value' }, usage ) |
| 386 |
), |
| 387 |
h( 'div', { className: 'se-sdk-stat' }, |
| 388 |
h( 'span', { className: 'se-sdk-stat-label' }, __( 'WP Auto-update', 'storeengine-sdk' ) ), |
| 389 |
h( 'span', { className: 'se-sdk-stat-value' }, |
| 390 |
h( 'span', { |
| 391 |
className: 'se-sdk-pill ' + ( autoUpdateOn ? 'se-sdk-pill-ok' : 'se-sdk-pill' ), |
| 392 |
title: autoUpdateOn |
| 393 |
? __( "WordPress will auto-install updates for this plugin (toggled on the Plugins screen).", 'storeengine-sdk' ) |
| 394 |
: __( 'Toggle auto-update from WordPress→Plugins.', 'storeengine-sdk' ), |
| 395 |
}, autoUpdateOn ? __( 'Enabled', 'storeengine-sdk' ) : __( 'Disabled', 'storeengine-sdk' ) ) |
| 396 |
) |
| 397 |
) |
| 398 |
); |
| 399 |
} |
| 400 |
|
| 401 |
/* ========================================================= |
| 402 |
Version history |
| 403 |
========================================================= */ |
| 404 |
function VersionsTable( props ) { |
| 405 |
const { versions, loading, installing, onInstall, current, onRefresh, refreshing, error } = props; |
| 406 |
const [ confirming, setConfirming ] = useState( null ); |
| 407 |
|
| 408 |
const header = h( 'div', { className: 'se-sdk-card-header' }, |
| 409 |
h( 'div', null, |
| 410 |
h( 'h2', null, __( 'Version history', 'storeengine-sdk' ) ), |
| 411 |
h( 'p', { className: 'se-sdk-card-subtitle' }, |
| 412 |
__( 'Install or roll back to any released version of this plugin.', 'storeengine-sdk' ) |
| 413 |
) |
| 414 |
), |
| 415 |
h( 'button', { |
| 416 |
type: 'button', |
| 417 |
className: 'se-sdk-btn se-sdk-btn-secondary', |
| 418 |
onClick: onRefresh, |
| 419 |
disabled: refreshing || loading, |
| 420 |
title: __( 'Re-fetch the version list from the license server.', 'storeengine-sdk' ), |
| 421 |
}, refreshing ? __( 'Refreshing…', 'storeengine-sdk' ) : __( 'Refresh', 'storeengine-sdk' ) ) |
| 422 |
); |
| 423 |
|
| 424 |
const body = ( inner ) => h( 'div', { className: 'se-sdk-card' }, |
| 425 |
header, |
| 426 |
h( 'div', { className: 'se-sdk-card-body' }, inner ) |
| 427 |
); |
| 428 |
|
| 429 |
if ( loading ) { |
| 430 |
return body( h( 'p', { className: 'se-sdk-empty-state' }, __( 'Loading version history…', 'storeengine-sdk' ) ) ); |
| 431 |
} |
| 432 |
|
| 433 |
if ( ! versions || versions.length === 0 ) { |
| 434 |
if ( error && error.code === 'sdk-server-version-list-unsupported' ) { |
| 435 |
return body( h( 'div', { className: 'se-sdk-empty-state se-sdk-empty-state-soft' }, |
| 436 |
h( 'p', null, h( 'strong', null, __( "The license server doesn't support version history yet.", 'storeengine-sdk' ) ) ), |
| 437 |
h( 'p', null, error.message ), |
| 438 |
h( 'p', null, __( 'Status updates above still work — they use the older /check-update endpoint. The Refresh button retries once the server is upgraded.', 'storeengine-sdk' ) ) |
| 439 |
) ); |
| 440 |
} |
| 441 |
|
| 442 |
if ( error ) { |
| 443 |
return body( h( 'div', { className: 'se-sdk-empty-state se-sdk-empty-state-soft' }, |
| 444 |
h( 'p', null, h( 'strong', null, __( 'Could not load version history.', 'storeengine-sdk' ) ) ), |
| 445 |
h( 'p', null, error.message ) |
| 446 |
) ); |
| 447 |
} |
| 448 |
|
| 449 |
return body( h( 'div', { className: 'se-sdk-empty-state' }, |
| 450 |
h( 'p', null, h( 'strong', null, __( 'No versions are available to install or roll back to yet.', 'storeengine-sdk' ) ) ), |
| 451 |
h( 'p', null, __( "Once the vendor publishes additional released versions on the license server, they'll appear here with Install and Roll back buttons.", 'storeengine-sdk' ) ) |
| 452 |
) ); |
| 453 |
} |
| 454 |
|
| 455 |
const onlyCurrent = versions.length === 1 && versions[ 0 ].is_current; |
| 456 |
|
| 457 |
return h( 'div', { className: 'se-sdk-card' }, |
| 458 |
header, |
| 459 |
onlyCurrent && h( 'div', { className: 'se-sdk-card-body' }, |
| 460 |
h( 'div', { className: 'se-sdk-empty-state se-sdk-empty-state-soft' }, |
| 461 |
h( 'p', null, __( "You're on the only released version. Roll back will become available once an earlier release is published.", 'storeengine-sdk' ) ) |
| 462 |
) |
| 463 |
), |
| 464 |
h( 'div', { className: 'se-sdk-versions-table-wrap' }, |
| 465 |
h( 'table', { className: 'se-sdk-versions-table' }, |
| 466 |
h( 'thead', null, |
| 467 |
h( 'tr', null, |
| 468 |
h( 'th', null, __( 'Version', 'storeengine-sdk' ) ), |
| 469 |
h( 'th', null, __( 'Status', 'storeengine-sdk' ) ), |
| 470 |
h( 'th', null, __( 'Released', 'storeengine-sdk' ) ), |
| 471 |
h( 'th', { className: 'se-sdk-col-actions' }, '' ) |
| 472 |
) |
| 473 |
), |
| 474 |
h( 'tbody', null, |
| 475 |
versions.map( ( v ) => |
| 476 |
h( Fragment, { key: v.id }, |
| 477 |
h( 'tr', null, |
| 478 |
h( 'td', null, |
| 479 |
h( 'code', null, v.version ), |
| 480 |
v.is_current && ' ', |
| 481 |
v.is_current && h( 'span', { className: 'se-sdk-pill se-sdk-pill-current' }, __( 'current', 'storeengine-sdk' ) ) |
| 482 |
), |
| 483 |
h( 'td', null, |
| 484 |
h( 'span', { className: 'se-sdk-pill se-sdk-pill-' + v.status }, v.status ) |
| 485 |
), |
| 486 |
h( 'td', null, v.deployed_at || '—' ), |
| 487 |
h( 'td', { className: 'se-sdk-col-actions' }, |
| 488 |
v.is_current |
| 489 |
? h( 'span', { style: { color: '#738496', fontSize: 12 } }, __( 'Installed', 'storeengine-sdk' ) ) |
| 490 |
: ( ! v.allow_rollback && current && compareVersions( v.version, current ) < 0 ) |
| 491 |
? h( 'span', { style: { color: '#b26200', fontSize: 12 }, title: __( 'Vendor disabled rollback to this version.', 'storeengine-sdk' ) }, __( 'Rollback blocked', 'storeengine-sdk' ) ) |
| 492 |
: h( 'button', { |
| 493 |
type: 'button', |
| 494 |
className: 'se-sdk-btn se-sdk-btn-secondary', |
| 495 |
onClick: () => { |
| 496 |
if ( v.breaking_changes ) { |
| 497 |
setConfirming( v ); |
| 498 |
} else { |
| 499 |
onInstall( v.version ); |
| 500 |
} |
| 501 |
}, |
| 502 |
disabled: installing, |
| 503 |
}, ( current && compareVersions( v.version, current ) < 0 ) |
| 504 |
? __( 'Roll back', 'storeengine-sdk' ) |
| 505 |
: __( 'Install', 'storeengine-sdk' ) ) |
| 506 |
) |
| 507 |
), |
| 508 |
confirming && confirming.id === v.id && h( 'tr', { className: 'se-sdk-confirm-row' }, |
| 509 |
h( 'td', { colSpan: 4 }, |
| 510 |
h( 'div', { className: 'se-sdk-confirm-box' }, |
| 511 |
h( 'strong', null, __( '⚠ Breaking changes', 'storeengine-sdk' ) ), |
| 512 |
h( 'p', null, confirming.breaking_changes ), |
| 513 |
h( 'div', { className: 'se-sdk-confirm-actions' }, |
| 514 |
h( 'button', { |
| 515 |
type: 'button', |
| 516 |
className: 'se-sdk-btn', |
| 517 |
onClick: () => { |
| 518 |
const version = confirming.version; |
| 519 |
setConfirming( null ); |
| 520 |
onInstall( version ); |
| 521 |
}, |
| 522 |
}, __( 'I understand, continue', 'storeengine-sdk' ) ), |
| 523 |
h( 'button', { |
| 524 |
type: 'button', |
| 525 |
className: 'se-sdk-btn se-sdk-btn-secondary', |
| 526 |
onClick: () => setConfirming( null ), |
| 527 |
}, __( 'Cancel', 'storeengine-sdk' ) ) |
| 528 |
) |
| 529 |
) |
| 530 |
) |
| 531 |
) |
| 532 |
) |
| 533 |
) |
| 534 |
) |
| 535 |
) |
| 536 |
) |
| 537 |
); |
| 538 |
} |
| 539 |
|
| 540 |
/* ========================================================= |
| 541 |
Settings card |
| 542 |
Note: server endpoint /settings/auto-update-window + storage |
| 543 |
still exist for a future scheduled-update feature, but the |
| 544 |
UI is hidden until a window parser + cron are wired in. |
| 545 |
========================================================= */ |
| 546 |
function SettingsCard( props ) { |
| 547 |
const { state, onBeta, savingBeta } = props; |
| 548 |
|
| 549 |
return h( 'div', { className: 'se-sdk-card' }, |
| 550 |
h( 'div', { className: 'se-sdk-card-header' }, |
| 551 |
h( 'div', null, |
| 552 |
h( 'h2', null, __( 'Update preferences', 'storeengine-sdk' ) ), |
| 553 |
h( 'p', { className: 'se-sdk-card-subtitle' }, |
| 554 |
__( 'Tune how this site receives updates from the license server.', 'storeengine-sdk' ) |
| 555 |
) |
| 556 |
) |
| 557 |
), |
| 558 |
h( 'div', { className: 'se-sdk-card-body' }, |
| 559 |
h( 'div', { className: 'se-sdk-setting-row' }, |
| 560 |
h( 'label', { className: 'se-sdk-toggle' }, |
| 561 |
h( 'input', { |
| 562 |
type: 'checkbox', |
| 563 |
checked: !! state.beta_enabled, |
| 564 |
onChange: ( e ) => onBeta( e.target.checked ), |
| 565 |
disabled: savingBeta, |
| 566 |
} ), |
| 567 |
h( 'span', { className: 'se-sdk-setting-label' }, __( 'Receive beta updates', 'storeengine-sdk' ) ) |
| 568 |
), |
| 569 |
h( 'p', { className: 'se-sdk-setting-help' }, |
| 570 |
__( 'Includes pre-release versions tagged as beta on the license server.', 'storeengine-sdk' ) |
| 571 |
) |
| 572 |
) |
| 573 |
) |
| 574 |
); |
| 575 |
} |
| 576 |
|
| 577 |
/* ========================================================= |
| 578 |
Activation-limit takeover modal (Freemius-style) |
| 579 |
Shown when the server returns 409 license-activation-limit-reached |
| 580 |
with the list of the user's active sites. The user picks which |
| 581 |
site(s) to deactivate to free a seat, then activates this one. |
| 582 |
========================================================= */ |
| 583 |
function LimitReachedModal( props ) { |
| 584 |
const { modal, busy, onCancel, onConfirm } = props; |
| 585 |
const [ selected, setSelected ] = useState( [] ); |
| 586 |
|
| 587 |
const sites = modal.sites || []; |
| 588 |
|
| 589 |
const toggle = ( id ) => setSelected( ( prev ) => |
| 590 |
prev.indexOf( id ) === -1 ? prev.concat( id ) : prev.filter( ( x ) => x !== id ) |
| 591 |
); |
| 592 |
|
| 593 |
const lead = modal.limit |
| 594 |
? sprintf( |
| 595 |
/* translators: %s: activation limit */ |
| 596 |
__( 'This license is already active on its maximum of %s site(s). Choose the site(s) to deactivate so this site can take a seat.', 'storeengine-sdk' ), |
| 597 |
modal.limit |
| 598 |
) |
| 599 |
: __( 'This license has reached its activation limit. Choose the site(s) to deactivate so this site can take a seat.', 'storeengine-sdk' ); |
| 600 |
|
| 601 |
return h( 'div', { |
| 602 |
className: 'se-sdk-modal-overlay', |
| 603 |
onClick: ( e ) => { if ( e.target === e.currentTarget && ! busy ) onCancel(); }, |
| 604 |
}, |
| 605 |
h( 'div', { className: 'se-sdk-modal', role: 'dialog', 'aria-modal': 'true' }, |
| 606 |
h( 'div', { className: 'se-sdk-modal-header' }, |
| 607 |
h( 'h2', null, __( 'Activation limit reached', 'storeengine-sdk' ) ), |
| 608 |
h( 'button', { |
| 609 |
type: 'button', |
| 610 |
className: 'se-sdk-modal-close', |
| 611 |
onClick: onCancel, |
| 612 |
disabled: busy, |
| 613 |
'aria-label': __( 'Close', 'storeengine-sdk' ), |
| 614 |
}, '×' ) |
| 615 |
), |
| 616 |
h( 'div', { className: 'se-sdk-modal-body' }, |
| 617 |
h( 'p', { className: 'se-sdk-modal-lead' }, lead ), |
| 618 |
sites.length === 0 |
| 619 |
? h( 'p', { className: 'se-sdk-modal-empty' }, |
| 620 |
__( 'No other active sites were reported. Please deactivate a site from your account dashboard, then try again.', 'storeengine-sdk' ) ) |
| 621 |
: h( 'ul', { className: 'se-sdk-site-list' }, |
| 622 |
sites.map( ( s ) => |
| 623 |
h( 'li', { |
| 624 |
key: s.id, |
| 625 |
className: 'se-sdk-site-item' + ( selected.indexOf( s.id ) !== -1 ? ' is-selected' : '' ), |
| 626 |
}, |
| 627 |
h( 'label', { className: 'se-sdk-site-label' }, |
| 628 |
h( 'input', { |
| 629 |
type: 'checkbox', |
| 630 |
checked: selected.indexOf( s.id ) !== -1, |
| 631 |
onChange: () => toggle( s.id ), |
| 632 |
disabled: busy, |
| 633 |
} ), |
| 634 |
h( 'span', { className: 'se-sdk-site-info' }, |
| 635 |
h( 'span', { className: 'se-sdk-site-url' }, s.site_url || __( '(unknown site)', 'storeengine-sdk' ) ), |
| 636 |
s.activated_at && h( 'span', { className: 'se-sdk-site-date' }, |
| 637 |
sprintf( /* translators: %s: date */ __( 'Activated %s', 'storeengine-sdk' ), s.activated_at ) ) |
| 638 |
) |
| 639 |
) |
| 640 |
) |
| 641 |
) |
| 642 |
) |
| 643 |
), |
| 644 |
h( 'div', { className: 'se-sdk-modal-footer' }, |
| 645 |
h( 'button', { |
| 646 |
type: 'button', |
| 647 |
className: 'se-sdk-btn se-sdk-btn-secondary', |
| 648 |
onClick: onCancel, |
| 649 |
disabled: busy, |
| 650 |
}, __( 'Cancel', 'storeengine-sdk' ) ), |
| 651 |
h( 'button', { |
| 652 |
type: 'button', |
| 653 |
className: 'se-sdk-btn se-sdk-btn-danger', |
| 654 |
onClick: () => onConfirm( selected ), |
| 655 |
disabled: busy || selected.length < 1, |
| 656 |
}, busy |
| 657 |
? __( 'Working…', 'storeengine-sdk' ) |
| 658 |
: __( 'Deactivate selected & activate here', 'storeengine-sdk' ) |
| 659 |
) |
| 660 |
) |
| 661 |
) |
| 662 |
); |
| 663 |
} |
| 664 |
|
| 665 |
/* ========================================================= |
| 666 |
App root |
| 667 |
========================================================= */ |
| 668 |
function App( props ) { |
| 669 |
const { config } = props; |
| 670 |
const [ state, setState ] = useState( null ); |
| 671 |
const [ versions, setVersions ] = useState( null ); |
| 672 |
const [ versionsLoading, setVersionsLoading ] = useState( true ); |
| 673 |
const [ versionsError, setVersionsError ] = useState( null ); |
| 674 |
const [ checking, setChecking ] = useState( false ); |
| 675 |
const [ installing, setInstalling ] = useState( false ); |
| 676 |
const [ installLog, setInstallLog ] = useState( [] ); |
| 677 |
const [ licenseBusy, setLicenseBusy ] = useState( false ); |
| 678 |
const [ licenseError, setLicenseError ] = useState( null ); |
| 679 |
const [ savingBeta, setSavingBeta ] = useState( false ); |
| 680 |
const [ toast, setToast ] = useState( null ); |
| 681 |
const [ limitModal, setLimitModal ] = useState( null ); |
| 682 |
const [ manualFallback, setManualFallback ] = useState( null ); |
| 683 |
|
| 684 |
const refreshStatus = useCallback( () => { |
| 685 |
return api( config, 'updates/status' ).then( setState ); |
| 686 |
}, [ config ] ); |
| 687 |
|
| 688 |
const refreshVersions = useCallback( () => { |
| 689 |
setVersionsLoading( true ); |
| 690 |
setVersionsError( null ); |
| 691 |
return api( config, 'updates/versions' ) |
| 692 |
.then( ( res ) => setVersions( res && res.versions ? res.versions : [] ) ) |
| 693 |
.catch( ( err ) => { |
| 694 |
setVersions( [] ); |
| 695 |
setVersionsError( { code: err.code, message: err.message } ); |
| 696 |
} ) |
| 697 |
.finally( () => setVersionsLoading( false ) ); |
| 698 |
}, [ config ] ); |
| 699 |
|
| 700 |
const refreshLicense = useCallback( () => { |
| 701 |
if ( config.isFree ) return Promise.resolve(); |
| 702 |
return api( config, 'license/status' ).then( ( lic ) => { |
| 703 |
setState( ( s ) => Object.assign( {}, s || {}, { license: lic } ) ); |
| 704 |
} ); |
| 705 |
}, [ config ] ); |
| 706 |
|
| 707 |
useEffect( () => { |
| 708 |
refreshStatus().catch( () => setState( {} ) ); |
| 709 |
if ( ! config.isFree ) refreshLicense(); |
| 710 |
refreshVersions(); |
| 711 |
}, [ refreshStatus, refreshLicense, refreshVersions, config.isFree ] ); |
| 712 |
|
| 713 |
const showToast = useCallback( ( msg, kind ) => { |
| 714 |
setToast( { msg, kind: kind || 'info' } ); |
| 715 |
setTimeout( () => setToast( null ), 4000 ); |
| 716 |
}, [] ); |
| 717 |
|
| 718 |
const handleCheckNow = useCallback( () => { |
| 719 |
setChecking( true ); |
| 720 |
api( config, 'updates/check-now', { method: 'POST' } ) |
| 721 |
.then( ( fresh ) => { |
| 722 |
setState( fresh ); |
| 723 |
showToast( fresh.update_available |
| 724 |
? sprintf( __( 'Update available: v%s', 'storeengine-sdk' ), fresh.latest_version ) |
| 725 |
: __( "You're up to date.", 'storeengine-sdk' ), |
| 726 |
'success' ); |
| 727 |
} ) |
| 728 |
.catch( ( err ) => showToast( err.message, 'error' ) ) |
| 729 |
.finally( () => setChecking( false ) ); |
| 730 |
}, [ config, showToast ] ); |
| 731 |
|
| 732 |
const handleInstall = useCallback( ( version ) => { |
| 733 |
setInstalling( true ); |
| 734 |
setManualFallback( null ); |
| 735 |
setInstallLog( [ { level: 'info', message: __( 'Starting…', 'storeengine-sdk' ), time: Date.now() / 1000 } ] ); |
| 736 |
api( config, 'updates/install', { method: 'POST', body: version ? { version } : {} } ) |
| 737 |
.then( ( res ) => { |
| 738 |
setInstallLog( res.log || [] ); |
| 739 |
showToast( |
| 740 |
res.is_rollback |
| 741 |
? sprintf( __( 'Rolled back to v%s', 'storeengine-sdk' ), res.target_version ) |
| 742 |
: sprintf( __( 'Installed v%s', 'storeengine-sdk' ), res.target_version ), |
| 743 |
'success' |
| 744 |
); |
| 745 |
setTimeout( () => window.location.reload(), 1500 ); |
| 746 |
} ) |
| 747 |
.catch( ( err ) => { |
| 748 |
if ( err.log ) setInstallLog( err.log ); |
| 749 |
showToast( err.message, 'error' ); |
| 750 |
// Automatic install failed — surface step-by-step manual |
| 751 |
// download+upload instructions instead of a dead end. |
| 752 |
setManualFallback( { message: err.message, version: version } ); |
| 753 |
refreshStatus(); |
| 754 |
} ) |
| 755 |
.finally( () => setInstalling( false ) ); |
| 756 |
}, [ config, showToast, refreshStatus ] ); |
| 757 |
|
| 758 |
const handleActivate = useCallback( ( licenseKey, deactivateActivations ) => { |
| 759 |
setLicenseBusy( true ); |
| 760 |
setLicenseError( null ); |
| 761 |
|
| 762 |
const body = { license: licenseKey }; |
| 763 |
if ( deactivateActivations && deactivateActivations.length ) { |
| 764 |
body.deactivate_activations = deactivateActivations; |
| 765 |
} |
| 766 |
|
| 767 |
return api( config, 'license/activate', { method: 'POST', body } ) |
| 768 |
.then( ( res ) => { |
| 769 |
setState( ( s ) => Object.assign( {}, s || {}, { license: res.license } ) ); |
| 770 |
showToast( res.message || __( 'License activated.', 'storeengine-sdk' ), 'success' ); |
| 771 |
setLimitModal( null ); |
| 772 |
refreshVersions(); |
| 773 |
} ) |
| 774 |
.catch( ( err ) => { |
| 775 |
// Activation limit reached — open the "free a seat" picker |
| 776 |
// with the list of the user's active sites the server sent, |
| 777 |
// instead of showing a dead-end error. |
| 778 |
if ( err.code === 'license-activation-limit-reached' && err.data && Array.isArray( err.data.sites ) ) { |
| 779 |
setLimitModal( { |
| 780 |
licenseKey, |
| 781 |
sites: err.data.sites, |
| 782 |
limit: err.data.limit, |
| 783 |
activations: err.data.activations, |
| 784 |
message: err.message, |
| 785 |
} ); |
| 786 |
setLicenseError( null ); |
| 787 |
} else { |
| 788 |
setLimitModal( null ); |
| 789 |
setLicenseError( err.message ); |
| 790 |
} |
| 791 |
} ) |
| 792 |
.finally( () => setLicenseBusy( false ) ); |
| 793 |
}, [ config, showToast, refreshVersions ] ); |
| 794 |
|
| 795 |
const handleDeactivate = useCallback( () => { |
| 796 |
if ( ! window.confirm( __( 'Deactivate this license on this site?', 'storeengine-sdk' ) ) ) return; |
| 797 |
setLicenseBusy( true ); |
| 798 |
setLicenseError( null ); |
| 799 |
api( config, 'license/deactivate', { method: 'POST' } ) |
| 800 |
.then( ( res ) => { |
| 801 |
setState( ( s ) => Object.assign( {}, s || {}, { license: res.license } ) ); |
| 802 |
showToast( res.message || __( 'License deactivated.', 'storeengine-sdk' ), 'success' ); |
| 803 |
} ) |
| 804 |
.catch( ( err ) => setLicenseError( err.message ) ) |
| 805 |
.finally( () => setLicenseBusy( false ) ); |
| 806 |
}, [ config, showToast ] ); |
| 807 |
|
| 808 |
const handleCheckLicense = useCallback( () => { |
| 809 |
setLicenseBusy( true ); |
| 810 |
api( config, 'license/status?force=true' ) |
| 811 |
.then( ( lic ) => { |
| 812 |
setState( ( s ) => Object.assign( {}, s || {}, { license: lic } ) ); |
| 813 |
showToast( __( 'License re-checked.', 'storeengine-sdk' ), 'success' ); |
| 814 |
} ) |
| 815 |
.catch( ( err ) => showToast( err.message, 'error' ) ) |
| 816 |
.finally( () => setLicenseBusy( false ) ); |
| 817 |
}, [ config, showToast ] ); |
| 818 |
|
| 819 |
const handleBeta = useCallback( ( enabled ) => { |
| 820 |
setSavingBeta( true ); |
| 821 |
api( config, 'settings/beta-channel', { method: 'POST', body: { enabled } } ) |
| 822 |
.then( () => { |
| 823 |
setState( ( s ) => Object.assign( {}, s || {}, { beta_enabled: enabled } ) ); |
| 824 |
refreshStatus(); |
| 825 |
} ) |
| 826 |
.catch( ( err ) => showToast( err.message, 'error' ) ) |
| 827 |
.finally( () => setSavingBeta( false ) ); |
| 828 |
}, [ config, refreshStatus, showToast ] ); |
| 829 |
|
| 830 |
if ( ! state ) { |
| 831 |
return h( 'div', { className: 'se-sdk-loading' }, __( 'Loading…', 'storeengine-sdk' ) ); |
| 832 |
} |
| 833 |
|
| 834 |
return h( 'div', { className: 'se-sdk-app' }, |
| 835 |
toast && h( 'div', { className: 'se-sdk-toast se-sdk-toast-' + toast.kind }, toast.msg ), |
| 836 |
|
| 837 |
h( StatusHero, { state, onCheckNow: handleCheckNow, checking } ), |
| 838 |
|
| 839 |
h( UpdateBanner, { state, installing, installLog, onInstall: handleInstall } ), |
| 840 |
h( ManualUpdateNotice, { fallback: manualFallback, onDismiss: () => setManualFallback( null ) } ), |
| 841 |
h( RollbackBanner, { state, installing, onRollback: handleInstall } ), |
| 842 |
|
| 843 |
! config.isFree && h( LicenseCard, { |
| 844 |
config, |
| 845 |
license: state.license || config.initialLicense, |
| 846 |
state, |
| 847 |
onActivate: handleActivate, |
| 848 |
onDeactivate: handleDeactivate, |
| 849 |
onCheckLicense: handleCheckLicense, |
| 850 |
busy: licenseBusy, |
| 851 |
error: licenseError, |
| 852 |
} ), |
| 853 |
|
| 854 |
h( VersionsTable, { |
| 855 |
versions, |
| 856 |
loading: versionsLoading, |
| 857 |
installing, |
| 858 |
onInstall: handleInstall, |
| 859 |
current: state.current_version, |
| 860 |
onRefresh: refreshVersions, |
| 861 |
refreshing: versionsLoading, |
| 862 |
error: versionsError, |
| 863 |
} ), |
| 864 |
|
| 865 |
h( SettingsCard, { |
| 866 |
state, |
| 867 |
onBeta: handleBeta, |
| 868 |
savingBeta, |
| 869 |
} ), |
| 870 |
|
| 871 |
limitModal && h( LimitReachedModal, { |
| 872 |
modal: limitModal, |
| 873 |
busy: licenseBusy, |
| 874 |
onCancel: () => setLimitModal( null ), |
| 875 |
onConfirm: ( ids ) => handleActivate( limitModal.licenseKey, ids ), |
| 876 |
} ) |
| 877 |
); |
| 878 |
} |
| 879 |
|
| 880 |
function boot() { |
| 881 |
const configs = Object.keys( window ).filter( ( k ) => k.indexOf( 'seSdkLicenseAppConfig_' ) === 0 ); |
| 882 |
configs.forEach( ( key ) => { |
| 883 |
const config = window[ key ]; |
| 884 |
const mount = document.getElementById( config.mountId ); |
| 885 |
if ( ! mount ) return; |
| 886 |
render( h( App, { config } ), mount ); |
| 887 |
} ); |
| 888 |
} |
| 889 |
|
| 890 |
if ( document.readyState === 'loading' ) { |
| 891 |
document.addEventListener( 'DOMContentLoaded', boot ); |
| 892 |
} else { |
| 893 |
boot(); |
| 894 |
} |
| 895 |
}( window.wp ) ); |
| 896 |
|