Unify oAuth success and failure screen with autorize page (#20746)

Goal, matching authorize page design

<img width="2062" height="1376" alt="image"
src="https://github.com/user-attachments/assets/93d0f77a-c769-4a32-b41b-16459378314f"
/>


## Before

<img width="1540" height="942" alt="image"
src="https://github.com/user-attachments/assets/4de64f37-8519-4fdc-9388-70d98a69663e"
/>


## After

<img width="962" height="657" alt="image"
src="https://github.com/user-attachments/assets/c4fd19aa-13e3-452e-b6b7-c10202cb1edf"
/>

<img width="705" height="437" alt="image"
src="https://github.com/user-attachments/assets/e1f743f1-a862-4629-a3d0-c8e62e429e04"
/>
This commit is contained in:
martmull
2026-05-20 09:43:34 +02:00
committed by GitHub
parent b821061526
commit 7ebcbe8801
2 changed files with 75 additions and 38 deletions
File diff suppressed because one or more lines are too long
@@ -1,5 +1,7 @@
import http from 'node:http';
import { OAUTH_MODAL_HEADER_IMAGE_BASE64 } from '@/cli/constants/oauth-modal-header-image-base64';
type CallbackResult =
| { success: true; code: string }
| { success: false; error: string };
@@ -11,12 +13,6 @@ type CallbackServer = {
close: () => void;
};
const TWENTY_LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 96 96">
<rect width="96" height="96" rx="11.3" fill="#000"/>
<path fill="#fff" d="M19.25 35.75c0-5.25 4.26-9.5 9.5-9.5h18.29c.27 0 .51.16.63.4.11.25.06.54-.12.75l-4.01 4.35c-.7.76-1.68 1.2-2.71 1.2H28.8c-1.57 0-2.85 1.27-2.85 2.85v7.18c0 .93-.75 1.67-1.68 1.67h-3.34c-.93 0-1.67-.75-1.67-1.67v-7.23z"/>
<path fill="#fff" d="M76.15 60.25c0 5.25-4.26 9.5-9.5 9.5h-7.77c-5.25 0-9.5-4.25-9.5-9.5V46.65c0-.93.35-1.82.98-2.5l4.53-4.92c.19-.2.49-.27.75-.17.26.11.44.36.44.64v20.52c0 1.57 1.28 2.85 2.85 2.85h7.68c1.57 0 2.85-1.28 2.85-2.85V35.8c0-1.57-1.28-2.85-2.85-2.85h-8.93c-1.02 0-2 .43-2.7 1.18L28.35 63.06h16c.92 0 1.67.75 1.67 1.68v3.34c0 .93-.75 1.67-1.67 1.67H22.79c-1.95 0-3.55-1.59-3.55-3.54v-1.77c0-.89.33-1.75.94-2.4l29.86-32.43c1.98-2.15 4.75-3.36 7.67-3.36h8.93c5.25 0 9.5 4.25 9.5 9.5v24.5z"/>
</svg>`;
const pageHtml = ({
title,
message,
@@ -48,34 +44,67 @@ const pageHtml = ({
.card {
background: #fff;
border-radius: 8px;
box-shadow: 2px 4px 16px rgba(0,0,0,0.08), 0 2px 4px rgba(0,0,0,0.04);
padding: 32px;
width: 400px;
max-width: calc(100vw - 32px);
box-shadow: 2px 4px 16px 0 rgba(0,0,0,0.16), 0 2px 4px 0 rgba(0,0,0,0.08);
max-width: min(100%, 360px);
width: fit-content;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
overflow: hidden;
}
.logo { margin-bottom: 4px; }
.icon {
width: 48px;
height: 48px;
border-radius: 50%;
.header {
height: 120px;
width: 100%;
display: flex;
align-items: center;
gap: 8px;
justify-content: center;
background-image: url('data:image/png;base64,${OAUTH_MODAL_HEADER_IMAGE_BASE64}');
background-position: center;
background-size: cover;
}
.logo-tile {
width: 36px;
height: 36px;
padding: 4px;
background: #fff;
border-radius: 100%;
box-shadow: 2px 4px 16px 0 rgba(0,0,0,0.16), 0 2px 4px 0 rgba(0,0,0,0.08);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.status-icon {
width: 24px;
height: 24px;
border-radius: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.icon-success { background: #f0faf0; }
.icon-error { background: #fef0f0; }
.icon svg { width: 24px; height: 24px; }
.status-icon svg {
width: 20px;
height: 20px;
}
.status-icon-success { background: #f0faf0; }
.status-icon-error { background: #fef0f0; }
.content {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px;
}
h2 {
font-size: 1.23rem;
font-weight: 600;
font-size: 1rem;
font-weight: 500;
line-height: 1.1;
color: #333;
text-align: center;
text-wrap: balance;
margin-bottom: 16px;
}
p {
font-size: 0.92rem;
font-size: 0.82rem;
color: #666;
text-align: center;
line-height: 1.5;
@@ -84,25 +113,31 @@ const pageHtml = ({
</head>
<body>
<div class="card">
<div class="logo">${TWENTY_LOGO_SVG}</div>
<div class="icon ${isSuccess ? 'icon-success' : 'icon-error'}">
${
isSuccess
? '<svg viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>'
: '<svg viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>'
}
<div class="header">
<div class="logo-tile">
<div class="status-icon ${isSuccess ? 'status-icon-success' : 'status-icon-error'}">
${
isSuccess
? '<svg viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>'
: '<svg viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>'
}
</div>
</div>
</div>
<div class="content">
<h2>${title}</h2>
<p>${message}</p>
</div>
<h2>${title}</h2>
<p>${message}</p>
</div>
</body>
</html>`;
const SUCCESS_HTML = pageHtml({
title: 'Authentication successful',
message: 'You can close this window and return to the terminal.',
isSuccess: true,
});
const successHtml = () =>
pageHtml({
title: 'Authentication successful',
message: 'You can close this window and return to the terminal.',
isSuccess: true,
});
const escapeHtml = (text: string): string =>
text
@@ -152,7 +187,7 @@ export const startCallbackServer = (options?: {
if (code) {
res.writeHead(200, headers);
res.end(SUCCESS_HTML);
res.end(successHtml());
callbackResolve({ success: true, code });
} else {
const errorMessage =