diff --git a/CHANGELOG.md b/CHANGELOG.md index 63ba679f..1ac49204 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ full engineering record. ## [Unreleased] +## [0.3.30] — 2026-07-31 + +- Let returning Farcaster Mini App players enter Genesis 001 directly when + Quick Auth, admission, current Terms acceptance, and their canonical keep all + agree. +- Kept players awaiting admission on the private Request Access step, and + required explicit acceptance whenever the versioned Alpha Terms change. +- Removed the title and ordinary menu from the successful Mini App launch path + without treating a query, host profile, route, or browser state as authority. +- Made missing or malformed Quick Auth credentials settle into a safe retry + instead of an endless loading state, and let native Back dismiss the nested + Terms dialog without leaving Warpkeep. +- Left ordinary browser entry, admission decisions, castle ownership, Workers, + resources, balances, terrain, and persistent world data unchanged. + ## [0.3.29] — 2026-07-31 - Made Realm gestures recover cleanly when a menu, record, or inactive @@ -325,7 +340,8 @@ full engineering record. Lowlands, a first keep, Farcaster sign-in, and an admission-gated shared-world foundation. -[Unreleased]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.29...HEAD +[Unreleased]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.30...HEAD +[0.3.30]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.29...v0.3.30 [0.3.29]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.28...v0.3.29 [0.3.28]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.27...v0.3.28 [0.3.27]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.26...v0.3.27 diff --git a/README.md b/README.md index d8c2e2f5..172ae401 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Genesis 001 is a persistent, invite-only 10,000-cell Lowlands realm with 100 permanent castle sites gathered near its founding district. Each admitted founder enters through a verified Farcaster identity, receives one durable keep, and returns to a world that remembers its coastline, twelve rivers, wintering north, sunscoured south, shaped forests, resource sites, Workers, wagons, and public keeper portraits. Four permanent Workers can gather Food / Wood / Stone / Gold, settle production automatically, and return home on command. -Alpha 0.3.29: The Realm Holds is live in ordinary browsers and as a portrait-first Farcaster Mini App. Verified players who are still outside the gate can request access for manual review. The intended core strategy loop is not playable yet: Warpkeep is a one-person, open-source experiment—not a finished MMO or financial product. Participation earns no token reward, airdrop, financial return, guaranteed reward, or future value. +Alpha 0.3.30: The Gate Remembers is live in ordinary browsers and as a portrait-first Farcaster Mini App. Returning admitted players with current Alpha Terms enter the Realm directly; verified players still outside the gate can request access for manual review. The intended core strategy loop is not playable yet: Warpkeep is a one-person, open-source experiment—not a finished MMO or financial product. Participation earns no token reward, airdrop, financial return, guaranteed reward, or future value. ![Development preview of Genesis 001 showing the Lowlands and an open Wheat Farm inspection panel.](docs/reference/screenshots/2026-07-22-realm-wheat-farm-preview/warpkeep-realm-wheat-farm-preview-f3b1f7e598c543d6.png) @@ -29,7 +29,7 @@ Open the local URL Vite prints; shared Alpha access stays off by default. Contri | State | Today | | --- | --- | -| ✅ Live | Alpha 0.3.29: The Realm Holds is live and invite-only in browsers and the Farcaster Mini App. | +| ✅ Live | Alpha 0.3.30: The Gate Remembers is live and invite-only in browsers and the Farcaster Mini App. | | ✅ World | Genesis 001 persists 10,000 cells, a coastline, twelve one-cell rivers, colder northern and drier southern frontiers, and 100 permanent castle sites near the founding district. Founders return to one durable keep, explore the Lowlands up to its fog, and inspect nearby founders through their public username / portrait / castle. | | ✅ Authority | FID is the durable identity; handles and portraits are bounded presentation metadata. Farcaster sign-in uses a browser-bound, least-privilege bridge. The browser presents; the server decides admission, ownership, resources, timers, and saved state. A private access request asks for manual review and grants nothing by itself. | | ✅ Resources | Each keep privately holds Food / Wood / Stone / Gold. Food, Wood, and Stone come from authoritative terrain yield and can also be gathered at Wheat Farms, Logging Camps, and Stone Quarries; Gold comes from Gold Mines. Completed yield settles without a Claim step, and occupied sites keep their gathering story in one record. The browser never invents balances. | @@ -54,7 +54,7 @@ Open the local URL Vite prints; shared Alpha access stays off by default. Contri - **Architecture:** The [technical architecture](docs/technical-architecture.md) explains what the browser shows and what the server decides. - **Roadmap:** The [roadmap](docs/design/roadmap.md) and [game direction](docs/design/warpkeep-direction.md) separate today's game from later plans. - **Authentication:** The [Farcaster integration](docs/farcaster-integration.md) guide covers sign-in, privacy, and public configuration. -- **Release:** The [Alpha 0.3.29 release notes](CHANGELOG.md#0329--2026-07-31) describe The Realm Holds. +- **Release:** The [Alpha 0.3.30 release notes](CHANGELOG.md#0330--2026-07-31) describe The Gate Remembers. - **Licensing:** [LICENSING.md](LICENSING.md) explains release rules; [asset provenance](ASSETS-LICENSE.md) records where media came from and what permissions apply. - **Contributing:** [CONTRIBUTING.md](CONTRIBUTING.md) covers checks and provenance; the [Realm Council issue forms](https://github.com/ael-dev3/Warpkeep/issues/new/choose) accept privacy-safe bugs and ideas. - **Security:** Report sensitive issues privately through [SECURITY.md](SECURITY.md), never through a public issue. diff --git a/docs/farcaster-integration.md b/docs/farcaster-integration.md index ae2ca49f..8929e524 100644 --- a/docs/farcaster-integration.md +++ b/docs/farcaster-integration.md @@ -5,7 +5,7 @@ ordinary browsers use Sign In with Farcaster (SIWF), while a verified Farcaster Mini App host may use Quick Auth. Neither path is a wallet connection, client-owned identity, admission grant, or Terms acceptance. -Alpha 0.3.29 keeps backend protocol 3 and authentication contract v2; admission +Alpha 0.3.30 keeps backend protocol 3 and authentication contract v2; admission remains gated. Production configuration and founder identities belong in the private operator record, not this guide. @@ -33,8 +33,11 @@ verified Mini App host -> bridge verifies signature, issuer, expiry, and domain warpkeep.com -> verified numeric sub becomes the candidate FID -> the same admission and auth-epoch resolver decides access + -> pending or disabled: Request Access remains visible, with no access token or Realm connection -> authorized: one short-lived memory-only SpacetimeDB access token - -> pending or disabled: no access token and no private Realm authority + -> SpacetimeDB confirms the exact current versioned Terms record for that FID + -> current Terms plus the matching canonical keep: enter the Realm directly + -> missing current Terms: require one explicit acceptance before Realm activation ``` The bridge, not the browser, establishes `sub: farcaster:`. Its exchange @@ -66,11 +69,17 @@ and never falls back to a local or anonymous database identity. ## Browser flow and privacy -Selecting **ENTER REALM** first checks whether the current authenticated FID has -already recorded the exact required Terms version. That same authorized session -may continue without asking again. A missing or stale record opens the concise +In ordinary browsers, selecting **ENTER REALM** first checks whether the current +authenticated FID has already recorded the exact required Terms version. In a +verified Mini App, the same check runs automatically after Quick Auth: an admitted +player with a matching canonical keep and current acceptance enters the Realm +without crossing the title or ordinary menu. A missing or stale record opens the concise **ALPHA PARTICIPATION TERMS** gate; only explicit agreement begins or completes entry. A checkbox alone has no authority, and Quick Auth never accepts Terms. +Acceptance is an immutable private FID/version record rather than a mutable +browser checkbox. Changing the required version makes every older record +non-current and prompts one new explicit acceptance while preserving the audit +history. Outside a verified Mini App host, title load, anonymous menu load, focus/visibility/pageshow events, ordinary route rendering, and direct `#realm` diff --git a/docs/releases/versioning.md b/docs/releases/versioning.md index 3884ccc1..9708e3f8 100644 --- a/docs/releases/versioning.md +++ b/docs/releases/versioning.md @@ -1,7 +1,7 @@ # Versioning and releases Warpkeep uses semantic versions for the product and Git commit SHAs for builds. -Alpha `0.3.29` is the live **The Realm Holds** release. A version is +Alpha `0.3.30` is the live **The Gate Remembers** release. A version is described as live only after its protected review, matching client deployment, and production smoke test all succeed. diff --git a/package-lock.json b/package-lock.json index b0e0dfb9..acda4b2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "warpkeep", - "version": "0.3.29", + "version": "0.3.30", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "warpkeep", - "version": "0.3.29", + "version": "0.3.30", "license": "Apache-2.0", "dependencies": { "@farcaster/auth-client": "0.7.1", diff --git a/package.json b/package.json index 2271c4f7..9896ddc9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "warpkeep", "private": true, - "version": "0.3.29", + "version": "0.3.30", "description": "A Farcaster-connected persistent strategy world in active Alpha development.", "license": "Apache-2.0", "homepage": "https://warpkeep.com", diff --git a/services/auth-bridge/README.md b/services/auth-bridge/README.md index 893006fa..a6bd7d37 100644 --- a/services/auth-bridge/README.md +++ b/services/auth-bridge/README.md @@ -6,7 +6,7 @@ Warpkeep's SpacetimeDB connection. It is isolated from the static browser app: browser code never receives a signing key, admin secret, Optimism RPC URL, resolver JWT, private Hermes JWT, or Maincloud credential. -Alpha 0.3.29 uses authentication contract v2 and backend protocol 3. The +Alpha 0.3.30 uses authentication contract v2 and backend protocol 3. The checked-in configuration fails closed: `wrangler.toml` keeps `PUBLIC_AUTH_ENABLED = "false"`, while any production enablement is a separate, privately recorded operation. World generation and resource features do not diff --git a/src/components/WarpkeepExperience.tsx b/src/components/WarpkeepExperience.tsx index 1aa88e41..c9d9b1ed 100644 --- a/src/components/WarpkeepExperience.tsx +++ b/src/components/WarpkeepExperience.tsx @@ -68,7 +68,11 @@ import { allowsSpeculativeMenuMediaPreload, type NetworkNavigatorSnapshot } from '../settings/networkPreloadPolicy'; -import { useMiniAppBackNavigation } from '../farcaster/miniapp'; +import { + hasExactMiniAppHint, + useMiniAppBackNavigation, + useMiniAppHost +} from '../farcaster/miniapp'; import { REALM_SURFACE_HISTORY_KEY, readRealmSurfaceHistoryState @@ -78,12 +82,15 @@ import { fallbackGatewayClientCenter, type WarpkeepTitleScreenHandle } from './title/titleScreenTypes'; +import { FarcasterMiniAppEntryGate } from './auth/FarcasterMiniAppEntryGate'; import './WarpkeepExperience.css'; const MENU_HASH = '#menu'; const REALM_HASH = '#realm'; const MENU_HISTORY_KEY = 'warpkeepMenu'; const REALM_HISTORY_KEY = 'warpkeepRealm'; +const DIRECT_REALM_HISTORY_KEY = 'warpkeepDirectRealm'; +const DIRECT_REALM_RETURN_WATCHDOG_MS = 1_500; const TITLE_HINT_DELAY_MS = 5_000; const WarpkeepTitleScreen3D = lazy(async () => { @@ -113,6 +120,7 @@ function SceneModuleFallback({ label }: Readonly<{ label: string }>) { type WarpkeepHistoryState = Record & { [MENU_HISTORY_KEY]?: true; [REALM_HISTORY_KEY]?: true; + [DIRECT_REALM_HISTORY_KEY]?: true; }; function hasMenuHash() { @@ -123,10 +131,17 @@ function hasRealmHash() { return typeof window !== 'undefined' && window.location.hash === REALM_HASH; } +function hasMiniAppLaunchHint() { + return typeof window !== 'undefined' && hasExactMiniAppHint(window.location.search); +} + function initialStablePhase(): WarpkeepStableExperiencePhase { // A hash is never a credential. Even a restored bridge session must first // re-check admission before the authoritative realm may mount. if (hasRealmHash()) return 'menu'; + // The exact Mini App launch URL opens on a lightweight authority gate. The + // host SDK must still verify the surface before any direct entry can occur. + if (hasMiniAppLaunchHint()) return 'menu'; return hasMenuHash() ? 'menu' : 'title'; } @@ -158,11 +173,12 @@ function menuHistoryState() { : {}; const nextState = { ...safeCurrent, [MENU_HISTORY_KEY]: true } as WarpkeepHistoryState; delete nextState[REALM_HISTORY_KEY]; + delete nextState[DIRECT_REALM_HISTORY_KEY]; delete nextState[REALM_SURFACE_HISTORY_KEY]; return nextState; } -function realmHistoryState() { +function realmHistoryState(directMiniAppEntry = false) { const current = window.history.state; const safeCurrent = current && typeof current === 'object' ? current as Record @@ -171,6 +187,11 @@ function realmHistoryState() { ...safeCurrent, [REALM_HISTORY_KEY]: true } as WarpkeepHistoryState; + if (directMiniAppEntry) { + nextState[DIRECT_REALM_HISTORY_KEY] = true; + } else { + delete nextState[DIRECT_REALM_HISTORY_KEY]; + } delete nextState[REALM_SURFACE_HISTORY_KEY]; return nextState; } @@ -253,6 +274,7 @@ export function WarpkeepExperience() { rememberDevice, setRememberDevice } = useFarcasterAuth(); + const miniAppHost = useMiniAppHost(); const backend = useWarpkeepBackend(); const initiallyAuthenticated = farcasterAuthState.phase === 'authenticated' && farcasterAuthState.assurance === 'bridge-oidc-alpha' @@ -272,6 +294,9 @@ export function WarpkeepExperience() { initialPhase ); const [pendingDestination, setPendingDestination] = useState<'realm' | null>(null); + const [directMiniAppEntryEnabled, setDirectMiniAppEntryEnabled] = useState( + hasMiniAppLaunchHint + ); const [inputModality, setInputModality] = useState('unknown'); const [reducedMotion, setReducedMotion] = useState(readReducedMotion); const [graphicsPreference, setGraphicsPreference] = useState(readGraphicsPreference); @@ -297,6 +322,7 @@ export function WarpkeepExperience() { const restoreTitleFocusRef = useRef(false); const blockedInitialRealmRef = useRef(hasRealmHash()); const realmAudioResetTimerRef = useRef(null); + const directRealmReturnCleanupRef = useRef<(() => void) | null>(null); const verifiedIdentityRef = useRef( initiallyAuthenticated ? farcasterAuthState.identity @@ -414,6 +440,7 @@ export function WarpkeepExperience() { if (realmAudioResetTimerRef.current !== null) { window.clearTimeout(realmAudioResetTimerRef.current); } + directRealmReturnCleanupRef.current?.(); }, []); useEffect(() => { @@ -486,6 +513,9 @@ export function WarpkeepExperience() { const titleInteractive = experience.phase === 'title'; const menuInteractive = experience.phase === 'menu' && !returnPreparing; const menuMediaActive = menuMounted; + const miniAppEntryGateActive = menuMounted + && directMiniAppEntryEnabled + && (miniAppHost.state === 'detecting' || miniAppHost.isMiniApp); useEffect(() => { if (!realmMounted) { @@ -588,7 +618,10 @@ export function WarpkeepExperience() { beginMenuTransition(activation, activation.input, true); }, [beginMenuTransition]); - const commitRealmEntry = useCallback((identity: VerifiedFarcasterIdentity) => { + const commitRealmEntry = useCallback(( + identity: VerifiedFarcasterIdentity, + routeMode: 'push' | 'replace' = 'push' + ) => { if ( !backend.sharedAlphaAvailable || phaseRef.current !== 'menu' @@ -610,13 +643,21 @@ export function WarpkeepExperience() { window.clearTimeout(realmAudioResetTimerRef.current); realmAudioResetTimerRef.current = null; } - // Start source preparation and the spatial music handoff inside the - // authenticated player gesture, before the route can unmount the menu. - audioDirectorRef.current?.ensurePlaybackFromGesture(); + // Explicit menu entry can unlock audio inside its player gesture. Direct + // Mini App entry only prepares the source; the first real touch remains + // responsible for satisfying iOS and Android autoplay policy. + if (routeMode === 'push') { + audioDirectorRef.current?.ensurePlaybackFromGesture(); + } audioDirectorRef.current?.prepareScene('realm'); audioDirectorRef.current?.transitionTo('realm'); if (!hasRealmHash()) { - window.history.pushState(realmHistoryState(), '', `${pageUrlWithoutHash()}${REALM_HASH}`); + const nextState = realmHistoryState(routeMode === 'replace'); + if (routeMode === 'replace') { + window.history.replaceState(nextState, '', `${pageUrlWithoutHash()}${REALM_HASH}`); + } else { + window.history.pushState(nextState, '', `${pageUrlWithoutHash()}${REALM_HASH}`); + } } setPresentedScreen('realm'); dispatch({ type: 'request-realm' }); @@ -660,6 +701,38 @@ export function WarpkeepExperience() { commitRealmEntry(verifiedIdentityRef.current); }, [backend.state.phase, commitRealmEntry, pendingDestination]); + useEffect(() => { + if ( + !directMiniAppEntryEnabled + || !miniAppHost.isMiniApp + || phaseRef.current !== 'menu' + || returnPreparingRef.current + || farcasterAuthState.phase !== 'authenticated' + || farcasterAuthState.assurance !== 'bridge-oidc-alpha' + || oidcSession === undefined + || oidcSession.expiresAt <= Date.now() + || backend.state.phase !== 'ready' + || !backend.entryAgreementSatisfied + || backend.state.identity?.fid !== farcasterAuthState.identity.fid + || backend.state.realm?.ownCastle.ownerFid !== farcasterAuthState.identity.fid + || !verifiedIdentityRef.current + ) { + return; + } + + // Quick Auth, admission, current Terms acceptance, and canonical Realm + // readiness have all been proven by their existing authorities. Replace + // the launch entry instead of growing host history with a skipped menu. + commitRealmEntry(verifiedIdentityRef.current, 'replace'); + }, [ + backend.state.phase, + commitRealmEntry, + directMiniAppEntryEnabled, + farcasterAuthState, + miniAppHost.isMiniApp, + oidcSession + ]); + useEffect(() => { if ( pendingDestination !== 'realm' @@ -682,17 +755,26 @@ export function WarpkeepExperience() { ]); const returnRealmToMenu = useCallback(() => { - if (phaseRef.current !== 'realm') { + if ( + phaseRef.current !== 'realm' + || directRealmReturnCleanupRef.current + ) { return; } clearPendingRealmDestination(); + setDirectMiniAppEntryEnabled(false); blurActiveElement(); audioDirectorRef.current?.ensurePlaybackFromGesture(); audioDirectorRef.current?.prepareScene('menu'); audioDirectorRef.current?.transitionTo('menu'); const state = window.history.state as WarpkeepHistoryState | null; - const canReturnThroughHistory = hasRealmHash() && state?.[REALM_HISTORY_KEY] === true; + const canReturnThroughHistory = hasRealmHash() + && state?.[REALM_HISTORY_KEY] === true + && state?.[DIRECT_REALM_HISTORY_KEY] !== true; + const returningFromDirectRealm = hasRealmHash() + && state?.[REALM_HISTORY_KEY] === true + && state?.[DIRECT_REALM_HISTORY_KEY] === true; const surfaceDepth = state && typeof state === 'object' ? readRealmSurfaceHistoryState( state[REALM_SURFACE_HISTORY_KEY] @@ -700,7 +782,90 @@ export function WarpkeepExperience() { : 0; setPresentedScreen('menu'); dispatch({ type: 'return-menu' }); - if (canReturnThroughHistory) { + if (returningFromDirectRealm && surfaceDepth > 0) { + let completed = false; + let retriesRemaining = 1; + let watchdog: number | undefined; + + const teardown = () => { + window.removeEventListener('popstate', handlePopState); + if (watchdog !== undefined) window.clearTimeout(watchdog); + watchdog = undefined; + if (directRealmReturnCleanupRef.current === cancel) { + directRealmReturnCleanupRef.current = null; + } + }; + const finish = () => { + if (completed) return; + completed = true; + teardown(); + window.history.replaceState( + menuHistoryState(), + '', + `${pageUrlWithoutHash()}${MENU_HASH}` + ); + }; + const cancel = () => { + if (completed) return; + completed = true; + teardown(); + }; + const currentSurfaceDepth = () => { + const current = window.history.state; + return current && typeof current === 'object' + ? readRealmSurfaceHistoryState( + (current as WarpkeepHistoryState)[REALM_SURFACE_HISTORY_KEY] + )?.stack.length ?? 0 + : 0; + }; + const armWatchdog = () => { + if (watchdog !== undefined) window.clearTimeout(watchdog); + watchdog = window.setTimeout(() => { + watchdog = undefined; + const remainingDepth = currentSurfaceDepth(); + if (remainingDepth === 0) { + finish(); + return; + } + if (retriesRemaining <= 0) { + // A WebView that refuses traversal still receives a safe menu + // route; subsequent Realm-shaped history is normalized by the + // ordinary route gate rather than reopening gameplay. + finish(); + return; + } + retriesRemaining -= 1; + try { + window.history.go(-remainingDepth); + armWatchdog(); + } catch { + finish(); + } + }, DIRECT_REALM_RETURN_WATCHDOG_MS); + }; + function handlePopState() { + const remainingDepth = currentSurfaceDepth(); + if (remainingDepth === 0) { + finish(); + return; + } + try { + window.history.go(-remainingDepth); + armWatchdog(); + } catch { + finish(); + } + } + + directRealmReturnCleanupRef.current = cancel; + window.addEventListener('popstate', handlePopState); + try { + window.history.go(-surfaceDepth); + armWatchdog(); + } catch { + finish(); + } + } else if (canReturnThroughHistory) { window.history.go(-(surfaceDepth + 1)); } else { window.history.replaceState(menuHistoryState(), '', `${pageUrlWithoutHash()}${MENU_HASH}`); @@ -932,6 +1097,7 @@ export function WarpkeepExperience() { useEffect(() => { const synchronizeHistory = () => { + if (directRealmReturnCleanupRef.current) return; const phase = phaseRef.current; if (hasRealmHash() && phase !== 'realm') { gateAnonymousRealmRoute(); @@ -962,7 +1128,7 @@ export function WarpkeepExperience() { return; } if (phase === 'menu') { - beginTitleTransition('none'); + if (!miniAppEntryGateActive) beginTitleTransition('none'); } else if (phase === 'realm') { // A direct #realm visit has no preceding menu entry. Preserve a useful // in-app route rather than exposing a blank phase on browser Back. @@ -983,6 +1149,7 @@ export function WarpkeepExperience() { beginTitleTransition, cancelPreparedReturn, gateAnonymousRealmRoute, + miniAppEntryGateActive, titleReady ]); @@ -997,10 +1164,18 @@ export function WarpkeepExperience() { // effect. commitRealmEntry has already written #realm, while React has // not yet reduced request-realm. Preserve that exact explicit intent // for the next render instead of mistaking it for a forged deep link. - if (pendingDestination !== 'realm') { + const state = window.history.state as WarpkeepHistoryState | null; + if ( + pendingDestination !== 'realm' + && state?.[DIRECT_REALM_HISTORY_KEY] !== true + ) { gateAnonymousRealmRoute(); } - } else if (!hasMenuHash() && !hasRealmHash()) { + } else if ( + !miniAppEntryGateActive + && !hasMenuHash() + && !hasRealmHash() + ) { entryLockedRef.current = false; beginTitleTransition('none'); } @@ -1020,6 +1195,7 @@ export function WarpkeepExperience() { beginTitleTransition, experience.phase, gateAnonymousRealmRoute, + miniAppEntryGateActive, pendingDestination, returnPreparing, titleReady @@ -1170,7 +1346,7 @@ export function WarpkeepExperience() { > {menuMounted ? ( ) : null} @@ -1235,43 +1411,60 @@ export function WarpkeepExperience() { aria-hidden={!menuInteractive} inert={!menuInteractive ? true : undefined} > - + {miniAppEntryGateActive ? ( + + ) : ( + + )} ) : null} @@ -1398,12 +1591,14 @@ export function WarpkeepExperience() { /> ) : null} - + {miniAppEntryGateActive ? null : ( + + )} diff --git a/src/components/auth/FarcasterAdmissionPanel.tsx b/src/components/auth/FarcasterAdmissionPanel.tsx index 13683c9b..a9dc72cd 100644 --- a/src/components/auth/FarcasterAdmissionPanel.tsx +++ b/src/components/auth/FarcasterAdmissionPanel.tsx @@ -18,11 +18,14 @@ const IDLE_ACCESS_REQUEST: AccessRequestViewState = Object.freeze({ phase: 'idle export type FarcasterAdmissionPanelProps = Readonly<{ phase: Exclude; identity: VerifiedFarcasterIdentity; + autoFocusHeading?: boolean; headingRef?: Ref; primaryActionRef?: Ref; onPresentationReady?: () => void; - onBackToMenu: () => void; + /** Optional outside the ordinary menu flow, including direct Mini App entry. */ + onBackToMenu?: () => void; onCheckAgain: () => void; + onReviewTerms?: () => void; accessRequest?: AccessRequestViewState; onRequestAccess?: () => void; onRetryAccessRequestStatus?: () => void; @@ -86,11 +89,13 @@ const presentationByPhase: Record { + if (!autoFocusHeading) return undefined; const frame = window.requestAnimationFrame(() => { localHeadingRef.current?.focus({ preventScroll: true }); }); return () => window.cancelAnimationFrame(frame); - }, [phase]); + }, [autoFocusHeading, phase]); return (
- Return to Enter Realm and accept the current Alpha Terms and Hegemony Social Contract - before Hegemony records open. + Accept the current Alpha Terms and Hegemony Social Contract before Hegemony records + open.

) : null} @@ -189,6 +195,16 @@ export function FarcasterAdmissionPanel({
+ {awaitingTerms && onReviewTerms ? ( + + ) : null} {denied && onRequestAccess && onRetryAccessRequestStatus ? ( ) : null} - + {onBackToMenu ? ( + + ) : null}