Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fba594f
Add site key config resolution
jackmcdade Sep 1, 2026
5785ef2
Accept site_ and legacy key formats
jackmcdade Sep 1, 2026
d5aec87
Env writer for site key
jackmcdade Sep 1, 2026
b346149
Mint site key on install
jackmcdade Sep 1, 2026
ca7825f
Add site:fresh-key command
jackmcdade Sep 1, 2026
ac03aaa
Split identity and license messaging
jackmcdade Sep 1, 2026
d2b4268
CP UI identity vs license copy
jackmcdade Sep 1, 2026
d3f6e57
CLI please license command
jackmcdade Sep 1, 2026
9c7f257
CP license handoff
jackmcdade Sep 1, 2026
d662de9
Claim prompt on pro:enable
jackmcdade Sep 1, 2026
47efa1e
Document site keys
jackmcdade Sep 1, 2026
64e11fa
Add CI mint guards, shared-key copy, and site key rotation
jackmcdade Sep 1, 2026
7fd72e6
Drive licensing from a single connect-first action.
jackmcdade Sep 2, 2026
3dc771a
Skip the license device flow when Outpost already knows the site.
jackmcdade Sep 2, 2026
501c53e
Show a Renew License CTA when Pro is out of range.
jackmcdade Sep 2, 2026
f666654
Merge remote-tracking branch 'origin/6.x' into feature/site-key
jackmcdade Sep 3, 2026
db0559a
Harden site key linking and simplify the licensing model.
jackmcdade Sep 3, 2026
90f4872
Merge remote-tracking branch 'origin/6.x' into feature/site-key
jackmcdade Sep 3, 2026
23ef69f
Prioritize linking over buying in the Licensing utility.
jackmcdade Sep 3, 2026
8ca4663
Pass the current host into the licensing handoff URL.
jackmcdade Sep 3, 2026
cf5d489
Align ConfigTest with legacy license key precedence.
jackmcdade Sep 4, 2026
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
10 changes: 9 additions & 1 deletion CHANGELOG.md

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Individual PRs shouldn't update the CHANGELOG.md

Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Release Notes

## Unreleased

### What's new
- Generate a `STATAMIC_SITE_KEY` on install to identify a project across environments. A site key is not a license — attach one with `php please license` or the Control Panel. Sites with a `STATAMIC_LICENSE_KEY` are left alone.
- Add `php please site:fresh-key` for cloned templates, plus a matching "Generate new key" action in the Licensing utility for when someone else linked your key first.
- Generate a missing site key from the Control Panel Licensing utility.
- Clarify Licensing purchase actions with a checkout summary modal.
- Drive the Licensing utility from a single primary action (generate / link / buy / add domain) based on whether the site is linked to a statamic.com account.

## 6.31.0 (2026-09-01)

