Skip to content

Restyle login page with white card layout + Certified branding - #110

Merged
s-adamantine merged 8 commits into
mainfrom
restyle-login-page
Apr 29, 2026
Merged

Restyle login page with white card layout + Certified branding#110
s-adamantine merged 8 commits into
mainfrom
restyle-login-page

Conversation

@s-adamantine

@s-adamantine s-adamantine commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Restyle the OAuth login page (packages/auth-service/src/routes/login-page.ts):

Before:

Screenshot 2026-04-23 at 10 57 56 Screenshot 2026-04-23 at 10 58 25

After:
Screenshot 2026-04-23 at 01 30 18

Screenshot 2026-04-23 at 10 53 02
  • White card on muted grey: floating card layout, rounded inputs, pill-shaped Continue + ATProto buttons, "Powered by Certified" footer below the card.
  • New Certified branding: inlined Certified wordmark SVG (themeable via currentColor), Certified brandmark as fallback top logo when a client omits logo_uri.
  • "Or sign in with ATProto/Bluesky" button: toggles the email form into handle-entry mode (same pattern as the demo's LoginForm). Handle submission redirects to <client-origin>/api/oauth/login?handle=X, which resolves the handle dynamically and starts a fresh PAR against the correct PDS — so off-PDS handles end up on their own PDS's auth screen, just like the demo's flow.
  • Terms & Privacy line: hardcoded By signing in, you agree to Certified's Terms of Use and Privacy Policy. — both links underlined and pointing to https://certified.app/terms and https://certified.app/privacy. Copy does not vary per client (the auth server is always Certified's, regardless of which client initiated the flow).
  • Copy tweaks: Email addressEnter your email address, Continue with emailContinue, removed to use <AppName> subtitle, removed the h1 subtitle treatment.

Theming — CSS variables for trusted clients

Trusted clients can override these from their injected branding.css:

:root {
  --page-bg: #YOUR_OUTER_BG;
  --card-bg: #YOUR_CARD_BG;
  --input-bg: #YOUR_INPUT_BG;
  --input-border: #YOUR_INPUT_BORDER;
  --muted-foreground: #YOUR_MUTED_TEXT;  /* terms + "Powered by" color */
}

Defaults: --page-bg: #E8E8E8, --card-bg: #F8F8F8, --input-bg: #ffffff, --input-border: #e5e5e5, --muted-foreground: #999. The email input and the ATProto button consume the same --input-bg / --input-border so they stay visually matched.

Hiding the "Recover with backup email" link

The recovery link in the OTP step is shown by default. Clients that don't want to surface backup-email recovery from the OAuth login flow can hide it from their branding.css:

:root { --recovery-link-display: none; }

The link itself stays in the DOM (so the recovery flow at /auth/recover is still reachable via direct navigation, e.g. from the /account page) — only its appearance on the login page is suppressed. Default is block.

Assets added

  • packages/auth-service/public/certified-text-monochrome.svg — wordmark (inlined into the page, themeable via currentColor)
  • packages/auth-service/public/certified-brandmark.svg — square icon (fallback top logo, served at /static/certified-brandmark.svg)

Before / After

Before:

After:

Preview URLs (PR-110 Railway environment)

Test plan

  • Launch stack and visit the login page via the demo app — confirm card layout, rounded inputs, footer.
  • ATProto button toggles email ↔ handle mode; submit in handle mode redirects to <client-origin>/api/oauth/login?handle=X and (for off-PDS handles) ends up on the correct PDS's auth screen.
  • Log in from a client without logo_uri — Certified brandmark appears as top logo.
  • Log in from a client with logo_uri — its logo appears at top.
  • Trusted-client branding.css overrides work: set --page-bg, --card-bg, --input-bg, --input-border, --muted-foreground and confirm all surfaces retint in unison.
  • Recovery link visible by default on the OTP step; injecting :root { --recovery-link-display: none; } from a client's branding.css hides it.
  • OTP step renders correctly (card still centered, input/button styling consistent).
  • Terms/Privacy links underline correctly and open https://certified.app/terms and https://certified.app/privacy in new tabs.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Six-digit segmented one-time-code entry with keyboard navigation, paste handling, and auto-submission.
    • New theme preset option for login page customization.
    • Account recovery link now displayed during OTP entry by default.
  • Style

    • Redesigned login page with centered card layout and updated color scheme.
    • Refreshed input and button styling across authentication interface.
    • Updated submit button copy ("Continue" in email mode).
  • Tests

    • Updated end-to-end tests to cover new segmented OTP entry behavior.

