Align Google/Microsoft SSO buttons with brand guidelines (#79)
Unit Tests / test (pull_request) Successful in 10s

Add official logos and approved dark-theme button styling so Sign In /
Sign Up SSO controls match Google and Microsoft identity UX requirements.
This commit is contained in:
2026-08-01 15:03:26 -05:00
parent 70dfa85d22
commit 6e80c24132
3 changed files with 189 additions and 31 deletions
@@ -0,0 +1,46 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SsoButtons from './SsoButtons';
import { startOAuth } from '../../auth/sso';
jest.mock('../../auth/sso', () => ({
startOAuth: jest.fn(),
}));
describe('SsoButtons', () => {
beforeEach(() => {
jest.mocked(startOAuth).mockReset();
});
it('renders nothing when no providers enabled', () => {
const { container } = render(
<SsoButtons intent="login" providers={{ google: false, microsoft: false }} />
);
expect(container).toBeEmptyDOMElement();
});
it('uses brand-compliant labels and logos for login', () => {
render(<SsoButtons intent="login" providers={{ google: true, microsoft: true }} />);
expect(screen.getByRole('button', { name: 'Continue with Google' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Sign in with Microsoft' })).toBeInTheDocument();
expect(document.querySelectorAll('svg').length).toBeGreaterThanOrEqual(2);
});
it('uses Sign up with Google for signup intent', () => {
render(<SsoButtons intent="signup" providers={{ google: true }} />);
expect(screen.getByRole('button', { name: 'Sign up with Google' })).toBeInTheDocument();
});
it('starts OAuth for the selected provider', async () => {
const user = userEvent.setup();
render(<SsoButtons intent="login" providers={{ google: true, microsoft: true }} />);
await user.click(screen.getByRole('button', { name: 'Continue with Google' }));
expect(startOAuth).toHaveBeenCalledWith('google', 'login');
await user.click(screen.getByRole('button', { name: 'Sign in with Microsoft' }));
expect(startOAuth).toHaveBeenCalledWith('microsoft', 'login');
});
});
@@ -20,21 +20,32 @@ const Divider = styled.div`
} }
`; `;
const SsoButton = styled.button` const ButtonStack = styled.div`
display: flex;
flex-direction: column;
width: 100%; width: 100%;
background: rgba(255, 255, 255, 0.06); gap: 0.6rem;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 0.5rem;
color: #fff;
padding: 0.85rem 1rem;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease, transform 0.2s ease;
margin-top: 0.6rem; margin-top: 0.6rem;
`;
&:hover { /** Shared layout for IdP buttons — equal size / visual weight. */
background: rgba(255, 255, 255, 0.12); const SsoButtonBase = styled.button`
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
width: 100%;
min-height: 2.75rem;
border-radius: 0.25rem;
padding: 0.65rem 1rem;
font-size: 0.875rem;
font-weight: 500;
font-family: 'Roboto', 'Google Sans', system-ui, sans-serif;
letter-spacing: 0.01em;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
&:hover:not(:disabled) {
transform: translateY(-1px); transform: translateY(-1px);
} }
@@ -45,6 +56,93 @@ const SsoButton = styled.button`
} }
`; `;
/**
* Google dark-theme button per
* https://developers.google.com/identity/branding-guidelines
* Fill #131314, stroke #8E918F, text #E3E3E3; multicolor G on white.
*/
const GoogleButton = styled(SsoButtonBase)`
background: #131314;
border: 1px solid #8e918f;
color: #e3e3e3;
&:hover:not(:disabled) {
background: #1e1f20;
border-color: #a8aba9;
}
`;
/**
* Microsoft dark-theme button — logo + "Sign in with Microsoft"
* https://learn.microsoft.com/en-us/entra/identity-platform/howto-add-branding-in-apps
*/
const MicrosoftButton = styled(SsoButtonBase)`
background: #2f2f2f;
border: 1px solid transparent;
color: #ffffff;
&:hover:not(:disabled) {
background: #3b3b3b;
}
`;
const GoogleLogoBadge = styled.span`
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 1.25rem;
height: 1.25rem;
background: #ffffff;
border-radius: 0.125rem;
`;
const LogoMark = styled.span`
display: inline-flex;
flex-shrink: 0;
width: 1.25rem;
height: 1.25rem;
svg {
width: 100%;
height: 100%;
display: block;
}
`;
/** Official multicolor Google "G" (standard color Super G). */
const GoogleGIcon = (): JSX.Element => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" aria-hidden="true" focusable="false">
<path
fill="#EA4335"
d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"
/>
<path
fill="#4285F4"
d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"
/>
<path
fill="#FBBC05"
d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"
/>
<path
fill="#34A853"
d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"
/>
<path fill="none" d="M0 0h48v48H0z" />
</svg>
);
/** Official Microsoft four-square logo — do not recolor. */
const MicrosoftLogoIcon = (): JSX.Element => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21" aria-hidden="true" focusable="false">
<rect x="1" y="1" width="9" height="9" fill="#f25022" />
<rect x="11" y="1" width="9" height="9" fill="#7fba00" />
<rect x="1" y="11" width="9" height="9" fill="#00a4ef" />
<rect x="11" y="11" width="9" height="9" fill="#ffb900" />
</svg>
);
export type OAuthProviderFlags = { export type OAuthProviderFlags = {
google?: boolean; google?: boolean;
microsoft?: boolean; microsoft?: boolean;
@@ -56,6 +154,9 @@ type SsoButtonsProps = {
disabled?: boolean; disabled?: boolean;
}; };
const googleLabel = (intent: 'login' | 'signup'): string =>
intent === 'signup' ? 'Sign up with Google' : 'Continue with Google';
const SsoButtons = ({ intent, providers, disabled = false }: SsoButtonsProps): JSX.Element | null => { const SsoButtons = ({ intent, providers, disabled = false }: SsoButtonsProps): JSX.Element | null => {
const google = Boolean(providers.google); const google = Boolean(providers.google);
const microsoft = Boolean(providers.microsoft); const microsoft = Boolean(providers.microsoft);
@@ -66,24 +167,34 @@ const SsoButtons = ({ intent, providers, disabled = false }: SsoButtonsProps): J
return ( return (
<> <>
<Divider>or continue with</Divider> <Divider>or continue with</Divider>
<ButtonStack>
{google && ( {google && (
<SsoButton <GoogleButton
type="button" type="button"
disabled={disabled} disabled={disabled}
onClick={() => startOAuth('google', intent)} onClick={() => startOAuth('google', intent)}
> >
Continue with Google <GoogleLogoBadge>
</SsoButton> <LogoMark>
<GoogleGIcon />
</LogoMark>
</GoogleLogoBadge>
{googleLabel(intent)}
</GoogleButton>
)} )}
{microsoft && ( {microsoft && (
<SsoButton <MicrosoftButton
type="button" type="button"
disabled={disabled} disabled={disabled}
onClick={() => startOAuth('microsoft', intent)} onClick={() => startOAuth('microsoft', intent)}
> >
Continue with Microsoft <LogoMark>
</SsoButton> <MicrosoftLogoIcon />
</LogoMark>
Sign in with Microsoft
</MicrosoftButton>
)} )}
</ButtonStack>
</> </>
); );
}; };
@@ -85,7 +85,7 @@ describe('SignIn', () => {
renderSignIn(); renderSignIn();
expect(await screen.findByRole('button', { name: 'Continue with Google' })).toBeInTheDocument(); expect(await screen.findByRole('button', { name: 'Continue with Google' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Continue with Microsoft' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Sign in with Microsoft' })).toBeInTheDocument();
}); });
it('hides SSO buttons when oauth not configured', async () => { it('hides SSO buttons when oauth not configured', async () => {
@@ -99,6 +99,7 @@ describe('SignIn', () => {
await waitFor(() => { await waitFor(() => {
expect(screen.queryByRole('button', { name: 'Continue with Google' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Continue with Google' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Sign in with Microsoft' })).not.toBeInTheDocument();
}); });
}); });