Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 7 additions & 13 deletions src/components/Common/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import Link from 'next/link'
import { LichessSignInButton } from 'src/components/Common/LichessSignInButton'
import Image from 'next/image'
import { PlayType } from 'src/types'
import { useRouter } from 'next/router'
Expand Down Expand Up @@ -66,7 +67,7 @@ export const Header: React.FC = () => {
const [showProfileDropdown, setShowProfileDropdown] = useState(false)
const { isMobile, width } = useContext(WindowSizeContext)

const { user, connectLichess, logout } = useContext(AuthContext)
const { user, logout } = useContext(AuthContext)

// Get leaderboard status for the logged in user
const { status: leaderboardStatus, loading: leaderboardLoading } =
Expand Down Expand Up @@ -194,12 +195,7 @@ export const Header: React.FC = () => {
>
<span className="material-symbols-outlined text-2xl">settings</span>
</Link>
<button
onClick={() => connectLichess('header')}
className="px-2 py-1 text-sm tracking-wide text-primary/80 transition-all duration-200 hover:text-primary"
>
Sign in
</button>
<LichessSignInButton source="header" />
</>
)

Expand Down Expand Up @@ -584,12 +580,10 @@ export const Header: React.FC = () => {
</span>
</div>
) : (
<button
onClick={() => connectLichess('header_menu')}
className="uppercase"
>
Sign in
</button>
<LichessSignInButton
source="header_menu"
className="px-4 py-2 text-base"
/>
)}
</div>
</div>
Expand Down
30 changes: 30 additions & 0 deletions src/components/Common/LichessSignInButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useContext } from 'react'
import { AuthContext } from 'src/contexts/AuthContext'
import { SignInSource } from 'src/lib/analytics'

interface Props {
source: SignInSource
// A path on this site to come back to; the current page without it.
returnTo?: string
className?: string
}

/** "Sign in with Lichess", with Lichess's logo. */
export const LichessSignInButton = ({ source, returnTo, className }: Props) => {
const { connectLichess } = useContext(AuthContext)
return (
<button
type="button"
onClick={() => connectLichess(source, returnTo)}
className={`flex items-center justify-center gap-2 rounded-md border border-glass-border bg-white/5 px-3 py-1.5 text-sm font-medium text-white/90 transition-colors duration-200 hover:border-white/20 hover:bg-white/10 hover:text-white ${className ?? ''}`}
>
<img
src="/assets/icons/lichess.svg"
className="h-4 w-4 shrink-0"
alt=""
aria-hidden
/>
Sign in with Lichess
</button>
)
}
39 changes: 39 additions & 0 deletions src/components/Play/PlayControls.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react'
import { Chess } from 'chess.ts'
import { GameTree } from 'src/types'
import { PlayControls } from './PlayControls'
import { AuthContext } from 'src/contexts/AuthContext'

// The components index pulls in modules Jest can't parse (react-markdown);
// only the resign dialog is used here.
Expand Down Expand Up @@ -64,3 +65,41 @@ describe('PlayControls practice buttons', () => {
expect(takeBack).not.toHaveBeenCalled()
})
})

describe('PlayControls sign-in prompt after a game', () => {
const connectLichess = jest.fn()
const renderOver = (
user: { clientId: string; displayName: string; lichessId?: string } | null,
gameOver = true,
) =>
render(
<AuthContext.Provider value={{ user, connectLichess, logout: jest.fn() }}>
<PlayControls
game={game}
playerActive={false}
gameOver={gameOver}
playAgain={jest.fn()}
/>
</AuthContext.Provider>,
)

beforeEach(() => connectLichess.mockClear())

it('asks a guest to sign in, coming back to /play', () => {
renderOver({ clientId: 'c', displayName: '' })
fireEvent.click(screen.getByText('Sign in with Lichess'))
expect(connectLichess).toHaveBeenCalledWith('game_over', '/play')
})

it('does not ask a signed-in player', () => {
renderOver({ clientId: 'c', displayName: 'alice', lichessId: 'alice' })
expect(screen.queryByText('Sign in with Lichess')).toBeNull()
})

it('does not ask before the account has loaded, or during a game', () => {
renderOver(null)
expect(screen.queryByText('Sign in with Lichess')).toBeNull()
renderOver({ clientId: 'c', displayName: '' }, false)
expect(screen.queryByText('Sign in with Lichess')).toBeNull()
})
})
22 changes: 21 additions & 1 deletion src/components/Play/PlayControls.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useEffect, useState } from 'react'
import { useContext, useEffect, useState } from 'react'

