Skip to content

Staging to Prod.#41

Merged
swoosh1337 merged 21 commits into
mainfrom
staging
Jun 5, 2026
Merged

Staging to Prod.#41
swoosh1337 merged 21 commits into
mainfrom
staging

Conversation

@swoosh1337

@swoosh1337 swoosh1337 commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Account-restore flow for pending-deletion sign-ins with restore UI and Google credential support.
    • Penalties experience: dedicated HUDs, overlays, counters and penalty flows.
    • Friend-invite parsing/validation, invite UX improvements, and image preloading for smoother phases.
    • Daily Challenges: “Get ready” intro and session prefetching.
  • Bug Fixes

    • Settings back now navigates to Play.
    • More robust session refresh and socket auth recovery handling.
  • Improvements

    • Higher bar cap, boosted bar behaviors, refined penalty timing.
    • Typography and locale persistence tweaks; lowered kickoff BGM volume.
  • Tests

    • Expanded coverage across auth, friend lobbies, penalties, daily challenges, and match flows.

@vercel

vercel Bot commented Jun 2, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
quizball-web Ready Ready Preview, Comment Jun 5, 2026 6:09pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

Adds penalty-mode gameplay and UI/timing, raises bar-battle cap to 20, introduces a correlation-aware lobby command machine and invite-code utilities, implements pending-deletion restore and richer refresh semantics in auth flows, updates UI components and tests, and includes supporting infra (i18n, fonts, sounds, preload hook, test storage shim).

Changes

Penalties, Lobby, and Auth Restore

