diff --git a/e2e/stack-files.spec.ts b/e2e/stack-files.spec.ts index 64c632b7..ecaad5c7 100644 --- a/e2e/stack-files.spec.ts +++ b/e2e/stack-files.spec.ts @@ -198,7 +198,7 @@ test.describe('File explorer - community (read-only)', () => { test('upgrade pill is visible in the left pane', async ({ page }) => { await expect( - page.getByRole('button', { name: /upgrade to unlock/i }) + page.getByRole('button', { name: /upgrade to unlock upload/i }) ).toBeVisible({ timeout: 5_000 }); }); diff --git a/frontend/src/components/AdmiralGate.tsx b/frontend/src/components/AdmiralGate.tsx index f99ca86d..fba8d428 100644 --- a/frontend/src/components/AdmiralGate.tsx +++ b/frontend/src/components/AdmiralGate.tsx @@ -25,7 +25,11 @@ export function AdmiralGate({ children, featureName = 'This feature', compact = if (isPaid && license?.variant === 'admiral') return <>{children}; - if (compact || dismissed) { + // Inline lock for list items (single SSO provider card, etc.). Renders the + // children blurred behind a small pill so the surrounding list keeps its + // shape; the IP exposure is minor because the children are tiny inline UI, + // and the visual continuity is intentional. Dismissal does not apply here. + if (compact) { return (
@@ -41,6 +45,30 @@ export function AdmiralGate({ children, featureName = 'This feature', compact = ); } + // Post-dismissal placeholder for full-page consumers. Renders only the + // pill so the lazy-loaded children behind an Admiral view never mount + // during the dismissal window. Clicking the pill clears the dismissal + // flag and restores the full upsell card so users who dismissed + // accidentally or want to revisit pricing have a way back without + // clearing localStorage. + if (dismissed) { + return ( +
+ +
+ ); + } + return (
diff --git a/frontend/src/components/PaidGate.tsx b/frontend/src/components/PaidGate.tsx index 2293e832..3104458d 100644 --- a/frontend/src/components/PaidGate.tsx +++ b/frontend/src/components/PaidGate.tsx @@ -25,7 +25,11 @@ export function PaidGate({ children, featureName = 'This feature', compact = fal if (isPaid) return <>{children}; - if (compact || dismissed) { + // Inline lock for list items (single SSO provider card, etc.). Renders the + // children blurred behind a small pill so the surrounding list keeps its + // shape; the IP exposure is minor because the children are tiny inline UI, + // and the visual continuity is intentional. Dismissal does not apply here. + if (compact) { return (
@@ -41,6 +45,29 @@ export function PaidGate({ children, featureName = 'This feature', compact = fal ); } + // Post-dismissal placeholder for full-page consumers. Renders only the + // pill so the lazy-loaded children behind a paid view never mount during + // the dismissal window. Clicking the pill clears the dismissal flag and + // restores the full upsell card so users who dismissed accidentally or + // want to revisit pricing have a way back without clearing localStorage. + if (dismissed) { + return ( +
+ +
+ ); + } + return (