diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 3337eaa5c..d11fa5dfc 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -277,6 +277,10 @@ The self-hosted activation return notice on `/settings/system-pro` is part of that same boundary: the `trial` query result is a one-shot handoff outcome and must be consumed into owned UI state, then removed from the URL rather than becoming sticky page state across refresh and sharing. +That same shared notice owner must also keep activation-result framing +customer-facing: replayed handoffs should confirm the current entitlement state +rather than reading like a fresh failure, while invalid and unavailable states +should direct the operator back to the secure trial handoff on this instance. That same hosted owner also applies after Stripe returns to `/trial-signup/complete`: customer-facing completion failures must stay inside owned trial UX rather than dropping raw control-plane error strings, and they diff --git a/frontend-modern/src/utils/__tests__/licensePresentation.test.ts b/frontend-modern/src/utils/__tests__/licensePresentation.test.ts index b8f0d7463..d478657ad 100644 --- a/frontend-modern/src/utils/__tests__/licensePresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/licensePresentation.test.ts @@ -106,6 +106,18 @@ describe('licensePresentation', () => { title: 'Trial activated', tone: expect.stringContaining('green'), }); + expect(getTrialActivationNotice('replayed')).toMatchObject({ + title: 'Trial already activated', + tone: expect.stringContaining('sky'), + }); + expect(getTrialActivationNotice('invalid')).toMatchObject({ + title: 'Activation link invalid', + body: expect.stringContaining('fresh secure trial handoff'), + }); + expect(getTrialActivationNotice('unavailable')).toMatchObject({ + title: 'Activation unavailable', + body: expect.stringContaining('Refresh the billing state below'), + }); expect(getTrialActivationNotice('ineligible')).toMatchObject({ title: 'Trial not available', tone: expect.stringContaining('red'), diff --git a/frontend-modern/src/utils/licensePresentation.ts b/frontend-modern/src/utils/licensePresentation.ts index eb3ea0cce..51c9d3b36 100644 --- a/frontend-modern/src/utils/licensePresentation.ts +++ b/frontend-modern/src/utils/licensePresentation.ts @@ -207,19 +207,19 @@ export const getTrialActivationNotice = (result?: string | null): LicenseInlineN return { tone: 'border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-900 text-red-900 dark:text-red-100', title: 'Activation link invalid', - body: 'That activation handoff is invalid or expired. Start the hosted checkout flow again from this Pulse instance.', + body: 'That activation handoff is invalid or expired. Return to Pulse Pro settings on this instance and start a fresh secure trial handoff.', }; case 'replayed': return { - tone: 'border-amber-200 dark:border-amber-900 bg-amber-50 dark:bg-amber-900 text-amber-900 dark:text-amber-100', - title: 'Activation link already used', - body: 'This checkout handoff was already redeemed. Use the current entitlement state below or start a new checkout if needed.', + tone: 'border-sky-200 dark:border-sky-900 bg-sky-50 dark:bg-sky-900 text-sky-900 dark:text-sky-100', + title: 'Trial already activated', + body: 'This activation handoff was already redeemed for this instance. Use the current entitlement state below as the source of truth.', }; case 'unavailable': return { tone: 'border-amber-200 dark:border-amber-900 bg-amber-50 dark:bg-amber-900 text-amber-900 dark:text-amber-100', title: 'Activation unavailable', - body: 'Pulse could not finish activation right now. Retry the return link from checkout or start the flow again from this instance.', + body: 'Pulse could not finish activation right now. Refresh the billing state below, then retry the return link or start the secure trial handoff again from this instance if needed.', }; case 'ineligible': return {