Layer / File(s) Summary
All changes (semantic review order)
src/features/possession/**, src/features/friend/**, src/lib/auth/**, src/components/auth/**, src/lib/friend/**, src/lib/queries/**, src/contexts/**, src/test/**, src/messages/*, src/app/**, src/lib/sounds/**
Penalty-mode: MAX_BARS increased to 20; added keeper-shield offsets, charge-impact keyframes, penalty-aware view-model/layout and HUD/overlay wiring, plus tests. Friend flow: added useLobbyCommandMachine, normalized invite extraction, correlation-aware lobby emits, and updated join/create/leave UIs/tests. Auth: refreshSession single-flight/terminal handling, pending-deletion restore endpoints/flows, OAuth callback and welcome modal/restore wiring, and many auth-service/store/api client updates. Supporting: invite-code utilities, usePreloadImages, locale persistence gating, test storage shim, Poppins font import, sound file/volume changes, daily-challenge intro/prefetch, translation updates, and many unit/integration tests.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

"I’m a rabbit on the pitch, ears aflutter in the night,
Twenty bars now shimmer, keeper shields shine bright.
Lobbies heed the command machine, invites tidy and neat,
Lost accounts hop home again — restored and re-complete.
Poppins hums as tests run green — a tiny victory beat. 🐇"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch staging

@swoosh1337 swoosh1337 changed the title Staging Staging to Prod. Jun 2, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/lib/geo/countries.ts (1)

534-540: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Los Angeles coordinates look closer to San Bernardino than downtown LA.

In src/lib/geo/countries.ts, the "los angeles" point (lon: -117.65, lat: 34.15) is ~55.7 km from downtown LA (34.0522, -118.2437) and ~33.5 km from San Bernardino (34.1083, -117.2898). If the intent is to mark Los Angeles’s city center, update these coordinates. https://www.google.com/maps?q=34.15,-117.65

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/geo/countries.ts` around lines 534 - 540, The "los angeles" entry in
the countries.ts dataset uses coordinates (lon: -117.65, lat: 34.15) which are
nearer San Bernardino; update the "los angeles" object's lon/lat to downtown LA
coordinates (approx lon: -118.2437, lat: 34.0522) in src/lib/geo/countries.ts
for the "los angeles" key and keep the city/country/flag fields unchanged;
verify the updated point is now close to LA city center and adjust slightly if
you prefer an alternate official center.
src/types/api.generated.ts (1)

901-908: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing secret query param in callback type contract.

Line 897-898 documents callback auth via secret query param, and Line 925 defines a 401 for invalid secret, but Line 901-908 omits secret from typed query params. This creates a client/server contract mismatch for /api/v1/auth/sms/smsoffice-callback.

Suggested type fix
            parameters: {
                query: {
+                    secret: string;
                    reference: string;
                    status: string;
                    reason?: string;
                    destination: string;
                    timestamp?: string;
                    operator?: string;
                };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types/api.generated.ts` around lines 901 - 908, The generated callback
query type for /api/v1/auth/sms/smsoffice-callback is missing the required
`secret` param, causing a client/server contract mismatch; update the `query`
shape in the type (the object containing `reference`, `status`, `reason?`,
`destination`, `timestamp?`, `operator?`) to include `secret: string` (required,
not optional) so the typed contract matches the documented/authenticated
callback and the 401 handling.
🧹 Nitpick comments (8)
src/features/welcome/WelcomeScreen.tsx (1)

185-193: 💤 Low value

Consider removing the unnecessary fallback.

The variant prop uses authNoticeModal ?? 'check-email', but the modal only renders when authNoticeModal !== null. The fallback will never be used.

♻️ Proposed cleanup
       <WelcomeAuthNoticeModal
         open={authNoticeModal !== null}
-        variant={authNoticeModal ?? 'check-email'}
+        variant={authNoticeModal!}
         onClose={handleCloseAuthNoticeModal}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/welcome/WelcomeScreen.tsx` around lines 185 - 193, The variant
prop fallback is redundant because WelcomeAuthNoticeModal is only rendered when
authNoticeModal !== null; remove the nullish coalescing fallback and pass
authNoticeModal directly to variant (i.e., use variant={authNoticeModal}) and
keep existing handlers (handleCloseAuthNoticeModal, handleNoticeModalGoToSignIn,
handleRestorePendingDeletion) and restore props (restoreSubmitting,
restoreError) unchanged so behavior is preserved.
src/lib/config.ts (1)

4-12: ⚖️ Poor tradeoff

Consider environment-specific configuration strategy.

Hardcoding PHONE_AUTH_ENABLED = true removes the ability to toggle this feature via environment variables. While the comment indicates this is intentional for staging testing, consider whether you want to:

  • Keep the env var for production control, or
  • Use a different approach to gate the feature per environment
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/config.ts` around lines 4 - 12, Replace the hardcoded export const
PHONE_AUTH_ENABLED = true with an environment-driven boolean: read a new env var
(e.g., process.env.PHONE_AUTH_ENABLED) and parse it to a boolean with a sensible
default (true for staging or configurable via another env like NODE_ENV); update
the PHONE_AUTH_ENABLED constant to use that parsed value so the feature can be
toggled per environment while preserving current default behavior. Ensure
parsing handles "1"/"true"/"false"/"0" case-insensitively and falls back to the
existing default when unset.
src/components/auth/__tests__/WelcomeScreen.test.tsx (1)

328-328: 💤 Low value

Consider relaxing the assertion to avoid testing implementation details.

The test explicitly expects undefined as the 4th argument, but this tests how the function is called rather than what it does. If the implementation changes to omit the argument entirely (which is semantically identical), this test would break.

Consider using expect.anything() or omitting the check for optional parameters:

-    expect(socialLoginWithIdTokenMock).toHaveBeenCalledWith('google', 'tok', 'nonce', undefined);
+    expect(socialLoginWithIdTokenMock).toHaveBeenCalledWith('google', 'tok', 'nonce', expect.anything());

Or verify just the required parameters and use toHaveBeenCalledTimes for call count.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/auth/__tests__/WelcomeScreen.test.tsx` at line 328, The
assertion in WelcomeScreen.test.tsx is too strict by expecting an explicit
fourth argument of undefined when calling socialLoginWithIdTokenMock; update the
test to only assert the meaningful parameters (e.g., 'google', 'tok', 'nonce')
and relax or omit the check for the optional fourth argument—either drop the
fourth-arg check and rely on toHaveBeenCalledTimes or use a matcher like
expect.anything()/a wildcard for the fourth parameter when calling
socialLoginWithIdTokenMock so changes that omit the trailing undefined won't
break the test.
src/lib/api/__tests__/client.test.ts (1)

32-33: 💤 Low value

Remove the redundant/no-op setup line.

Line 32 sets mockRejectedValue(undefined) via optional chaining (the method always exists on a vi.fn) and is immediately overwritten by line 33. It has no effect and is misleading in the test.

♻️ Proposed cleanup
-    mockedRequest.mockRejectedValue?.(undefined);
     mockedRequest.mockRejectedValue(new ApiError("Request failed", 401, null));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/api/__tests__/client.test.ts` around lines 32 - 33, Remove the
redundant no-op mock setup: delete the line calling
mockedRequest.mockRejectedValue?.(undefined) since mockRejectedValue always
exists on the vi.fn and the next line mockedRequest.mockRejectedValue(new
ApiError("Request failed", 401, null)) immediately overwrites it; update the
test in client.test.ts by keeping only the meaningful
mockedRequest.mockRejectedValue(new ApiError(...)) call (refer to
mockedRequest.mockRejectedValue and ApiError in the test).
src/features/friend/hooks/useLobbyCommandMachine.ts (2)

208-208: 💤 Low value

Clarify magic numbers in backoff formula.

The backoff delay 220 + attempt * 140 uses unexplained constants. Consider extracting these as named constants with comments describing the rationale (e.g., minimum backoff, increment per retry).

♻️ Suggested refactor
+const RETRY_BASE_DELAY_MS = 220;
+const RETRY_INCREMENT_MS = 140;
+
 for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
   const result = await send(correlationId);
   if (sequenceRef.current !== sequence) return null;
   
   // ... logging ...
   
   if (result.ok) {
     // ... success handling ...
   }
   
   if (result.retryable && attempt < maxRetries) {
-    await wait(220 + attempt * 140);
+    await wait(RETRY_BASE_DELAY_MS + attempt * RETRY_INCREMENT_MS);
     continue;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/friend/hooks/useLobbyCommandMachine.ts` at line 208, Extract the
magic numbers in the backoff formula into named constants (e.g., MIN_BACKOFF_MS
and BACKOFF_INCREMENT_MS) used in the wait call inside useLobbyCommandMachine
(the location with await wait(220 + attempt * 140)); replace the inline
expression with a computed value using those constants, and add a brief comment
explaining the rationale (minimum base backoff and incremental backoff per
retry) and units (ms) so the intent is clear.

231-231: 💤 Low value

Verify asymmetric retry counts across lobby commands.

createLobby retries up to 2 times, joinByCode up to 5, and leaveLobby 3. The higher retry count for join may reflect its time-sensitivity (joining an active lobby before it fills/closes), but the difference is notable. Confirm this trade-off aligns with the intended UX and failure recovery strategy.

Also applies to: 268-268, 283-283

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/friend/hooks/useLobbyCommandMachine.ts` at line 231,
createLobby, joinByCode, and leaveLobby use different maxRetries (2, 5, 3
respectively); confirm this asymmetry is intentional and either normalize retry
counts or document the rationale. Locate the retry configs in
useLobbyCommandMachine (symbols: createLobby, joinByCode, leaveLobby) and: if
they should be uniform, replace hard-coded values with a shared constant (e.g.,
LOBBY_COMMAND_MAX_RETRIES) and use it for all three; if the differences are
intentional, add a short comment explaining why joinByCode needs more retries
(e.g., time-sensitivity) and ensure those counts are defined as named constants
for clarity and future tuning.
src/stores/realtime-match/match.slice.ts (1)

158-160: ⚡ Quick win

Document the matchPaused/resume countdown state flow

setMatchCountdown forces matchPaused: true when payload.reason === 'resume' (and clears pauseUntil), so matchPaused is used as a gameplay/UI gate during the resume countdown. The pause state is then cleared only on the subsequent match:resume socket event (store.clearMatchPaused()), not automatically when the countdown reaches 0. Add a brief comment near setMatchCountdown (or in the state docs) explaining that matchPaused is expected to remain true until match:resume, to avoid confusion about the “resume sets paused=true” logic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/realtime-match/match.slice.ts` around lines 158 - 160, Add a short
comment near setMatchCountdown (or in the match slice state docs) explaining
that when payload.reason === 'resume' the reducer intentionally sets
state.matchPaused = true and clears pauseUntil to gate gameplay/UI during the
resume countdown, and that this paused flag is NOT cleared when the countdown
reaches 0 but is cleared only when the external socket event match:resume calls
store.clearMatchPaused(); reference state.matchPaused, pauseUntil, pausedAt,
setMatchCountdown, and clearMatchPaused in the comment so future readers
understand the flow.
src/features/possession/hooks/usePossessionFieldState.ts (1)

125-135: 💤 Low value

Replace the inline MatchStatePayload cast with a typed penalty.kicksTaken shape

  • A repo-wide TypeScript search finds kicksTaken only in the inline assertion in src/features/possession/hooks/usePossessionFieldState.ts (around lines 125-135), so the shared realtime/socket typing for MatchStatePayload doesn’t declare penalty?.kicksTaken.
  • If the server sends penalty.kicksTaken, extend the shared socket types; otherwise avoid depending on this untyped payload shape.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/possession/hooks/usePossessionFieldState.ts` around lines 125 -
135, The inline cast to MatchStatePayload for accessing penalty.kicksTaken is
unsafe; update types instead of casting: either extend the shared
realtime/socket MatchStatePayload to declare penalty?: { kicksTaken?: { seat1?:
number; seat2?: number } } so usePossessionFieldState can access
stateWithPenalty.penalty?.kicksTaken with correct typing, or introduce a local
typed shape (e.g., PenaltyKicksTaken) and a small type guard/nullable extraction
in usePossessionFieldState before calling reconstructPenaltyAttempts
(referencing stateWithPenalty, possessionState.penaltyGoals,
reconstructPenaltyAttempts) so you no longer rely on an inline assertion. Ensure
the chosen change is applied in the shared types if the server actually sends
kicksTaken; otherwise keep it local and remove the cast.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/layout/Sidebar.tsx`:
- Line 85: The badge typography in the Sidebar component was changed from
font-black to font-semibold which lightens the visual weight; locate the badge
span(s) in Sidebar.tsx (the span with className containing "rounded-full
bg-red-500 ... font-poppins ... text-white") and either revert the class from
font-semibold back to font-black if the original heavy weight is required by
design, or confirm and propagate the new weight consistently across the other
badge occurrences mentioned (the other span(s) referenced as "99-99"); update
the className(s) accordingly in the Sidebar component and any duplicate badge
elements so the typography matches the approved design decision.
- Around line 72-81: The inline style in Sidebar (in the JSX that builds the nav
button using the isActive variable) lowered font weight to 700 and may have
dropped the Tailwind tracking utility; restore the original heavier weight by
changing the inline style to fontWeight: 900 or, better, remove the inline
fontWeight and re-add the Tailwind class "font-black" to the className for the
button (target the same JSX block that currently sets style={{ fontFamily:
"'Poppins', sans-serif", fontWeight: 700, ... }} and the class list with
"relative inline-flex ... tracking-wide"), and ensure "tracking-wide" remains
present in the class list.

In `@src/features/friend/FriendMatchHubPage.tsx`:
- Around line 78-86: The effect returns immediately after
router.push(`/friend/room/${lobby.inviteCode}`) and never clears the navigating
flag; ensure you reset the navigation state after navigation by calling the
appropriate setter (e.g. setIsNavigatingToRoom(false)) or a shared reset
function rather than returning early. Concretely, in the block that checks
lobby?.inviteCode && isNavigatingToRoom, call resetLobbyCommand() (if it doesn't
already) and then reset the isNavigatingToRoom state (via
setIsNavigatingToRoom(false) or the existing resetNavigation helper) after
router.push, remove the early return so the state-clearing runs, and keep
dependencies including isNavigatingToRoom, resetLobbyCommand and router
unchanged.

In `@src/features/welcome/useWelcomeAuthController.ts`:
- Around line 449-458: The early return inside the verifyGeorgianPhoneOtp catch
block leaves authSubmitting true; fix by ensuring authSubmitting is reset before
returning when isPendingDeletionAuthError(error) is true — specifically, call
the state setter that clears loading (authSubmitting -> false) before calling
setPendingRestoreAction and setAuthNoticeModal or immediately after those but
prior to returning in the catch branch that handles isPendingDeletionAuthError;
locate this logic around verifyGeorgianPhoneOtp, isPendingDeletionAuthError,
setPendingRestoreAction and setAuthNoticeModal and ensure the loading state is
explicitly cleared on that early-return path.

---

Outside diff comments:
In `@src/lib/geo/countries.ts`:
- Around line 534-540: The "los angeles" entry in the countries.ts dataset uses
coordinates (lon: -117.65, lat: 34.15) which are nearer San Bernardino; update
the "los angeles" object's lon/lat to downtown LA coordinates (approx lon:
-118.2437, lat: 34.0522) in src/lib/geo/countries.ts for the "los angeles" key
and keep the city/country/flag fields unchanged; verify the updated point is now
close to LA city center and adjust slightly if you prefer an alternate official
center.

In `@src/types/api.generated.ts`:
- Around line 901-908: The generated callback query type for
/api/v1/auth/sms/smsoffice-callback is missing the required `secret` param,
causing a client/server contract mismatch; update the `query` shape in the type
(the object containing `reference`, `status`, `reason?`, `destination`,
`timestamp?`, `operator?`) to include `secret: string` (required, not optional)
so the typed contract matches the documented/authenticated callback and the 401
handling.

---

Nitpick comments:
In `@src/components/auth/__tests__/WelcomeScreen.test.tsx`:
- Line 328: The assertion in WelcomeScreen.test.tsx is too strict by expecting
an explicit fourth argument of undefined when calling
socialLoginWithIdTokenMock; update the test to only assert the meaningful
parameters (e.g., 'google', 'tok', 'nonce') and relax or omit the check for the
optional fourth argument—either drop the fourth-arg check and rely on
toHaveBeenCalledTimes or use a matcher like expect.anything()/a wildcard for the
fourth parameter when calling socialLoginWithIdTokenMock so changes that omit
the trailing undefined won't break the test.

In `@src/features/friend/hooks/useLobbyCommandMachine.ts`:
- Line 208: Extract the magic numbers in the backoff formula into named
constants (e.g., MIN_BACKOFF_MS and BACKOFF_INCREMENT_MS) used in the wait call
inside useLobbyCommandMachine (the location with await wait(220 + attempt *
140)); replace the inline expression with a computed value using those
constants, and add a brief comment explaining the rationale (minimum base
backoff and incremental backoff per retry) and units (ms) so the intent is
clear.
- Line 231: createLobby, joinByCode, and leaveLobby use different maxRetries (2,
5, 3 respectively); confirm this asymmetry is intentional and either normalize
retry counts or document the rationale. Locate the retry configs in
useLobbyCommandMachine (symbols: createLobby, joinByCode, leaveLobby) and: if
they should be uniform, replace hard-coded values with a shared constant (e.g.,
LOBBY_COMMAND_MAX_RETRIES) and use it for all three; if the differences are
intentional, add a short comment explaining why joinByCode needs more retries
(e.g., time-sensitivity) and ensure those counts are defined as named constants
for clarity and future tuning.

In `@src/features/possession/hooks/usePossessionFieldState.ts`:
- Around line 125-135: The inline cast to MatchStatePayload for accessing
penalty.kicksTaken is unsafe; update types instead of casting: either extend the
shared realtime/socket MatchStatePayload to declare penalty?: { kicksTaken?: {
seat1?: number; seat2?: number } } so usePossessionFieldState can access
stateWithPenalty.penalty?.kicksTaken with correct typing, or introduce a local
typed shape (e.g., PenaltyKicksTaken) and a small type guard/nullable extraction
in usePossessionFieldState before calling reconstructPenaltyAttempts
(referencing stateWithPenalty, possessionState.penaltyGoals,
reconstructPenaltyAttempts) so you no longer rely on an inline assertion. Ensure
the chosen change is applied in the shared types if the server actually sends
kicksTaken; otherwise keep it local and remove the cast.

In `@src/features/welcome/WelcomeScreen.tsx`:
- Around line 185-193: The variant prop fallback is redundant because
WelcomeAuthNoticeModal is only rendered when authNoticeModal !== null; remove
the nullish coalescing fallback and pass authNoticeModal directly to variant
(i.e., use variant={authNoticeModal}) and keep existing handlers
(handleCloseAuthNoticeModal, handleNoticeModalGoToSignIn,
handleRestorePendingDeletion) and restore props (restoreSubmitting,
restoreError) unchanged so behavior is preserved.

In `@src/lib/api/__tests__/client.test.ts`:
- Around line 32-33: Remove the redundant no-op mock setup: delete the line
calling mockedRequest.mockRejectedValue?.(undefined) since mockRejectedValue
always exists on the vi.fn and the next line mockedRequest.mockRejectedValue(new
ApiError("Request failed", 401, null)) immediately overwrites it; update the
test in client.test.ts by keeping only the meaningful
mockedRequest.mockRejectedValue(new ApiError(...)) call (refer to
mockedRequest.mockRejectedValue and ApiError in the test).

In `@src/lib/config.ts`:
- Around line 4-12: Replace the hardcoded export const PHONE_AUTH_ENABLED = true
with an environment-driven boolean: read a new env var (e.g.,
process.env.PHONE_AUTH_ENABLED) and parse it to a boolean with a sensible
default (true for staging or configurable via another env like NODE_ENV); update
the PHONE_AUTH_ENABLED constant to use that parsed value so the feature can be
toggled per environment while preserving current default behavior. Ensure
parsing handles "1"/"true"/"false"/"0" case-insensitively and falls back to the
existing default when unset.

In `@src/stores/realtime-match/match.slice.ts`:
- Around line 158-160: Add a short comment near setMatchCountdown (or in the
match slice state docs) explaining that when payload.reason === 'resume' the
reducer intentionally sets state.matchPaused = true and clears pauseUntil to
gate gameplay/UI during the resume countdown, and that this paused flag is NOT
cleared when the countdown reaches 0 but is cleared only when the external
socket event match:resume calls store.clearMatchPaused(); reference
state.matchPaused, pauseUntil, pausedAt, setMatchCountdown, and clearMatchPaused
in the comment so future readers understand the flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ccfadd63-4b8f-4cc0-9dc4-bb46371a5106

📥 Commits

Reviewing files that changed from the base of the PR and between 4304f0b and 2fb604d.

📒 Files selected for processing (103)
  • .gitignore
  • CLAUDE.md
  • src/app/(app)/friend/room/[code]/page.tsx
  • src/app/(app)/settings/__tests__/page.test.tsx
  • src/app/(app)/settings/page.tsx
  • src/app/(fullscreen)/dev/animations/page.tsx
  • src/app/(fullscreen)/dev/bar-battle/page.tsx
  • src/app/(fullscreen)/dev/penalties/page.tsx
  • src/app/layout.tsx
  • src/components/auth/OAuthCallbackScreen.tsx
  • src/components/auth/__tests__/WelcomeScreen.test.tsx
  • src/components/game/PossessionQuestionPanel.tsx
  • src/components/layout/Sidebar.tsx
  • src/components/layout/__tests__/AppShell.test.tsx
  • src/components/layout/app-shell/useAppShellViewModel.ts
  • src/components/shared/BanCategoryCard.tsx
  • src/components/shared/FriendPlayModal.tsx
  • src/contexts/LocaleContext.tsx
  • src/contexts/__tests__/LocaleContext.test.tsx
  • src/features/daily/CareerPathGame.tsx
  • src/features/daily/FootballLogicGame.tsx
  • src/features/daily/daily-challenge.utils.ts
  • src/features/friend/FriendMatchHubPage.tsx
  • src/features/friend/components/AlreadyInLobbyModal.tsx
  • src/features/friend/components/CreateJoinPanel.tsx
  • src/features/friend/components/FriendLobbyScreen.tsx
  • src/features/friend/components/LobbyBrowsePanel.tsx
  • src/features/friend/components/LobbyCard.tsx
  • src/features/friend/components/LobbyHeader.tsx
  • src/features/friend/components/LobbySettings.tsx
  • src/features/friend/components/__tests__/CreateJoinPanel.test.tsx
  • src/features/friend/hooks/__tests__/useFriendLobbyLogic.test.tsx
  • src/features/friend/hooks/useFriendLobbyLogic.ts
  • src/features/friend/hooks/useLobbyCommandMachine.ts
  • src/features/play/ModeSelectionScreen.tsx
  • src/features/possession/components/BarBattleOverlay.tsx
  • src/features/possession/components/GoalProgressBar.tsx
  • src/features/possession/components/HalftimeScreen.tsx
  • src/features/possession/components/LiveSpecialQuestionPanel.tsx
  • src/features/possession/components/PenaltyHUD.tsx
  • src/features/possession/components/PenaltyStartCountdownOverlay.tsx
  • src/features/possession/components/PitchVisualization.tsx
  • src/features/possession/components/PossessionHUD.tsx
  • src/features/possession/components/PossessionMatchViewport.tsx
  • src/features/possession/components/__tests__/BarBattleOverlay.test.tsx
  • src/features/possession/components/__tests__/HalftimeScreen.test.tsx
  • src/features/possession/components/__tests__/PenaltyHUD.test.tsx
  • src/features/possession/components/__tests__/PitchVisualization.test.tsx
  • src/features/possession/components/__tests__/barBattle.helpers.test.ts
  • src/features/possession/components/bar-battle/BarBattleBar.tsx
  • src/features/possession/components/bar-battle/BarBattleBars.tsx
  • src/features/possession/components/bar-battle/BarBattleStackedBar.tsx
  • src/features/possession/components/bar-battle/barBattle.helpers.ts
  • src/features/possession/components/bar-battle/barBattle.types.ts
  • src/features/possession/components/bar-battle/useBarBattleViewModel.ts
  • src/features/possession/components/live-special/shared.tsx
  • src/features/possession/components/pitch/PitchHtmlActors.tsx
  • src/features/possession/components/pitch/usePitchSceneModel.ts
  • src/features/possession/hooks/__tests__/useBarBattle.test.ts
  • src/features/possession/hooks/__tests__/usePossessionBarBattleFlights.test.ts
  • src/features/possession/hooks/useBarBattle.ts
  • src/features/possession/hooks/usePossessionAnimationOrchestrator.ts
  • src/features/possession/hooks/usePossessionBarBattleFlights.ts
  • src/features/possession/hooks/usePossessionFieldState.ts
  • src/features/possession/hooks/usePossessionGoalCelebration.ts
  • src/features/possession/hooks/usePossessionMatchSounds.ts
  • src/features/possession/hooks/usePossessionRoundTransition.ts
  • src/features/possession/hooks/useRealtimePossessionMatchController.ts
  • src/features/settings/SettingsScreen.tsx
  • src/features/settings/components/SettingsSection.tsx
  • src/features/training/TrainingMatchProvider.tsx
  • src/features/welcome/WelcomeAuthNoticeModal.tsx
  • src/features/welcome/WelcomeScreen.tsx
  • src/features/welcome/useWelcomeAuthController.ts
  • src/lib/analytics/game-events.ts
  • src/lib/api/__tests__/client.test.ts
  • src/lib/api/client.ts
  • src/lib/auth/__tests__/auth.refresh.test.ts
  • src/lib/auth/__tests__/postAuthRedirect.test.ts
  • src/lib/auth/auth.service.ts
  • src/lib/auth/postAuthRedirect.ts
  • src/lib/config.ts
  • src/lib/friend/inviteCode.ts
  • src/lib/geo/__tests__/countryCode.test.ts
  • src/lib/geo/countries.ts
  • src/lib/geo/resolveLocation.ts
  • src/lib/match/__tests__/useRealtimeGameLogic.test.ts
  • src/lib/match/useRealtimeGameLogic.ts
  • src/lib/queries/__tests__/leaderboard.queries.test.tsx
  • src/lib/queries/__tests__/social.queries.test.tsx
  • src/lib/queries/leaderboard.queries.ts
  • src/lib/queries/social.queries.ts
  • src/lib/realtime/socket-client.ts
  • src/lib/realtime/socket.types.ts
  • src/lib/sounds/__tests__/gameSounds.test.ts
  • src/lib/sounds/gameSounds.ts
  • src/lib/usePreloadImages.ts
  • src/messages/en.json
  • src/messages/ka.json
  • src/stores/__tests__/realtimeMatch.store.test.ts
  • src/stores/realtime-match/match.slice.ts
  • src/test/setup.ts
  • src/types/api.generated.ts
💤 Files with no reviewable changes (2)
  • CLAUDE.md
  • src/features/daily/daily-challenge.utils.ts

Comment thread src/components/layout/Sidebar.tsx
Comment thread src/components/layout/Sidebar.tsx
Comment thread src/features/friend/FriendMatchHubPage.tsx
Comment thread src/features/welcome/useWelcomeAuthController.ts

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/messages/ka.json (1)

48-48: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Version string mismatch between locales.

The Georgian locale shows version 0.9.15 (ბეტა) while the English locale (line 114 in en.json) shows Version 0.9.5 (Beta). Version strings should be identical across all locales to maintain consistency in user-facing displays and simplify release tracking.

🔧 Suggested fix to align with English version
-    "version": "ვერსია 0.9.15 (ბეტა)",
+    "version": "ვერსია 0.9.5 (ბეტა)",

Alternatively, if 0.9.15 is correct, update en.json line 114 to match.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/messages/ka.json` at line 48, The "version" value in src/messages/ka.json
("ვერსია 0.9.15 (ბეტა)") is inconsistent with the English locale; update the
"version" key in either src/messages/ka.json or src/messages/en.json so both
locales use the identical version string (e.g., change the "version" value in
src/messages/ka.json to match the English "version" value from
src/messages/en.json or vice versa if 0.9.15 is the intended release).
🧹 Nitpick comments (2)
src/features/settings/SettingsScreen.tsx (1)

507-514: ⚡ Quick win

The last-child close-button hack is fragile in both dialogs.

Both dialogs rely on [&>button:last-child]:hidden to suppress the default DialogContent close button. That only works while the wrapper keeps rendering its internal close button as the last child. Please switch this to a wrapper-level API instead of reaching into its DOM structure from each caller.

Also applies to: 601-608

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/settings/SettingsScreen.tsx` around lines 507 - 514, The
DialogContent consumer is using a fragile CSS selector
"[&>button:last-child]:hidden" to hide the internal close button; instead add a
wrapper-level prop (e.g., hideDefaultCloseButton: boolean) to the DialogContent
component and use that prop inside DialogContent to conditionally render or omit
its default close button. Update callers (the SettingsScreen usages around the
ModalCloseButton and the other dialog at lines ~601-608) to remove the CSS hack
and pass hideDefaultCloseButton={true}, retaining the external ModalCloseButton
and the onClose logic that checks isUpdatingPhone and calls
setPhoneDialogOpen(false).
src/features/profile/components/AvatarPicker.tsx (1)

437-438: ⚡ Quick win

Avoid hiding the default dialog close button via last-child.

[&>button:last-child]:hidden couples this screen to DialogContent’s current internal DOM order. If src/components/ui/dialog.tsx ever adds another trailing button or reorders children, this will hide the wrong element and the duplicate close button comes back. A first-class hideDefaultCloseButton prop on the dialog wrapper would make this safe.

♻️ Safer direction
<DialogContent hideDefaultCloseButton className="sm:max-w-3xl max-h-[90dvh] overflow-y-auto rounded-[24px] border-0 bg-brand-blue p-6 sm:p-8">
  <ModalCloseButton onClose={() => onOpenChange(false)} />
  ...
</DialogContent>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/profile/components/AvatarPicker.tsx` around lines 437 - 438,
Remove the fragile CSS selector [&>button:last-child]:hidden from the
DialogContent usage and instead pass a first-class prop to explicitly hide the
built-in close button (e.g., <DialogContent hideDefaultCloseButton ...>); keep
the custom ModalCloseButton(onClose={() => onOpenChange(false)}) inside the
DialogContent. Update the DialogContent usage here (reference symbol:
DialogContent) to include hideDefaultCloseButton and remove the selector, and if
needed add support for that prop in the dialog wrapper implementation so it
suppresses the default close button rather than relying on DOM order.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/shared/ModalCloseButton.tsx`:
- Around line 40-44: In ModalCloseButton (component ModalCloseButton), replace
the Tailwind classes that hide the browser focus outline: change "outline-none
focus:outline-none" to "outline-hidden focus:outline-hidden" while keeping the
existing focus-visible:ring-* classes intact; update the class string used in
the close button element (where outline-none appears) so
forced-colors/high-contrast users retain an accessible focus indicator.

In `@src/features/welcome/WelcomeGoogleButton.tsx`:
- Around line 70-72: The Button in WelcomeGoogleButton.tsx currently strips
keyboard focus styles (it contains focus-visible:ring-0 and
focus-visible:outline-none); restore a visible focus indicator by removing those
neutralizing classes and replacing them with an accessible focus-visible style
(for example use focus-visible:ring-2 or focus-visible:outline-2 plus a
high-contrast color like focus-visible:ring-black or
focus-visible:ring-brand-yellow-deep) on the same Button element (the one using
onClick and the long className) so keyboard users can see focus.

In `@src/stores/auth.store.ts`:
- Around line 129-143: Both terminal branches that clear tokens and set
anonymous state (the branch under isAuthFailure(retryError) and the branch
checking refreshed.terminal) must also call resetUser() to reset analytics
identity; add a call to resetUser() immediately after clearLocalSession() and
before set({ status: "anonymous", user: null, hasBootstrapped: true }) in both
places so the PostHog identity is cleared when bootstrap forces an anonymous
session.
- Around line 146-154: The retry catch currently logs bootstrapUser() failures
and leaves the store in "loading"; if the error is an HTTP 401 we must treat it
as terminal: detect a 401 from retryError (e.g. retryError.status === 401 or
retryError.response?.status === 401), clear any local session/auth tokens (call
the existing session clear function or remove stored credentials), set({ status:
"anonymous", user: null, hasBootstrapped: true }) and log the terminal outcome
instead of leaving "loading"; for non-401 errors keep the existing warn/log and
set({ status: "loading" }) behavior. Ensure you reference bootstrapUser,
refreshSession, set, and logger.warn/logger.info when making the change.

---

Outside diff comments:
In `@src/messages/ka.json`:
- Line 48: The "version" value in src/messages/ka.json ("ვერსია 0.9.15 (ბეტა)")
is inconsistent with the English locale; update the "version" key in either
src/messages/ka.json or src/messages/en.json so both locales use the identical
version string (e.g., change the "version" value in src/messages/ka.json to
match the English "version" value from src/messages/en.json or vice versa if
0.9.15 is the intended release).

---

Nitpick comments:
In `@src/features/profile/components/AvatarPicker.tsx`:
- Around line 437-438: Remove the fragile CSS selector
[&>button:last-child]:hidden from the DialogContent usage and instead pass a
first-class prop to explicitly hide the built-in close button (e.g.,
<DialogContent hideDefaultCloseButton ...>); keep the custom
ModalCloseButton(onClose={() => onOpenChange(false)}) inside the DialogContent.
Update the DialogContent usage here (reference symbol: DialogContent) to include
hideDefaultCloseButton and remove the selector, and if needed add support for
that prop in the dialog wrapper implementation so it suppresses the default
close button rather than relying on DOM order.

In `@src/features/settings/SettingsScreen.tsx`:
- Around line 507-514: The DialogContent consumer is using a fragile CSS
selector "[&>button:last-child]:hidden" to hide the internal close button;
instead add a wrapper-level prop (e.g., hideDefaultCloseButton: boolean) to the
DialogContent component and use that prop inside DialogContent to conditionally
render or omit its default close button. Update callers (the SettingsScreen
usages around the ModalCloseButton and the other dialog at lines ~601-608) to
remove the CSS hack and pass hideDefaultCloseButton={true}, retaining the
external ModalCloseButton and the onClose logic that checks isUpdatingPhone and
calls setPhoneDialogOpen(false).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3c523b5c-d3a2-4fa3-8e06-4238f2a72734

📥 Commits

Reviewing files that changed from the base of the PR and between 2fb604d and a2aa412.

📒 Files selected for processing (23)
  • src/components/auth/OAuthCallbackScreen.tsx
  • src/components/auth/__tests__/OAuthCallbackScreen.test.tsx
  • src/components/auth/__tests__/WelcomeScreen.test.tsx
  • src/components/shared/ModalCloseButton.tsx
  • src/features/friend/components/CreateJoinPanel.tsx
  • src/features/friend/components/LobbyBrowsePanel.tsx
  • src/features/profile/components/AvatarPicker.tsx
  • src/features/settings/SettingsScreen.tsx
  • src/features/welcome/InAppBrowserInstructions.tsx
  • src/features/welcome/WelcomeGoogleButton.tsx
  • src/features/welcome/WelcomeLoginDialog.tsx
  • src/features/welcome/WelcomePhoneAuthForm.tsx
  • src/features/welcome/WelcomeScreen.tsx
  • src/features/welcome/useWelcomeAuthController.ts
  • src/lib/auth/auth.service.ts
  • src/lib/auth/google-identity.ts
  • src/lib/auth/in-app-browser.ts
  • src/lib/auth/useGeorgianPhoneAuthAvailability.ts
  • src/lib/i18n/copy.ts
  • src/messages/en.json
  • src/messages/ka.json
  • src/stores/__tests__/auth.store.test.ts
  • src/stores/auth.store.ts
💤 Files with no reviewable changes (2)
  • src/lib/auth/in-app-browser.ts
  • src/features/welcome/InAppBrowserInstructions.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/lib/i18n/copy.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/features/friend/components/LobbyBrowsePanel.tsx
  • src/features/welcome/WelcomeScreen.tsx
  • src/components/auth/OAuthCallbackScreen.tsx
  • src/features/friend/components/CreateJoinPanel.tsx
  • src/lib/auth/auth.service.ts
  • src/components/auth/tests/WelcomeScreen.test.tsx
  • src/features/welcome/useWelcomeAuthController.ts

Comment thread src/components/shared/ModalCloseButton.tsx Outdated
Comment thread src/features/welcome/WelcomeGoogleButton.tsx Outdated
Comment thread src/stores/auth.store.ts
Comment thread src/stores/auth.store.ts
- Google/Facebook buttons: show a loading spinner + disable on press so a
  tap always gives immediate feedback while the token exchange / OAuth
  redirect runs.
- Google: GIS-everywhere — the overlaid Google button now covers the whole
  button so any tap runs the in-page token flow (works in Safari/Chrome and
  in-app browsers); classic redirect is kept only as a fallback when GIS
  can't load.
- OAuth callback: a cancelled/denied consent (error in the query OR hash) is
  no longer treated as a failure — it returns to the landing page instead of
  logging an error and flashing the failure screen.
- Possession bar: the 100 goal-threshold label is green instead of red.
- Tweak the OAuth 'finalizing' loading copy.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/messages/en.json (1)

686-686: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the product-name casing in this restore message.

Quizball is inconsistent with the rest of the UI branding and should stay QuizBall.

✏️ Suggested copy fix
-    "restoreAccountDescription": "This account is scheduled for deletion. Restoring it will cancel deletion and let you continue using Quizball.",
+    "restoreAccountDescription": "This account is scheduled for deletion. Restoring it will cancel deletion and let you continue using QuizBall.",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/messages/en.json` at line 686, The string value for the localization key
"restoreAccountDescription" uses incorrect product-name casing ("Quizball");
update the value to use the correct branding "QuizBall" so the message reads:
This account is scheduled for deletion. Restoring it will cancel deletion and
let you continue using QuizBall. Ensure you only change the product name in the
"restoreAccountDescription" entry in src/messages/en.json.
src/features/daily/__tests__/CountdownGame.test.tsx (1)

86-100: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Wait past the debounce before asserting the answer stays hidden.

Line 96 only schedules the debounced auto-check path, so the assertion on Line 99 runs before the changed behavior can fire. A delayed reveal regression would still pass this test.

🧪 Suggested test fix
-  it('does not leak answers as suggestions while typing a partial', () => {
+  it('does not leak answers as suggestions while typing a partial', async () => {
+    vi.useFakeTimers();
     render(
       <CountdownGame
         session={session as never}
         onBack={vi.fn()}
         onComplete={vi.fn()}
       />
     );

     const input = screen.getByPlaceholderText('Press Enter to submit...');
     fireEvent.change(input, { target: { value: 'rona' } });
+    await vi.advanceTimersByTimeAsync(200);

     // The full answer must NOT be revealed before it's accepted.
     expect(screen.queryByText('Cristiano Ronaldo')).not.toBeInTheDocument();
+    vi.useRealTimers();
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/daily/__tests__/CountdownGame.test.tsx` around lines 86 - 100,
The test for CountdownGame is asserting too soon—after fireEvent.change the
component schedules a debounced auto-check, so the assertion that "Cristiano
Ronaldo" is not revealed must wait until the debounce window has elapsed; update
the test around the input interaction in CountdownGame.test.tsx to advance
timers or await the component (e.g., use act +
jest.advanceTimersByTime(debounceMs) or await waitFor with a timeout) after
calling fireEvent.change so the debounced path (the component's auto-check
logic) has a chance to run before asserting that the full answer is not in the
document.
src/features/play/ModeSelectionScreen.tsx (1)

103-145: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix entrance animation gating to avoid SSR permanently disabling it
shouldPlayEntranceAnimation() returns false when window is undefined, so useState(shouldPlayEntranceAnimation) initializes playEntranceAnimation to false during SSR. That keeps the motion.div in the non-animated path (initial=false, transition.duration=0) and the useEffect never sets PLAY_ENTRANCE_SESSION_KEY, so a hard refresh won’t trigger the “once per browser session” animation when the key is missing. Move the sessionStorage read and derived animation state into a mounted effect (or gate rendering on mount) so the client can decide after hydration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/play/ModeSelectionScreen.tsx` around lines 103 - 145, The
entrance animation flag is being initialized during SSR because
useState(shouldPlayEntranceAnimation) calls window-dependent logic; instead
initialize playEntranceAnimation to a safe server value (e.g. false/undefined)
and determine the true client-side value inside a mounting effect: in an effect
run on mount call shouldPlayEntranceAnimation() and read
PLAY_ENTRANCE_SESSION_KEY from sessionStorage (catching errors) and then
setPlayEntranceAnimation according to those results (and set the session key
when animation should play). Update any logic that reads playEntranceAnimation
(e.g. motion.div props and the existing useEffect that writes
PLAY_ENTRANCE_SESSION_KEY) to rely on the client-set state so SSR does not
permanently disable the animation.
src/features/welcome/useWelcomeAuthController.ts (1)

193-225: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Split GIS acquisition from the in-place Google login path.

This try/catch is too broad. Once signInWithGoogleIdentity() succeeds, a successful in-place login returns without clearing socialSubmitting or closing the dialog, and a later bootstrap() failure is treated as “GIS unavailable” and falls back into a second redirect-based OAuth attempt. Keep the GIS acquisition fallback separate from the token-exchange/bootstrap path.

Proposed fix
   if (googleClientId) {
     let googleIdentity: { idToken: string; nonce?: string } | null = null;
     try {
       googleIdentity = await signInWithGoogleIdentity(googleClientId);
-      await socialLoginWithIdToken('google', googleIdentity.idToken, googleIdentity.nonce);
-      await bootstrap({ force: true });
-      return;
     } catch (gisError) {
-      if (googleIdentity && isPendingDeletionAuthError(gisError)) {
-        setPendingRestoreAction({
-          kind: 'social-token',
-          provider: 'google',
-          idToken: googleIdentity.idToken,
-          nonce: googleIdentity.nonce,
-        });
-        setAuthNoticeModal('pending-deletion');
-        setLoginOpen(true);
-        setSocialSubmitting(null);
-        return;
-      }
       console.warn('GIS sign-in unavailable, falling back to redirect', gisError);
     }
+
+    if (googleIdentity) {
+      try {
+        await socialLoginWithIdToken('google', googleIdentity.idToken, googleIdentity.nonce);
+        await bootstrap({ force: true });
+        setLoginOpen(false);
+        setSocialSubmitting(null);
+        return;
+      } catch (error) {
+        if (isPendingDeletionAuthError(error)) {
+          setPendingRestoreAction({
+            kind: 'social-token',
+            provider: 'google',
+            idToken: googleIdentity.idToken,
+            nonce: googleIdentity.nonce,
+          });
+          setAuthNoticeModal('pending-deletion');
+          setLoginOpen(true);
+          setSocialSubmitting(null);
+          return;
+        }
+        console.error('Google login failed', error);
+        setSocialSubmitting(null);
+        return;
+      }
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/welcome/useWelcomeAuthController.ts` around lines 193 - 225,
Split the GIS acquisition and the token-exchange/bootstrap flows: first wrap
only signInWithGoogleIdentity(googleClientId) in its own try/catch so failures
there trigger the redirect fallback via socialLogin('google', redirectTo); once
signInWithGoogleIdentity returns a googleIdentity, perform
socialLoginWithIdToken('google', googleIdentity.idToken, googleIdentity.nonce)
and await bootstrap({ force: true }) inside a separate try/catch so bootstrap
failures are handled locally (setSocialSubmitting(null), show an error modal or
keep dialog open) and do not fall through to the redirect path; ensure on
successful in-place login you clear socialSubmitting and close the dialog (call
setSocialSubmitting(null) and setLoginOpen(false)) and return to prevent
executing the redirect block; keep existing pending-deletion handling
(isPendingDeletionAuthError) after socialLoginWithIdToken as-is using
setPendingRestoreAction and setAuthNoticeModal.
🧹 Nitpick comments (2)
src/app/(app)/daily/challenges/[challengeId]/page.tsx (1)

54-61: ⚡ Quick win

Avoid invalidating the daily-challenge list twice.

useCompleteDailyChallenge() already invalidates queryKeys.dailyChallenges.all in onSettled, so this helper guarantees a second refetch on every completion. Keeping the wallet invalidation here is fine, but the daily-challenge invalidation should live in one place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(app)/daily/challenges/[challengeId]/page.tsx around lines 54 - 61,
The helper invalidateAfterComplete is redundantly invalidating the
daily-challenge list; remove the queryClient.invalidateQueries({ queryKey:
queryKeys.dailyChallenges.all }) call from invalidateAfterComplete and only keep
the wallet invalidation (queryClient.invalidateQueries({ queryKey:
queryKeys.store.wallet() })); rely on useCompleteDailyChallenge's onSettled
handler to perform the daily-challenge list invalidation so you don't trigger a
duplicate refetch. Ensure Promise.all (or the surrounding async logic) is
updated accordingly to only include the wallet invalidation.
src/components/shared/BanCategoryCard.tsx (1)

103-110: ⚡ Quick win

Don't mark every card image as eager/high-priority.

These cards render in groups, so loading="eager" plus fetchPriority="high" on all of them makes every artwork compete with more important above-the-fold assets. The browser defaults are a better fit here unless you're intentionally boosting a single hero image.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/shared/BanCategoryCard.tsx` around lines 103 - 110, The card
image in BanCategoryCard.tsx is marked with loading="eager" and
fetchPriority="high", causing every card to compete with above-the-fold assets;
remove those attributes (or set loading="lazy" if you want deferred loading)
from the <img> in the BanCategoryCard component so only intentional hero images
are prioritized, or add a prop to selectively enable eager/high priority for a
single hero image if needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/auth/OAuthCallbackScreen.tsx`:
- Around line 54-64: The current OAuthCallbackScreen branch treats any provider
"error" as a benign cancellation; update the oauth error handling to only
silently handle known user-cancel codes (e.g., "access_denied",
"consent_required", "user_denied" — use whatever provider-specific cancellation
codes your app expects) by checking oauthError against a small whitelist before
logging the benign message, replacing history, and calling router.replace("/");
if oauthError is not in that whitelist, do not return here so the existing
failure handling runs and surfaces real OAuth/server errors. Locate this logic
around the variables hashParams, oauthError, logger, and router.replace in
OAuthCallbackScreen and implement the whitelist check.

In `@src/features/daily/CountdownGame.tsx`:
- Around line 71-88: handleRoundEnd can run twice and both calls will read the
same captured currentRound/foundAnswers, causing duplicate appends and
overshooting currentRound; make the transition atomic by using a functional
updater on setCurrentRound that examines the previous round and only
advances/marks finished once, and perform the append to setAllRoundAnswers and
setFinished based on that previous value (e.g., inside the updater or
immediately after checking prev in a single synchronous branch), ensuring you
still use setAllRoundAnswers(prev => [...prev, foundAnswers]) and clear
foundAnswers/input only when the update actually happened; update handleRoundEnd
to rely on prev values (not captured currentRound) to be idempotent.

In `@src/features/daily/dailyChallengeSessionPrefetch.ts`:
- Around line 39-45: The cache currently treats an in-flight entry stale by
comparing now - existing.createdAt against TTL_MS and can trigger duplicate
POSTs; change the logic so pending promises are never expired: when reading
cache (in prefetchDailyChallengeSession and consumeDailyChallengeSession) always
return existing.promise if existing.promise is still pending, and only apply TTL
eviction based on a resolvedAt timestamp set when the promise resolves (or set
createdAt on resolution inside the promise chain that wraps
createDailyChallengeSession(...).then(toDailyChallengeSession)). Update
references to existing.createdAt, entry.promise and TTL_MS and ensure
createDailyChallengeSession and toDailyChallengeSession are wrapped so
resolution sets the freshness timestamp instead of using the original insertion
time.

In `@src/features/daily/MoneyDropGame.tsx`:
- Around line 251-255: The auto-advance timeout set in the block using
autoAdvanceRef.current and setTimeout can still fire after a manual advance;
modify the logic to store the timeout id (e.g., in a new ref like
autoAdvanceTimeoutRef), clearTimeout(autoAdvanceTimeoutRef.current) when the
player manually advances (in the handler that calls
handleNextQuestionRef.current), and set autoAdvanceTimeoutRef.current =
setTimeout(...) when scheduling the auto-advance; also reset/clear the ref after
the timeout callback runs to avoid leaking stale ids.

In `@src/features/possession/components/HalftimeScreen.tsx`:
- Around line 269-272: The hardcoded title "Ban 1 Category Each" in the
HalftimeScreen component should be localized: update the div that currently
renders that literal to use the app's i18n translation function (e.g. call
t('possession.banTitle') or the project's equivalent) and ensure useTranslation
or the project's translation hook is imported and used in HalftimeScreen; also
add the corresponding translation key/value to the locale files for all
supported languages.

In `@src/features/profile/components/AvatarPicker.tsx`:
- Around line 259-264: In AvatarPicker, the empty-option label is hardcoded as
"None" inside the span and must be localized; update the AvatarPicker component
to use the app's localization helper (e.g., useTranslation() / t or i18n.t) and
replace the literal "None" in the span with the localized key (add a suitable
key like "avatar.none" to the translation files if missing) so the picker is
fully translated.

In `@src/features/welcome/WelcomeFacebookButton.tsx`:
- Around line 21-34: The button loses its accessible name when submitting
because the loading branch only renders <Loader2>; preserve the label by adding
aria-label={t('welcome.continueWithFacebook')} to the Button (or include an
offscreen span with the same t('welcome.continueWithFacebook') text inside the
submitting branch). Update the component where Button is rendered (the
submitting conditional that shows Loader2) so the Button keeps an accessible
name while submitting; use the existing i18n key
t('welcome.continueWithFacebook') to ensure consistency.

---

Outside diff comments:
In `@src/features/daily/__tests__/CountdownGame.test.tsx`:
- Around line 86-100: The test for CountdownGame is asserting too soon—after
fireEvent.change the component schedules a debounced auto-check, so the
assertion that "Cristiano Ronaldo" is not revealed must wait until the debounce
window has elapsed; update the test around the input interaction in
CountdownGame.test.tsx to advance timers or await the component (e.g., use act +
jest.advanceTimersByTime(debounceMs) or await waitFor with a timeout) after
calling fireEvent.change so the debounced path (the component's auto-check
logic) has a chance to run before asserting that the full answer is not in the
document.

In `@src/features/play/ModeSelectionScreen.tsx`:
- Around line 103-145: The entrance animation flag is being initialized during
SSR because useState(shouldPlayEntranceAnimation) calls window-dependent logic;
instead initialize playEntranceAnimation to a safe server value (e.g.
false/undefined) and determine the true client-side value inside a mounting
effect: in an effect run on mount call shouldPlayEntranceAnimation() and read
PLAY_ENTRANCE_SESSION_KEY from sessionStorage (catching errors) and then
setPlayEntranceAnimation according to those results (and set the session key
when animation should play). Update any logic that reads playEntranceAnimation
(e.g. motion.div props and the existing useEffect that writes
PLAY_ENTRANCE_SESSION_KEY) to rely on the client-set state so SSR does not
permanently disable the animation.

In `@src/features/welcome/useWelcomeAuthController.ts`:
- Around line 193-225: Split the GIS acquisition and the
token-exchange/bootstrap flows: first wrap only
signInWithGoogleIdentity(googleClientId) in its own try/catch so failures there
trigger the redirect fallback via socialLogin('google', redirectTo); once
signInWithGoogleIdentity returns a googleIdentity, perform
socialLoginWithIdToken('google', googleIdentity.idToken, googleIdentity.nonce)
and await bootstrap({ force: true }) inside a separate try/catch so bootstrap
failures are handled locally (setSocialSubmitting(null), show an error modal or
keep dialog open) and do not fall through to the redirect path; ensure on
successful in-place login you clear socialSubmitting and close the dialog (call
setSocialSubmitting(null) and setLoginOpen(false)) and return to prevent
executing the redirect block; keep existing pending-deletion handling
(isPendingDeletionAuthError) after socialLoginWithIdToken as-is using
setPendingRestoreAction and setAuthNoticeModal.

In `@src/messages/en.json`:
- Line 686: The string value for the localization key
"restoreAccountDescription" uses incorrect product-name casing ("Quizball");
update the value to use the correct branding "QuizBall" so the message reads:
This account is scheduled for deletion. Restoring it will cancel deletion and
let you continue using QuizBall. Ensure you only change the product name in the
"restoreAccountDescription" entry in src/messages/en.json.

---

Nitpick comments:
In `@src/app/`(app)/daily/challenges/[challengeId]/page.tsx:
- Around line 54-61: The helper invalidateAfterComplete is redundantly
invalidating the daily-challenge list; remove the
queryClient.invalidateQueries({ queryKey: queryKeys.dailyChallenges.all }) call
from invalidateAfterComplete and only keep the wallet invalidation
(queryClient.invalidateQueries({ queryKey: queryKeys.store.wallet() })); rely on
useCompleteDailyChallenge's onSettled handler to perform the daily-challenge
list invalidation so you don't trigger a duplicate refetch. Ensure Promise.all
(or the surrounding async logic) is updated accordingly to only include the
wallet invalidation.

In `@src/components/shared/BanCategoryCard.tsx`:
- Around line 103-110: The card image in BanCategoryCard.tsx is marked with
loading="eager" and fetchPriority="high", causing every card to compete with
above-the-fold assets; remove those attributes (or set loading="lazy" if you
want deferred loading) from the <img> in the BanCategoryCard component so only
intentional hero images are prioritized, or add a prop to selectively enable
eager/high priority for a single hero image if needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b736bb0b-c10f-491a-9d84-77347cd4ce3e

📥 Commits

Reviewing files that changed from the base of the PR and between a710bdc and e95aa27.

⛔ Files ignored due to path filters (2)
  • public/sounds/correct_answer.mp3 is excluded by !**/*.mp3
  • public/sounds/wrong_answer.mp3 is excluded by !**/*.mp3
📒 Files selected for processing (38)
  • src/app/(app)/daily/challenges/[challengeId]/page.tsx
  • src/app/(app)/daily/challenges/page.tsx
  • src/components/auth/OAuthCallbackScreen.tsx
  • src/components/layout/app-shell/useAppShellViewModel.ts
  • src/components/shared/BanCategoryCard.tsx
  • src/components/shared/FriendPlayModal.tsx
  • src/features/daily/CareerPathGame.tsx
  • src/features/daily/ClueGame.tsx
  • src/features/daily/CountdownGame.tsx
  • src/features/daily/FootballLogicGame.tsx
  • src/features/daily/ImposterGame.tsx
  • src/features/daily/MoneyDropGame.tsx
  • src/features/daily/PutInOrderGame.tsx
  • src/features/daily/__tests__/CountdownGame.test.tsx
  • src/features/daily/components/DailyChallengeIntro.tsx
  • src/features/daily/components/useResultSplash.ts
  • src/features/daily/dailyChallengeSessionPrefetch.ts
  • src/features/friend/components/CreateJoinPanel.tsx
  • src/features/leaderboard/LeaderboardScreen.tsx
  • src/features/play/ModeSelectionScreen.tsx
  • src/features/play/RankedCategoryBlockingScreen.tsx
  • src/features/possession/components/GoalProgressBar.tsx
  • src/features/possession/components/HalftimeScreen.tsx
  • src/features/possession/hooks/usePossessionMatchSounds.ts
  • src/features/profile/components/AvatarPicker.tsx
  • src/features/store/StoreScreen.tsx
  • src/features/welcome/WelcomeFacebookButton.tsx
  • src/features/welcome/WelcomeGoogleButton.tsx
  • src/features/welcome/WelcomeLoginDialog.tsx
  • src/features/welcome/WelcomeScreen.tsx
  • src/features/welcome/useWelcomeAuthController.ts
  • src/lib/avatars/partNames.ts
  • src/lib/avatars/parts.ts
  • src/lib/queries/dailyChallenges.queries.ts
  • src/lib/sounds/gameSounds.ts
  • src/messages/en.json
  • src/messages/ka.json
  • src/styles/globals.css
✅ Files skipped from review due to trivial changes (2)
  • src/features/leaderboard/LeaderboardScreen.tsx
  • src/lib/avatars/parts.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/lib/sounds/gameSounds.ts
  • src/features/daily/CareerPathGame.tsx
  • src/features/daily/FootballLogicGame.tsx
  • src/features/welcome/WelcomeScreen.tsx
  • src/features/welcome/WelcomeGoogleButton.tsx
  • src/features/welcome/WelcomeLoginDialog.tsx
  • src/components/shared/FriendPlayModal.tsx
  • src/messages/ka.json

Comment thread src/components/auth/OAuthCallbackScreen.tsx
Comment thread src/features/daily/CountdownGame.tsx
Comment thread src/features/daily/dailyChallengeSessionPrefetch.ts Outdated
Comment thread src/features/daily/MoneyDropGame.tsx Outdated
Comment thread src/features/possession/components/HalftimeScreen.tsx
Comment thread src/features/profile/components/AvatarPicker.tsx
Comment thread src/features/welcome/WelcomeFacebookButton.tsx

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/messages/en.json (1)

686-686: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inconsistent brand name capitalization.

"Quizball" should be "QuizBall" to match the branding used elsewhere in the file (e.g., copyright on line 643).

Suggested fix
-    "restoreAccountDescription": "This account is scheduled for deletion. Restoring it will cancel deletion and let you continue using Quizball.",
+    "restoreAccountDescription": "This account is scheduled for deletion. Restoring it will cancel deletion and let you continue using QuizBall.",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/messages/en.json` at line 686, The value for the key
"restoreAccountDescription" uses inconsistent branding ("Quizball"); update the
string value to use the correct capitalization "QuizBall" so it matches other
entries (e.g., the copyright) — locate the "restoreAccountDescription" entry and
replace "Quizball" with "QuizBall".
src/messages/ka.json (2)

48-48: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Version mismatch between locale files.

The Georgian locale shows version 0.9.15 while the English locale (en.json line 114) shows 0.9.5. These should be consistent across all locale files to avoid user confusion.

Suggested fix (assuming 0.9.5 is correct)
-    "version": "ვერსია 0.9.15 (ბეტა)",
+    "version": "ვერსია 0.9.5 (ბეტა)",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/messages/ka.json` at line 48, Update the Georgian locale's "version"
value to match the canonical version used in other locales (change the "version"
key value in src/messages/ka.json from "ვერსია 0.9.15 (ბეტა)" to the correct
canonical string used in en.json, e.g., "ვერსია 0.9.5 (ბეტა)") so the "version"
key is consistent across locale files; ensure you update the exact "version"
JSON key in ka.json to the same text used in en.json.

284-284: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inconsistent brand name capitalization.

Same issue as in en.json — "Quizball" should be "QuizBall" to match the product branding.

Suggested fix
-    "restoreAccountDescription": "ეს ანგარიში წაშლის რიგშია. აღდგენა გააუქმებს წაშლას და Quizball-ის გამოყენებას გააგრძელებთ.",
+    "restoreAccountDescription": "ეს ანგარიში წაშლის რიგშია. აღდგენა გააუქმებს წაშლას და QuizBall-ის გამოყენებას გააგრძელებთ.",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/messages/ka.json` at line 284, The string value for the key
restoreAccountDescription contains the incorrectly capitalized product name
"Quizball"; update the value so the brand matches the canonical "QuizBall"
(i.e., change "Quizball-ის" to "QuizBall-ის") in the restoreAccountDescription
entry to ensure consistent branding across locales.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/messages/en.json`:
- Line 686: The value for the key "restoreAccountDescription" uses inconsistent
branding ("Quizball"); update the string value to use the correct capitalization
"QuizBall" so it matches other entries (e.g., the copyright) — locate the
"restoreAccountDescription" entry and replace "Quizball" with "QuizBall".

In `@src/messages/ka.json`:
- Line 48: Update the Georgian locale's "version" value to match the canonical
version used in other locales (change the "version" key value in
src/messages/ka.json from "ვერსია 0.9.15 (ბეტა)" to the correct canonical string
used in en.json, e.g., "ვერსია 0.9.5 (ბეტა)") so the "version" key is consistent
across locale files; ensure you update the exact "version" JSON key in ka.json
to the same text used in en.json.
- Line 284: The string value for the key restoreAccountDescription contains the
incorrectly capitalized product name "Quizball"; update the value so the brand
matches the canonical "QuizBall" (i.e., change "Quizball-ის" to "QuizBall-ის")
in the restoreAccountDescription entry to ensure consistent branding across
locales.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 456cb91c-c186-4dfe-a9ef-0c38350b22f2

📥 Commits

Reviewing files that changed from the base of the PR and between e95aa27 and e4660be.

📒 Files selected for processing (19)
  • src/components/auth/OAuthCallbackScreen.tsx
  • src/components/auth/__tests__/WelcomeScreen.test.tsx
  • src/components/shared/ModalCloseButton.tsx
  • src/features/daily/CountdownGame.tsx
  • src/features/daily/MoneyDropGame.tsx
  • src/features/friend/FriendMatchHubPage.tsx
  • src/features/play/RankedCategoryBlockingScreen.tsx
  • src/features/possession/components/HalftimeScreen.tsx
  • src/features/possession/hooks/__tests__/usePossessionMatchSounds.test.ts
  • src/features/profile/components/AvatarPicker.tsx
  • src/features/welcome/WelcomeFacebookButton.tsx
  • src/features/welcome/WelcomeGoogleButton.tsx
  • src/features/welcome/useWelcomeAuthController.ts
  • src/lib/api/__tests__/client.test.ts
  • src/lib/api/client.ts
  • src/lib/auth/google-identity.ts
  • src/messages/en.json
  • src/messages/ka.json
  • src/stores/auth.store.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • src/features/welcome/WelcomeFacebookButton.tsx
  • src/components/shared/ModalCloseButton.tsx
  • src/features/possession/hooks/tests/usePossessionMatchSounds.test.ts
  • src/lib/auth/google-identity.ts
  • src/lib/api/client.ts
  • src/features/possession/components/HalftimeScreen.tsx
  • src/stores/auth.store.ts
  • src/features/friend/FriendMatchHubPage.tsx
  • src/features/profile/components/AvatarPicker.tsx
  • src/components/auth/tests/WelcomeScreen.test.tsx
  • src/features/daily/CountdownGame.tsx
  • src/features/welcome/useWelcomeAuthController.ts
  • src/components/auth/OAuthCallbackScreen.tsx

@swoosh1337 swoosh1337 merged commit 23b76b9 into main Jun 5, 2026
3 checks passed
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.

1 participant