@vercel

vercel Bot commented Apr 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
epds-demo Ready Ready Preview, Comment Apr 29, 2026 10:02am

Request Review

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces a theme-driven CSS system for the login page with redesigned card and typography, replaces branding.background_color with theme variables, swaps the logo fallback to a Certified brandmark with "Powered by" text, restructures OTP entry from a single input to per-digit segmented boxes with auto-submission, updates email/handle mode submit labels, adds optional terms consent block shown only on email step, and extends the demo theme system with border properties and a new cream preset.

Changes

Cohort / File(s) Summary
Login Page Redesign
packages/auth-service/src/routes/login-page.ts
Introduces theme-driven CSS variables (--page-bg, --card-bg, --focus-border, etc.), replaces branding.background_color usage, adds Certified brandmark SVG with "Powered by" text using currentColor tinting, refactors OTP entry from single #code text input to per-digit .otp-box elements with input/paste handling, arrow/backspace navigation, automatic code aggregation, and auto-submission on final digit, adds dynamic #heading and optional terms consent block (email step only), changes recovery-backup-email link to render by default with opt-out via --recovery-link-display: none CSS variable, updates submit button labels to "Continue" in email mode.
Demo Theme System
packages/demo/src/lib/theme.ts, packages/demo/src/app/components/PageShell.tsx, packages/demo/.env.example
Extends PageTheme interface with cardBorder and secondaryBorder properties, populates these in existing ocean and amber presets, adds new cream theme preset with distinct page palette and extensive injectedCss overrides for consent and login-page styling, updates .env.example documentation to reflect expanded preset list and new default theme behavior.
E2E OTP Refactoring
e2e/support/otp.ts, e2e/step-definitions/auth.steps.ts, e2e/step-definitions/client-branding.steps.ts, e2e/step-definitions/consent.steps.ts, e2e/step-definitions/account-recovery.steps.ts, e2e/support/flows.ts
Introduces shared fillOtp(page, otp) helper for segmented OTP entry targeting .otp-box elements with auto-submission, replaces direct #code filling and verify-button clicking across multiple step definitions, updates OTP length/character set derivation to read .otp-box count and first box's inputmode, changes account recovery navigation from UI-driven (clicking #recovery-link) to direct page.goto(/auth/recover), updates asserted login-page background color from rgb(248, 249, 250) to rgb(232, 232, 232).
Release Notes
.changeset/login-page-theming.md
Documents refreshed sign-in page design with centered white card on muted background, updated input/button styling, six-segment OTP entry with enhanced keyboard/paste and auto-submit behavior, new theming via CSS custom properties for client-driven overrides, deprecation of background_color metadata in favor of --page-bg, and default visibility of recovery-backup-email link with opt-out CSS variable.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • aspiers

Poem

🐰 A segmented code so fine, in boxes dancing in a line,
Cream and ocean, amber too, theming takes a brand-new hue,
OTP flows with auto-grace, Certified adorns the space,
One page redesigned with care, rabbit hops with flair!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main changes: restyling the login page to a white card layout with Certified branding.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch restyle-login-page

Comment @coderabbitai help to get the list of available commands and usage tips.

@changeset-bot

changeset-bot Bot commented Apr 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: dd5faf4

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@railway-app

railway-app Bot commented Apr 22, 2026

Copy link
Copy Markdown

🚅 Deployed to the ePDS-pr-110 environment in ePDS

Service Status Web Updated (UTC)
@certified-app/pds-core ✅ Success (View Logs) Web Apr 29, 2026 at 4:15 pm
@certified-app/auth-service ✅ Success (View Logs) Web Apr 29, 2026 at 3:07 pm
@certified-app/demo ✅ Success (View Logs) Web Apr 28, 2026 at 11:51 pm
@certified-app/demo untrusted ✅ Success (View Logs) Web Apr 28, 2026 at 11:51 pm