### What's new
Expand Down Expand Up @@ -37,7 +46,6 @@
- French translations [#15303](https://github.com/statamic/cms/issues/15303) by @ebeauchamps



## 6.30.0 (2026-08-28)

### What's new
Expand Down
28 changes: 28 additions & 0 deletions config/system.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,48 @@

return [

/*
|--------------------------------------------------------------------------
| Site Key
|--------------------------------------------------------------------------
|
| Identifies this project across environments. Generated on install.
| A site key is not a license — attach a license on statamic.com.
|
| https://statamic.dev/licensing
|
*/

'site_key' => env('STATAMIC_SITE_KEY'),

/*
|--------------------------------------------------------------------------
| License Key
|--------------------------------------------------------------------------
|
| The license key for the corresponding domain from your Statamic account.
| Without a key entered, your app will be considered to be in Trial Mode.
| Legacy alias for site_key — still accepted.
|
| https://statamic.dev/licensing#trial-mode
|
*/

'license_key' => env('STATAMIC_LICENSE_KEY'),

/*
|--------------------------------------------------------------------------
| Licensing Endpoints
|--------------------------------------------------------------------------
|
| Override these in local/dev to point at a statamic.com worktree.
|
*/

'licensing_url' => env('STATAMIC_LICENSING_URL', 'https://statamic.com'),

'outpost_url' => env('STATAMIC_OUTPOST_URL', 'https://outpost.statamic.com/v3/query'),

/*
|--------------------------------------------------------------------------
| Enable Multi-site
Expand Down
23 changes: 20 additions & 3 deletions lang/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -164,16 +164,33 @@
'licensing_error_invalid_domain' => 'Invalid domain',
'licensing_error_invalid_edition' => 'License is for :edition edition',
'licensing_error_no_domains' => 'No domains defined',
'licensing_error_no_site_key' => 'No site license key',
'licensing_error_no_site_key' => 'No site key',
'licensing_error_outside_license_range' => 'License valid for versions :start and :end',
'licensing_error_unknown_site' => 'Unknown site',
'licensing_error_unknown_site' => 'Site key not recognized',
'licensing_error_unlicensed' => 'Unlicensed',
'licensing_incorrect_key_format_body' => 'Your site key format is incorrect. Site keys are 16-character alphanumeric strings. Get your current key from your account at <a href="https://statamic.com" class="underline">statamic.com</a>. Do not use legacy UUID license keys.',
'licensing_incorrect_key_format_body' => 'Your site key format is incorrect. Site keys look like <code>site_</code> followed by 26 characters, or a legacy 16-character alphanumeric key. Get your current key from your account at <a href="https://statamic.com" class="underline">statamic.com</a>. Do not use UUID license keys.',
'licensing_incorrect_key_format_heading' => 'Incorrect site key format',
'licensing_production_alert' => 'This site is running Statamic Pro and commercial addons on a production domain. To continue using these features, please ensure you have valid licenses.',
'licensing_production_alert_addons' => 'This site is using commercial addons on a production domain. To continue using these features, please ensure you have valid licenses.',
'licensing_production_alert_renew_statamic' => 'Your Statamic Pro license needs to be renewed to use this version. Please update your license or downgrade your Statamic version to continue.',
'licensing_production_alert_statamic' => 'This site is running Statamic Pro on a production domain. To continue using Pro features, please ensure you have a valid license.',
'licensing_site_key_already_exists' => 'This site already has a key.',
'licensing_site_key_found' => 'Site key found — no license attached yet. Run `php please license` to attach one.',
'licensing_site_key_generated' => 'Site key generated. Commit .env.example so other environments pick it up.',
'licensing_site_key_missing' => 'This site does not have a site key yet. Generate one from the Licensing utility.',
'licensing_fresh_key_unavailable' => 'A new site key can only be generated before this key has been linked to an account.',
'licensing_fresh_key_prompt' => 'This site key is already linked to a different account. Generate a new key so this project gets its own identity.',
'licensing_fresh_key_confirm' => 'Generate a new site key? Any other environment using the old key will need to be updated.',
'licensing_not_connected' => 'This site key isn\'t linked to a statamic.com account yet.',
'licensing_connected_unlicensed' => 'This site is linked to a statamic.com account but has no license attached.',
'licensing_connected_needs_renewal' => 'This site\'s Statamic Pro license needs to be renewed.',
'licensing_renew_pro_description' => 'Your Statamic Pro license does not cover this version. Renew to keep using it, or downgrade to a version in range.',
'licensing_refresh_instructions' => 'License data refreshes from statamic.com hourly. Use Refresh after you make changes there.',
'licensing_buy_addon_detail' => ':author',
'licensing_buy_addons_description' => 'These commercial addons need a license for :site.',
'licensing_buy_mixed_description' => ':site needs Statamic Pro and commercial addon licenses.',
'licensing_buy_pro_description' => 'Statamic Pro unlocks users, revisions, Git, multi-site, and more. One license per production site.',
'licensing_buy_pro_detail' => 'Per site. First year, then $99/year to renew.',
'licensing_sync_instructions' => 'License data syncs from statamic.com hourly. Use the sync button to check for any recent changes you\'ve made.',
'licensing_trial_mode_alert' => 'This site is using Statamic Pro and commercial addons in trial mode. Valid licenses will be required when you\'re ready to launch.',
'licensing_trial_mode_alert_addons' => 'This site is using commercial addons in trial mode. Valid licenses will be required when you\'re ready to launch.',
Expand Down
97 changes: 95 additions & 2 deletions resources/js/components/LicensingAlert.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
<script setup>
import { Modal, Description, Button } from '@/components/ui';
import { computed, ref } from 'vue';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import useStatamicPageProps from '@/composables/page-props.js';
import { router } from '@inertiajs/vue3';
import statamicMark from '@/../svg/statamic-mark-lime.svg?raw';

const { licensing } = useStatamicPageProps();
const { alert } = licensing;
const message = ref(alert?.message);
const testing = ref(alert?.testing);
const manageUrl = ref(alert?.manageUrl);
const handoffUrl = ref(alert?.handoffUrl);
const siteUrl = ref(alert?.siteUrl);
const refreshUrl = ref(alert?.refreshUrl);
const mintUrl = ref(alert?.mintUrl);
const primaryAction = ref(alert?.primaryAction);
const connected = ref(alert?.connected);
const minting = ref(false);
const awaitingReturn = ref(false);
const key = 'statamic.snooze_license_banner';
const open = ref(localStorage.getItem(key) < new Date().valueOf());
const snoozeMinutes = computed(() => testing.value ? (24 * 60) : 5);
const snoozeMilliseconds = computed(() => snoozeMinutes.value * 60 * 1000);

function mint() {
if (!mintUrl.value || minting.value) {
return;
}

minting.value = true;
router.post(mintUrl.value, {}, {
onFinish: () => {
minting.value = false;
},
});
}

function snooze() {
open.value = false;
localStorage.setItem(key, new Date(Date.now() + snoozeMilliseconds.value).valueOf());
Expand All @@ -23,6 +45,35 @@ function manageLicenses() {
snooze();
router.get(manageUrl.value);
}

function markOutbound() {
awaitingReturn.value = true;
}

function maybeRefresh() {
if (!awaitingReturn.value || !refreshUrl.value) {
return;
}

awaitingReturn.value = false;
router.visit(refreshUrl.value);
}

function onVisibilityChange() {
if (document.visibilityState === 'visible') {
maybeRefresh();
}
}

onMounted(() => {
document.addEventListener('visibilitychange', onVisibilityChange);
window.addEventListener('focus', maybeRefresh);
});

onUnmounted(() => {
document.removeEventListener('visibilitychange', onVisibilityChange);
window.removeEventListener('focus', maybeRefresh);
});
</script>

<template>
Expand All @@ -42,7 +93,49 @@ function manageLicenses() {
<template #footer>
<div class="flex items-center justify-end space-x-3 pt-3 pb-1">
<Button @click="snooze" :text="__('Snooze')" variant="ghost" tabindex="-1" />
<Button v-if="manageUrl" @click="manageLicenses" :text="__('Manage Licenses')" />
<Button v-if="manageUrl" @click="manageLicenses" :text="__('View details')" variant="ghost" />
<Button
v-if="!connected && manageUrl"
:text="__('Buy Licenses')"
@click="manageLicenses"
/>
<Button
v-if="primaryAction === 'mint'"
:disabled="minting"
variant="primary"
:text="__('Generate site key')"
@click="mint"
/>
<Button
v-if="connected && primaryAction === 'buy' && manageUrl"
variant="primary"
:text="__('Buy Licenses')"
@click="manageLicenses"
/>
<Button
v-if="connected && primaryAction === 'renew' && manageUrl"
variant="primary"
:text="__('Renew License')"
@click="manageLicenses"
/>
<Button
v-if="connected && primaryAction === 'domain'"
:href="siteUrl"
target="_blank"
variant="primary"
:text="__('Add domain on statamic.com')"
@click="markOutbound"
/>
<Button
v-if="handoffUrl && !connected"
:href="handoffUrl"
target="_blank"
variant="primary"
:icon="statamicMark"
:text="__('Link to Account')"
class="[&>svg]:opacity-100!"
@click="markOutbound"
/>
</div>
</template>
</Modal>
Expand Down
Loading
Loading