From 9337fd8475280d0a5a06babeb4b88dcb77f14b2e Mon Sep 17 00:00:00 2001 From: 0xward <0xward.dev@gmail.com> Date: Sun, 2 Aug 2026 16:20:03 +0000 Subject: [PATCH 1/2] The gate goes first, and it looks like the game MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things the owner found by opening the real thing in a private window, both of them mine. PLACEMENT. The gate was rendered BELOW `phase === 'menu'`, and 'menu' is what draws the world map. Sequential early returns mean order in the file *is* the behaviour: the map won, and the login could not appear until something moved the phase off it — so it turned up AFTER the map instead of before it. Owner: *"harusnya sebelum user membuka maps, jd di awal banget persis setelah user klik Play game di landing page."* The block now sits ahead of every phase, which is what its own comment claimed all along. STYLING. It used `ns-signin-card` and `ns-signin-sub`, which this app does not define anywhere, so it rendered as unstyled text on a black rectangle. Owner: *"halaman login nya jelek banget."* Nothing caught it and nothing could have: tsc does not know about CSS, and check:cssvars only validates custom properties. It now uses the markup and stylesheet SignInScreen already had — the bracket frame, the plate fill, the white Google button, the bevelled clips — and the test suite gained a check that every ns- class the gate names is really declared in signin.css. THE THREE METHODS ARE ON THE SCREEN. One button that opened a modal to ask which method you wanted hid the offer behind a press: a player who wants Google could not see that Google was on offer. Each method is its own button now, and each opens Privy already narrowed to that one method, so the choice is made on a screen that looks like this game and Privy only handles the mechanics. The three marks moved to components/game/SignInMarks.tsx rather than being copied. Two Google marks is two chances for one to be quietly recoloured until the screens diverge — and Google's guidelines make that a compliance problem rather than a matter of taste. Verified: tsc clean, build unchanged at 142/242 kB, 22/22 test:privy, check:copy, check:cssvars, check:market and audit pass, lint 35 (one below the baseline). The screen was rendered in a real browser at 390x844 and the screenshot sent for approval before merge. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017A764RdnwpyWnG7uCNhMiQ --- components/game/GameFlowManager.tsx | 57 +++++++------ components/game/PrivyGate.tsx | 120 +++++++++++++++++++--------- components/game/SignInMarks.tsx | 47 +++++++++++ components/game/SignInScreen.tsx | 35 +------- scripts/test-privy-gate.js | 39 +++++++++ 5 files changed, 202 insertions(+), 96 deletions(-) create mode 100644 components/game/SignInMarks.tsx diff --git a/components/game/GameFlowManager.tsx b/components/game/GameFlowManager.tsx index 8028a3d..4466f40 100644 --- a/components/game/GameFlowManager.tsx +++ b/components/game/GameFlowManager.tsx @@ -701,6 +701,38 @@ export default function GameFlowManager() { setPhase('settings') } + // ── THE PRIVY GATE, AHEAD OF EVERYTHING ─────────────────────────────────── + // + // Above `phase === 'menu'` on purpose, and it was BELOW it in the first + // version — which is the whole bug the owner hit: the map is what 'menu' + // renders, so the gate could not appear until something moved the phase off + // it, and the login turned up AFTER the world map instead of before it. + // + // OWNER: *"harusnya sebelum user membuka maps, jd di awal banget persis + // setelah user klik Play game di landing page."* + // + // Nothing else may render first. This is the first screen of the session. + if (showPrivyGate) { + return ( + { + // Through the SAME adopt() the Firebase screen uses: derive an + // address from the id, carry the guest's progress onto it, store the + // identity. Nothing about Privy is special here — adopt() only ever + // wanted a stable id, and this is one. Without this the screen's own + // promise ("it follows you anywhere") would be false. + try { + await acct.adopt(uid, label) + } finally { + // Even a failed migration must not trap the player on this screen. + setGateDone(true) + } + }} + onSkip={() => { acct.rememberSkipped(); setGateDone(true) }} + /> + ) + } + // PHASE: MENU if (phase === 'menu') { // Same props for both — the hub is a drop-in swap for the classic menu. @@ -742,29 +774,8 @@ export default function GameFlowManager() { } // PHASE: SIGN IN — offered once, before the name, and only to the players - // who have nothing keeping their progress. See shouldOfferSignIn(). - // Ahead of every other phase: this is the first thing after the splash. - if (showPrivyGate) { - return ( - { - // Through the SAME adopt() the Firebase screen uses: derive an - // address from the id, carry the guest's progress onto it, store the - // identity. Nothing about Privy is special here — adopt() only ever - // wanted a stable id, and this is one. Without this the screen's own - // promise ("it follows you anywhere") would be false. - try { - await acct.adopt(uid, label) - } finally { - // Even a failed migration must not trap the player on this screen. - setGateDone(true) - } - }} - onSkip={() => { acct.rememberSkipped(); setGateDone(true) }} - /> - ) - } - + // who have nothing keeping their progress. See shouldOfferSignIn(). Off + // entirely while the Privy gate is on; see goPlaying(). if (phase === 'sign-in') { return ( (null) const adopted = useRef(false) // Privy restores an existing session asynchronously. Someone who already @@ -67,48 +78,79 @@ function GateInner({ onAuthenticated, onSkip }: PrivyGateProps) { ) }, [ready, authenticated, user, onAuthenticated]) - const start = useCallback(() => { - setBusy(true) - try { - // Privy owns the modal from here: Google, email and wallet all live - // inside it, so there is no second choice to present. - login() - } finally { - // The modal is Privy's, so there is no promise to await — clear the - // pending state once it has been asked to open, or a dismissed modal - // would leave the button stuck. - setTimeout(() => setBusy(false), 1200) - } + const start = useCallback((method: Method) => { + setBusy(method) + // Narrowed to the one method the player pressed. Privy's modal would + // otherwise re-ask the question this screen just answered. + login({ loginMethods: [method] }) + // The modal belongs to Privy, so there is no promise to await. Clear the + // pending state shortly after it has been asked to open, or a dismissed + // modal would leave the button stuck reading "Opening…" forever. + setTimeout(() => setBusy(null), 1500) }, [login]) + const disabled = !ready || busy !== null + return (
-
-

// SAVE YOUR PROGRESS

-

KEEP YOUR RUN

-

+

@@ -127,8 +169,8 @@ export default function PrivyGate(props: PrivyGateProps) {