diff --git a/src/components/Common/Header.tsx b/src/components/Common/Header.tsx index 881679a4..9e1ab733 100644 --- a/src/components/Common/Header.tsx +++ b/src/components/Common/Header.tsx @@ -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' @@ -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 } = @@ -194,12 +195,7 @@ export const Header: React.FC = () => { > settings - + ) @@ -584,12 +580,10 @@ export const Header: React.FC = () => { ) : ( - + )} diff --git a/src/components/Common/LichessSignInButton.tsx b/src/components/Common/LichessSignInButton.tsx new file mode 100644 index 00000000..ee181b03 --- /dev/null +++ b/src/components/Common/LichessSignInButton.tsx @@ -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 ( + + ) +} diff --git a/src/components/Play/PlayControls.test.tsx b/src/components/Play/PlayControls.test.tsx index ce7bc41a..c53381fc 100644 --- a/src/components/Play/PlayControls.test.tsx +++ b/src/components/Play/PlayControls.test.tsx @@ -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. @@ -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( + + + , + ) + + 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() + }) +}) diff --git a/src/components/Play/PlayControls.tsx b/src/components/Play/PlayControls.tsx index d30239f5..e09cf738 100644 --- a/src/components/Play/PlayControls.tsx +++ b/src/components/Play/PlayControls.tsx @@ -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' @@ -94,6 +96,7 @@ export const PlayControls: React.FC = ({ 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. @@ -144,6 +147,23 @@ export const PlayControls: React.FC = ({ {returnToLabel} ) : 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 ? ( +
+

+ Sign in to keep your rating and games on any device, and to + appear on the leaderboard. +

+ +
+ ) : null} ) : ( <> diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 4ee0313c..1786cf47 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -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 } @@ -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() diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index ec63979f..7e5537b9 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -587,6 +587,7 @@ export type SignInSource = | 'header' | 'header_menu' | 'error_page' + | 'game_over' | 'analysis_lichess_tab' | 'auth_prompt'