import { PlayedGame } from 'src/types'
import { ResignationConfirmModal } from 'src/components'
import { AuthContext } from 'src/contexts/AuthContext'
import { LichessSignInButton } from 'src/components/Common/LichessSignInButton'

type PracticeAction = 'takeBack' | 'addTime'

Expand Down Expand Up @@ -94,6 +96,7 @@ export const PlayControls: React.FC<Props> = ({
takeBack,
addTime,
}: Props) => {
const { user } = useContext(AuthContext)
const [showResignConfirm, setShowResignConfirm] = useState(false)
// Practice actions ask first, so a stray tap doesn't undo a move: the
// first tap turns the button into a confirm button, the second acts.
Expand Down Expand Up @@ -144,6 +147,23 @@ export const PlayControls: React.FC<Props> = ({
{returnToLabel}
</button>
) : null}
{/* Guests only (`user` is null until the account has loaded). A
first sign-in keeps the guest's rating and games, and only
signed-in players are on the leaderboard. It comes back to
/play: this page, without its game, would start a new one. */}
{user && !user.lichessId ? (
<div className="mt-1 flex flex-col gap-2 border-t border-glass-border pt-3">
<p className="text-center text-xs leading-relaxed text-white/70">
Sign in to keep your rating and games on any device, and to
appear on the leaderboard.
</p>
<LichessSignInButton
source="game_over"
returnTo="/play"
className="whitespace-nowrap px-2 md:text-[13px]"
/>
</div>
) : null}
</div>
) : (
<>
Expand Down
40 changes: 24 additions & 16 deletions src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import { registerProperties } from 'src/lib/posthogClient'

interface IAuthContext {
user: User | null
connectLichess: (source: SignInSource) => void
/**
* Goes to Lichess's sign-in, then back to `returnTo` (a path on this site)
* or, without it, the current page.
*/
connectLichess: (source: SignInSource, returnTo?: string) => void
logout: () => Promise<void>
}

Expand Down Expand Up @@ -69,22 +73,26 @@ export const AuthContextProvider: React.FC<{ children: ReactNode }> = ({
getAndSetAccount()
}, [])

const connectLichess = useCallback((source: SignInSource) => {
trackLichessConnectionInitiated(source)
try {
window.sessionStorage.setItem(
SIGN_IN_KEY,
JSON.stringify({ source, at: Date.now() }),
const connectLichess = useCallback(
(source: SignInSource, returnTo?: string) => {
trackLichessConnectionInitiated(source)
try {
window.sessionStorage.setItem(
SIGN_IN_KEY,
JSON.stringify({ source, at: Date.now() }),
)
} catch {
// Not measured, but the sign-in still works.
}
const back = returnTo
? new URL(returnTo, window.location.origin).toString()
: window.location.toString()
window.location.assign(
connectLichessUrl + '?redirect_url=' + encodeURIComponent(back),
)
} catch {
// Not measured, but the sign-in still works.
}
window.location.assign(
connectLichessUrl +
'?redirect_url=' +
encodeURIComponent(window.location.toString()),
)
}, [])
},
[],
)

const logout = useCallback(async () => {
trackLoggedOut()
Expand Down
1 change: 1 addition & 0 deletions src/lib/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,7 @@ export type SignInSource =
| 'header'
| 'header_menu'
| 'error_page'
| 'game_over'
| 'analysis_lichess_tab'
| 'auth_prompt'

Expand Down
Loading