@coveralls-official

coveralls-official Bot commented Apr 22, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 25102591635

Coverage increased (+0.1%) to 48.544%

Details

  • Coverage increased (+0.1%) from the base build.
  • Patch coverage: 40 of 40 lines across 5 files are fully covered (100%).
  • 1 coverage regression across 1 file.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
packages/pds-core/src/lib/client-css-injection.ts 1 74.78%

Coverage Stats

Coverage Status
Relevant Lines: 2622
Covered Lines: 1282
Line Coverage: 48.89%
Relevant Branches: 1603
Covered Branches: 769
Branch Coverage: 47.97%
Branches in Coverage %: Yes
Coverage Strength: 5.0 hits per line

💛 - Coveralls

@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 21:12 Destroyed
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 21:25 Destroyed
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 21:40 Destroyed
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 21:47 Destroyed
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 21:53 Destroyed
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 21:58 Destroyed
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 22:00 Destroyed
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 22:04 Destroyed
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 22:05 Destroyed
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 22:09 Destroyed
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 22:17 Destroyed
@blacksmith-sh

This comment has been minimized.

@blacksmith-sh

This comment has been minimized.

s-adamantine and others added 5 commits April 29, 2026 00:21
White card on muted grey, pill buttons, segmented 6-box OTP input
with paste/arrow/backspace/auto-submit, "Powered by Certified"
footer, Certified brandmark as fallback top logo.

Adds an ATProto/Bluesky toggle that swaps the email form into
handle-entry mode and submits to <client-origin>/api/oauth/login?handle=X
— the client then resolves the handle to its PDS and starts a fresh
PAR. Off-PDS handles end up on their own PDS's auth screen.

Removes the "Recover with backup email" link from the page.
Hardcodes the Terms/Privacy notice to Certified's terms (the auth
server is always Certified's regardless of which client started the
flow), with both links underlined and pointing at certified.app.

Hides the Terms/Privacy notice on the OTP step. h1 reads "Sign in"
on the email step, "Enter your code" on the OTP step.

Exposes CSS custom properties for trusted-client theming:
  --page-bg, --card-bg, --input-bg, --input-border,
  --muted-foreground, --card-border, --btn-secondary-border,
  --focus-border
Defaults stay neutral grey; trusted clients can override any of
these via injected branding.css.

Inlines the Certified wordmark SVG so the "Powered by" footer can
tint via currentColor.
The login-page restyle replaced the single visible OTP input with a
hidden #code field plus six .otp-box slots. page.fill('#code', ...)
resolves the locator but Playwright refuses to fill a type=hidden
input, timing out every OTP-step scenario.

Add a fillOtp(page, otp) helper that clears any stale digits, then
types one per box. The page's input handler auto-submits on the last
digit, so the explicit verify-button click is dropped at every call
site. The 'incorrect OTP {int} times' loop registers waitForResponse
before fillOtp since the submit fires from inside the helper.

Also reworks buildIncorrectOtpCode's DOM-inference fallback: length
now comes from .otp-box count and charset from a single box's
inputmode, since the hidden #code carries no maxlength / pattern.

The recovery flow's #code input is unchanged and untouched here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restores the "Recover with backup email" link on the OTP step,
shown by default, controlled by the new --recovery-link-display
CSS var so clients can hide it via branding.css.

