Fix self-hosting pricing page design. (#19930)
Some checks are pending
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Docs / docs-lint (push) Blocked by required conditions
CI Emails / changed-files-check (push) Waiting to run
CI Emails / emails-test (push) Blocked by required conditions
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
Push docs to Crowdin / Push documentation to Crowdin (push) Waiting to run
Push translations to Crowdin / Extract and upload translations (push) Waiting to run

Resolve:
https://discord.com/channels/1130383047699738754/1496108238909739079
This commit is contained in:
Abdullah. 2026-04-21 18:03:17 +05:00 committed by GitHub
parent 2b399fc94e
commit 7947b1b843
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 229 additions and 91 deletions

View file

@ -1,6 +1,12 @@
'use client';
import {
BaseButton,
buttonBaseStyles,
} from '@/design-system/components/Button/BaseButton';
import { Body, Heading } from '@/design-system/components';
import { theme } from '@/theme';
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
import { useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
@ -11,21 +17,10 @@ type ActivationResult = {
subscriptionId: string;
};
const PageWrap = styled.div`
box-sizing: border-box;
margin-left: auto;
margin-right: auto;
margin-top: ${theme.spacing(12)};
max-width: 700px;
min-height: 60vh;
padding-left: ${theme.spacing(4)};
padding-right: ${theme.spacing(4)};
`;
const Title = styled.h1`
font-size: ${theme.font.size(8)};
font-weight: 600;
margin-bottom: ${theme.spacing(4)};
const ContentStack = styled.div`
display: flex;
flex-direction: column;
gap: ${theme.spacing(6)};
`;
const ErrorBox = styled.div`
@ -33,27 +28,27 @@ const ErrorBox = styled.div`
border: 1px solid ${theme.colors.accent.pink[70]};
border-radius: ${theme.radius(2)};
color: ${theme.colors.accent.pink[100]};
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(4)};
line-height: 1.55;
padding: ${theme.spacing(4)};
`;
const SuccessLead = styled.p`
const successLeadClassName = css`
color: ${theme.colors.accent.green[100]};
margin-bottom: ${theme.spacing(4)};
`;
const LicenseeRow = styled.div`
margin-bottom: ${theme.spacing(6)};
align-items: baseline;
display: flex;
flex-wrap: wrap;
gap: ${theme.spacing(1)};
`;
const KeyLabel = styled.div`
font-weight: 600;
margin-bottom: ${theme.spacing(2)};
`;
const KeyHint = styled.p`
color: ${theme.colors.primary.text[60]};
font-size: ${theme.font.size(3)};
margin-bottom: ${theme.spacing(2)};
const KeySection = styled.div`
display: flex;
flex-direction: column;
gap: ${theme.spacing(2)};
`;
const KeyBlock = styled.div`
@ -69,34 +64,63 @@ const KeyBlock = styled.div`
word-break: break-all;
`;
const CopyButton = styled.button<{ $copied: boolean }>`
background-color: ${({ $copied }) =>
$copied ? theme.colors.accent.green[100] : theme.colors.primary.text[100]};
border: none;
border-radius: ${theme.radius(1)};
color: ${theme.colors.primary.background[100]};
cursor: pointer;
font-size: ${theme.font.size(2)};
padding: ${theme.spacing(2)} ${theme.spacing(3)};
const CopyTrigger = styled.button<{ $copied: boolean }>`
${buttonBaseStyles}
position: absolute;
right: ${theme.spacing(2)};
top: ${theme.spacing(2)};
${({ $copied }) =>
$copied
? `
& [data-slot='button-base-shape'] path,
& [data-slot='button-base-shape'] rect {
fill: ${theme.colors.accent.green[100]} !important;
}
& [data-slot='button-hover-fill'] {
opacity: 0 !important;
pointer-events: none;
}
& [data-slot='button-label'] {
color: ${theme.colors.primary.background[100]} !important;
}
&:is(:hover, :focus-visible) [data-slot='button-label'] {
color: ${theme.colors.primary.background[100]} !important;
}
`
: ''}
`;
const NextStepsBox = styled.div`
background-color: ${theme.colors.primary.border[5]};
border: 1px solid ${theme.colors.accent.blue[70]};
border: 1px solid ${theme.colors.primary.border[20]};
border-left: 3px solid ${theme.colors.highlight[100]};
border-radius: ${theme.radius(2)};
margin-top: ${theme.spacing(8)};
display: flex;
flex-direction: column;
gap: ${theme.spacing(2)};
padding: ${theme.spacing(4)};
`;
const NextStepsList = styled.ol`
line-height: 1.75;
margin-top: ${theme.spacing(2)};
display: flex;
flex-direction: column;
gap: ${theme.spacing(2)};
list-style-position: outside;
margin: 0;
padding-left: ${theme.spacing(5)};
`;
const nextStepItemClassName = css`
&::marker {
color: ${theme.colors.primary.text[60]};
font-weight: ${theme.font.weight.medium};
}
`;
export function EnterpriseActivateClient() {
const searchParams = useSearchParams();
const sessionId = searchParams.get('session_id');
@ -159,53 +183,105 @@ export function EnterpriseActivateClient() {
};
return (
<PageWrap>
<Title>{'Enterprise activation'}</Title>
{loading && <p>{'Activating your enterprise license…'}</p>}
<ContentStack>
{loading && (
<Body
body={{ text: 'Activating your enterprise license…' }}
size="sm"
variant="body-paragraph"
/>
)}
{error !== null && <ErrorBox>{error}</ErrorBox>}
{result !== null && (
<div>
<SuccessLead>
{'Your enterprise license has been activated successfully.'}
</SuccessLead>
<>
<Body
body={{
text: 'Your enterprise license has been activated successfully.',
}}
className={successLeadClassName}
size="md"
weight="medium"
/>
<LicenseeRow>
<strong>{'Licensee:'}</strong> {result.licensee}
<Body
as="span"
body={{ text: 'Licensee: ' }}
size="sm"
weight="medium"
/>
<Body as="span" body={{ text: result.licensee }} size="sm" />
</LicenseeRow>
<KeyLabel>{'Your enterprise key'}</KeyLabel>
<KeyHint>
{
'Copy this key and paste it into your Twenty self-hosted instance settings.'
}
</KeyHint>
<KeySection>
<Heading
as="h2"
segments={{ fontFamily: 'sans', text: 'Your enterprise key' }}
size="xs"
weight="medium"
/>
<Body
body={{
text: 'Copy this key and paste it into your Twenty self-hosted instance settings.',
}}
size="sm"
variant="body-paragraph"
/>
<KeyBlock>
{result.enterpriseKey}
<CopyButton
$copied={copied}
onClick={() => void handleCopy()}
type="button"
>
{copied ? 'Copied!' : 'Copy'}
</CopyButton>
</KeyBlock>
<KeyBlock>
{result.enterpriseKey}
<CopyTrigger
$copied={copied}
data-color="primary"
data-size="small"
data-variant="contained"
onClick={() => void handleCopy()}
type="button"
>
<BaseButton
color="primary"
label={copied ? 'Copied!' : 'Copy'}
size="small"
variant="contained"
/>
</CopyTrigger>
</KeyBlock>
</KeySection>
<NextStepsBox>
<strong>{'Next steps'}</strong>
<Heading
as="h3"
segments={{ fontFamily: 'sans', text: 'Next steps' }}
size="xs"
weight="medium"
/>
<NextStepsList>
<li>{'Copy the enterprise key above.'}</li>
<li>
{'Open your Twenty self-hosted instance Settings → Enterprise.'}
<li className={nextStepItemClassName}>
<Body
body={{ text: 'Copy the enterprise key above.' }}
size="sm"
/>
</li>
<li className={nextStepItemClassName}>
<Body
body={{
text: 'Open your Twenty self-hosted instance Settings → Enterprise.',
}}
size="sm"
/>
</li>
<li className={nextStepItemClassName}>
<Body
body={{ text: 'Paste the key and click Activate.' }}
size="sm"
/>
</li>
<li>{'Paste the key and click Activate.'}</li>
</NextStepsList>
</NextStepsBox>
</div>
</>
)}
</PageWrap>
</ContentStack>
);
}

View file

@ -1,20 +1,16 @@
import { MENU_DATA } from '@/app/_constants';
import { EnterpriseActivateClient } from '@/app/enterprise/activate/EnterpriseActivateClient';
import { Body, Container, Eyebrow } from '@/design-system/components';
import type { HeadingType } from '@/design-system/components/Heading/types/Heading';
import { Pages } from '@/enums/pages';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { Hero } from '@/sections/Hero/components';
import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme';
import { css } from '@linaria/core';
import type { Metadata } from 'next';
import { Suspense } from 'react';
import { EnterpriseActivateClient } from './EnterpriseActivateClient';
const activateFallbackClassName = css`
box-sizing: border-box;
color: ${theme.colors.primary.text[60]};
margin-left: auto;
margin-right: auto;
margin-top: ${theme.spacing(12)};
max-width: 700px;
padding-left: ${theme.spacing(4)};
padding-right: ${theme.spacing(4)};
`;
import { styled } from '@linaria/react';
export const metadata: Metadata = {
title: 'Enterprise activation | Twenty',
@ -22,16 +18,82 @@ export const metadata: Metadata = {
'Complete activation for your Twenty self-hosted enterprise license.',
};
const ENTERPRISE_ACTIVATE_HEADING: HeadingType[] = [
{ text: 'Enterprise ', fontFamily: 'serif' },
{ text: 'activation', fontFamily: 'sans' },
];
const ENTERPRISE_ACTIVATE_BODY = {
text: 'Your checkout is complete. Follow the steps below to copy your license key into your Twenty instance.',
};
const ActivatePageContent = styled.section`
background-color: ${theme.colors.primary.background[100]};
flex: 1;
padding-bottom: ${theme.spacing(20)};
padding-top: ${theme.spacing(8)};
width: 100%;
`;
const ActivateContentInner = styled.div`
margin-left: auto;
margin-right: auto;
max-width: 640px;
width: 100%;
`;
function EnterpriseActivateFallback() {
return (
<div className={activateFallbackClassName}>{'Loading activation…'}</div>
<Body
body={{ text: 'Loading activation…' }}
size="sm"
variant="body-paragraph"
/>
);
}
export default function EnterpriseActivatePage() {
export default async function EnterpriseActivatePage() {
const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
<Suspense fallback={<EnterpriseActivateFallback />}>
<EnterpriseActivateClient />
</Suspense>
<>
<Menu.Root
backgroundColor="#F3F3F3"
scheme="primary"
navItems={MENU_DATA.navItems}
socialLinks={menuSocialLinks}
>
<Menu.Logo scheme="primary" />
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
<Menu.Social scheme="primary" socialLinks={menuSocialLinks} />
<Menu.Cta scheme="primary" />
</Menu.Root>
<Hero.Root
backgroundColor={theme.colors.secondary.background[5]}
colorScheme="primary"
>
<Eyebrow
colorScheme="primary"
heading={{ text: 'Self-hosting', fontFamily: 'sans' }}
/>
<Hero.Heading
page={Pages.Pricing}
segments={ENTERPRISE_ACTIVATE_HEADING}
/>
<Hero.Body body={ENTERPRISE_ACTIVATE_BODY} page={Pages.Pricing} />
</Hero.Root>
<ActivatePageContent>
<Container>
<ActivateContentInner>
<Suspense fallback={<EnterpriseActivateFallback />}>
<EnterpriseActivateClient />
</Suspense>
</ActivateContentInner>
</Container>
</ActivatePageContent>
</>
);
}