E2E: recovery flows navigate to /auth/recover directly (more
robust than depending on the link's visibility, which is now
client-configurable). DEFAULT_LOGIN_BG_RGB updated to match the
new --page-bg default (#E8E8E8).

Adds a changeset documenting the login-page CSS theming surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Validate brand_color as a hex literal before interpolating into
  <style>; escapeHtml() doesn't sanitise CSS context, so a value
  containing `;` or `}` could break out of the declaration.
- OTP slot placeholder/aria-label now switch to "A"/"Character" when
  otpCharset === 'alphanumeric', matching the configured charset.
- Clarify EPDS_CLIENT_THEME unset behaviour in packages/demo/.env.example
  (getTheme() returns null; demo uses its own light look; no branding
  CSS shipped → auth-service renders default Certified styling).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The atproto-login-button feature was authored before PR #110's restyle
changed the email label from "Email address" to "Enter your email
address". Update the assertion to match the new copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aspiers

aspiers commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Autonomous loop done — ready for review/merge

CI: 15/15 green (e2e finished in 8m8s).

Branch tip: 60aed03. Eight commits since origin/main:

60aed03 feat(preview): add ATProto/Bluesky login-button toggle on /preview
ac1e14c feat(pds-core): inject default Certified branding CSS
eb5ff20 feat(auth-service): drive ToS/privacy links from PDS env vars
0e5bf82 fix(e2e): expect "Enter your email address" label after restyle
b83a387 fix(auth-service): address PR #110 review feedback
f4f1040 feat(auth-service): recovery link + e2e fixes after restyle
5fc9721 test(e2e): drive segmented OTP boxes instead of hidden #code input
77d136f feat(auth-service): restyle login page + ATProto handle sign-in toggle

Changes since last user touch:

  • Env-var-driven Terms/Privacy links on the login page (PDS_TERMS_OF_SERVICE_URL, PDS_PRIVACY_POLICY_URL, optional PDS_LEGAL_ENTITY_NAME for the possessive). Strict mode: line is omitted entirely if either URL is unset. Set on Railway pr-base, ePDS-pr-110, and test envs to recreate the previous hardcoded copy.
  • Default Certified-style CSS injected into upstream @atproto/oauth-provider-ui consent + chooser pages so unbranded ePDS deployments render coherently with the auth-service login page (muted page bg, light card surface with thin border + radius + soft shadow, white-with-thin-border account rows / secondary buttons, dark primary button). Trusted-client branding.css continues to override via cascade order.
  • "Powered by Certified" footer on the login page is now a link to https://certified.app/ with no underline (target=_blank, rel=noopener noreferrer).
  • /preview index gained an "ATProto/Bluesky login button" checkbox next to "Login — email step"; ?atproto=1 synthesises an epds_handle_login_url so the button is reachable in preview without editing real client metadata.
  • Docs/scripts audit applied: docs/configuration.md documents the three new legal-URL env vars; docs/tutorial.md updated to drop background_color and document the CSS-var override surface; packages/pds-core/.env.example lists the legal URLs as [shared]; scripts/setup.sh propagates all three new vars from the top-level .env into per-package files.

Unresolved review threads: 0.

Ready for review/merge.

aspiers and others added 3 commits April 29, 2026 09:59
Replace the hardcoded https://certified.app/terms and /privacy URLs in
the login page with values read from PDS_TERMS_OF_SERVICE_URL /
PDS_PRIVACY_POLICY_URL — the same env vars upstream PDS already
consumes, so deployments configure them once. The terms line is
omitted entirely when either var is missing (strict; avoids dead
half-links).

Optional PDS_LEGAL_ENTITY_NAME controls the possessive in the line:
"By signing in, you agree to <name>'s Terms of Use and Privacy Policy."
When unset, falls back to "By signing in, you agree to the Terms of
Use and Privacy Policy."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upstream @atproto/oauth-provider-ui renders the consent + chooser
pages with purple-on-white defaults that don't match the rest of an
ePDS deployment. Inject a neutral default stylesheet (muted grey page
bg, light card surface, dark primary button) into every
/oauth/authorize response so unbranded deployments render coherently
with auth-service's restyled login page.

Trusted-client branding.css is now layered on top of the default —
cascade order means client styles still win on overlapping selectors,
no client opt-in or migration needed.

CSP: each <style> block is hashed separately and appended to
style-src.

Also covers the /preview/consent and /preview/chooser routes so
client-app developers iterate against the production look.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Justifies the absence of any preview-only override that synthesises
`epds_handle_login_url`: the /preview/login button visibility is
identical to the real /oauth/authorize flow, so iterating against
real client metadata is the only way to see the button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Comment thread packages/auth-service/src/routes/login-page.ts
updateHiddenCode();
if (box.value && idx < otpBoxes.length - 1) otpBoxes[idx + 1].focus();
if (hiddenCode.value.length === otpBoxes.length) {
document.getElementById('form-verify-otp').requestSubmit();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This auto-submit caused a few bugs, one of which is fixed by #134.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants