diff --git a/CHANGELOG.md b/CHANGELOG.md index 32cf9f0..bbcddf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ full engineering record. ## [Unreleased] +## [0.3.38] — 2026-08-01 + +- Kept Farcaster's native Back control visible inside the Realm, including for + returning keepers who enter Genesis 001 directly from the Mini App. +- Made root Back return to Warpkeep's menu without signing out, while nested + Realm records still close one step at a time. +- Left the ordinary browser experience, authentication, admission, Terms, + ownership, resources, and persistent world state unchanged. + ## [0.3.37] — 2026-08-01 - Latched Request Access at the button boundary before the parent state round @@ -409,7 +418,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.37...HEAD +[Unreleased]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.38...HEAD +[0.3.38]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.37...v0.3.38 [0.3.37]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.36...v0.3.37 [0.3.36]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.35...v0.3.36 [0.3.35]: https://github.com/ael-dev3/Warpkeep/compare/v0.3.34...v0.3.35 diff --git a/docs/farcaster-integration.md b/docs/farcaster-integration.md index 13e3069..aaec3d6 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.37 keeps backend protocol 3 and authentication contract v2; admission +Alpha 0.3.38 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. diff --git a/docs/releases/versioning.md b/docs/releases/versioning.md index 3582b20..9864b83 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.37` is the live **The Gate Heeds Once** release. A version is +Alpha `0.3.38` is the live **The Way Back** 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 1e3e55b..147f7c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "warpkeep", - "version": "0.3.37", + "version": "0.3.38", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "warpkeep", - "version": "0.3.37", + "version": "0.3.38", "license": "Apache-2.0", "dependencies": { "@farcaster/auth-client": "0.7.1", diff --git a/package.json b/package.json index 91b3c82..6ab6161 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "warpkeep", "private": true, - "version": "0.3.37", + "version": "0.3.38", "description": "A Farcaster-connected persistent strategy world in active Alpha development.", "license": "Apache-2.0", "homepage": "https://warpkeep.com", diff --git a/src/components/menu/latestPatchNotes.ts b/src/components/menu/latestPatchNotes.ts index bb5e6ba..1d29dbc 100644 --- a/src/components/menu/latestPatchNotes.ts +++ b/src/components/menu/latestPatchNotes.ts @@ -6,6 +6,20 @@ export type LatestPatchNotes = Readonly<{ alphaNotice: string; }>; +const ALPHA_0_3_38_PATCH_NOTES: LatestPatchNotes = Object.freeze({ + releasedOn: '1 AUG 2026', + title: 'THE WAY BACK', + summary: + 'The Realm now keeps one clear road back to Warpkeep’s gates, even when Farcaster carries a keeper straight inside.', + highlights: Object.freeze([ + 'Farcaster’s native Back control now remains available at the Realm root for keepers who enter Genesis 001 directly.', + 'Back closes nested Realm records one step at a time, then returns to Warpkeep’s menu without signing the keeper out.', + 'The route adds no new map overlay and leaves authentication, admission, Terms, keeps, Workers, resources, and world state untouched.' + ]), + alphaNotice: + 'Alpha 0.3.38 remains an invite-only, unfinished world. This navigation release changes no admission decision, ownership record, balance, or persistent Realm data.' +}); + const ALPHA_0_3_37_PATCH_NOTES: LatestPatchNotes = Object.freeze({ releasedOn: '1 AUG 2026', title: 'THE GATE HEEDS ONCE', @@ -289,6 +303,7 @@ const ALPHA_0_3_22_PATCH_NOTES: LatestPatchNotes = Object.freeze({ export const WARPKEEP_PATCH_NOTES_BY_VERSION: Readonly> = Object.freeze({ + '0.3.38': ALPHA_0_3_38_PATCH_NOTES, '0.3.37': ALPHA_0_3_37_PATCH_NOTES, '0.3.36': ALPHA_0_3_36_PATCH_NOTES, '0.3.35': ALPHA_0_3_35_PATCH_NOTES, diff --git a/src/components/realm/RealmMapScreen.tsx b/src/components/realm/RealmMapScreen.tsx index dba5b8b..67701c6 100644 --- a/src/components/realm/RealmMapScreen.tsx +++ b/src/components/realm/RealmMapScreen.tsx @@ -545,11 +545,16 @@ function CanonicalRealmMapScreen(props: RealmMapScreenProps) { if (fullscreenDestinations) pushSurface(route); }, [fullscreenDestinations, pushSurface]); const handleMiniAppBack = useCallback(() => { - if (surfaceNavigation.depth > 0) backSurface(); - }, [backSurface, surfaceNavigation.depth]); - // The host owns Back at the Realm root. Only nested destinations bind an - // application handler; consuming root Back here would prevent native exit. - useMiniAppBackNavigation(surfaceNavigation.depth, handleMiniAppBack); + if (surfaceNavigation.depth > 0) { + backSurface(); + return; + } + onRequestReturn(); + }, [backSurface, onRequestReturn, surfaceNavigation.depth]); + // Direct-entry Mini Apps skip Warpkeep's menu, so keep the native host Back + // control available at the Realm root as their explicit route to it. Nested + // destinations still consume one navigation step before the root can leave. + useMiniAppBackNavigation(surfaceNavigation.depth + 1, handleMiniAppBack); const workerProjectionTelemetryEnabled = import.meta.env.DEV ? props.localQaWorkerProjectionTelemetry === true : false; diff --git a/tests/WarpkeepExperienceRealm.test.tsx b/tests/WarpkeepExperienceRealm.test.tsx index c576a9d..2857454 100644 --- a/tests/WarpkeepExperienceRealm.test.tsx +++ b/tests/WarpkeepExperienceRealm.test.tsx @@ -611,16 +611,18 @@ describe('Warpkeep Farcaster Mini App direct entry', () => { expect(authority.beginSignIn).not.toHaveBeenCalled(); expect(encodeQrCode).not.toHaveBeenCalled(); expect(backend.runtime.acceptAlphaTerms).not.toHaveBeenCalled(); + await settle(); + expect(sdk.back?.show).toHaveBeenCalled(); + expect(sdk.back?.onback).not.toBeNull(); - const traverseHistory = vi.spyOn(window.history, 'go'); - returnToMainMenuThroughPlayerProfile(); + act(() => sdk.back?.onback?.()); await act(async () => vi.advanceTimersByTime(20)); await settle(); - expect(traverseHistory).toHaveBeenCalledWith(-1); expect(container.querySelector('.warpkeep-experience')?.getAttribute('data-phase')).toBe('menu'); expect(window.location.hash).toBe('#menu'); expect(window.history.state?.warpkeepDirectRealm).toBeUndefined(); expect(screen.getByRole('button', { name: 'ENTER REALM' })).not.toBeNull(); + expect(bridge.logoutSession).not.toHaveBeenCalled(); act(() => window.history.back()); await act(async () => vi.advanceTimersByTime(20)); diff --git a/tests/buildInfo.test.ts b/tests/buildInfo.test.ts index 5ae09d0..45a9564 100644 --- a/tests/buildInfo.test.ts +++ b/tests/buildInfo.test.ts @@ -24,7 +24,7 @@ describe('Warpkeep build identity', () => { ); }); - it('reads the 0.3.37 product version from package metadata', () => { + it('reads the 0.3.38 product version from package metadata', () => { const packageJson = JSON.parse( readFileSync(resolve(process.cwd(), 'package.json'), 'utf8') ) as { version?: unknown }; @@ -32,7 +32,7 @@ describe('Warpkeep build identity', () => { readFileSync(resolve(process.cwd(), 'package-lock.json'), 'utf8') ) as { version?: unknown; packages?: Record }; - expect(readWarpkeepProductVersion(packageJson.version)).toBe('0.3.37'); + expect(readWarpkeepProductVersion(packageJson.version)).toBe('0.3.38'); expect(packageLock.version).toBe(packageJson.version); expect(packageLock.packages?.['']?.version).toBe(packageJson.version); expect(readWarpkeepProductVersion('1.0.0-alpha.1+build.7')).toBe('1.0.0-alpha.1+build.7'); diff --git a/tests/latestPatchNotes.test.ts b/tests/latestPatchNotes.test.ts index ffd4bce..7c6e683 100644 --- a/tests/latestPatchNotes.test.ts +++ b/tests/latestPatchNotes.test.ts @@ -18,26 +18,30 @@ describe('latest in-menu patch notes', () => { expect(Object.keys(WARPKEEP_PATCH_NOTES_BY_VERSION)).toContain(packageJson.version); expect(getLatestPatchNotes(packageJson.version)).toMatchObject({ releasedOn: '1 AUG 2026', - title: 'THE GATE HEEDS ONCE' + title: 'THE WAY BACK' }); expect(getLatestPatchNotes(packageJson.version)?.highlights.join(' ')).toMatch( - /Request Access.*Request Sent.*first gesture.*network round trip.*second tap/i + /native Back control.*Realm root.*Genesis 001 directly/i ); expect(getLatestPatchNotes(packageJson.version)?.highlights.join(' ')).toMatch( - /gold strike.*violet water-like echo.*exact admission resonance.*first accepted gesture/i + /nested Realm records.*one step.*menu.*without signing.*out/i ); expect(getLatestPatchNotes(packageJson.version)?.highlights.join(' ')).toMatch( - /SpacetimeDB.*Request Received.*closed across refreshes.*status-first retry/i + /no new map overlay.*authentication.*admission.*Terms.*world state untouched/i ); expect(getLatestPatchNotes(packageJson.version)?.highlights.join(' ')).not.toMatch( /released to players|deployed to players|public balances|guaranteed rewards/i ); - expect(getLatestPatchNotes(packageJson.version)?.summary).toContain('petition'); + expect(getLatestPatchNotes(packageJson.version)?.summary).toContain('road back'); expect(getLatestPatchNotes(packageJson.version)?.alphaNotice).toContain('unfinished'); expect(getLatestPatchNotes(packageJson.version)?.alphaNotice).toContain( - 'does not grant admission' + 'changes no admission decision' ); + expect(getLatestPatchNotes('0.3.37')).toMatchObject({ + title: 'THE GATE HEEDS ONCE' + }); + expect(getLatestPatchNotes('0.3.36')).toMatchObject({ title: 'THE GATE RESOUNDS' }); diff --git a/tests/menuFarcasterAuthIntegration.test.tsx b/tests/menuFarcasterAuthIntegration.test.tsx index 9e201c7..f83a50b 100644 --- a/tests/menuFarcasterAuthIntegration.test.tsx +++ b/tests/menuFarcasterAuthIntegration.test.tsx @@ -1112,7 +1112,7 @@ describe('WarpkeepMainMenu Farcaster authentication integration', () => { 'CREDITS' ]); expect(screen.getByRole('button', { - name: 'Open patch notes for Warpkeep ALPHA 0.3.37' + name: 'Open patch notes for Warpkeep ALPHA 0.3.38' })).not.toBeNull(); expect(screen.queryByRole('button', { name: 'CONTINUE' })).toBeNull(); diff --git a/tests/menuMainMenu.test.tsx b/tests/menuMainMenu.test.tsx index 416c5b6..0b864ca 100644 --- a/tests/menuMainMenu.test.tsx +++ b/tests/menuMainMenu.test.tsx @@ -22,7 +22,7 @@ function installMotionPreference(matches = false) { function getPatchNotesTrigger(options: { hidden?: boolean } = {}) { return screen.getByRole('button', { ...options, - name: 'Open patch notes for Warpkeep ALPHA 0.3.37' + name: 'Open patch notes for Warpkeep ALPHA 0.3.38' }); } @@ -131,11 +131,11 @@ describe('WarpkeepMainMenu', () => { act(() => patchNotes.focus()); expect(screen.getByRole('status').textContent).toContain('living frontier'); - expect(screen.queryByRole('region', { name: 'THE GATE HEEDS ONCE' })).toBeNull(); + expect(screen.queryByRole('region', { name: 'THE WAY BACK' })).toBeNull(); fireEvent.click(patchNotes, { detail: 0 }); expect(screen.queryByRole('status')).toBeNull(); - expect(screen.getByRole('region', { name: 'THE GATE HEEDS ONCE' })).not.toBeNull(); + expect(screen.getByRole('region', { name: 'THE WAY BACK' })).not.toBeNull(); expect(document.activeElement).toBe(patchNotes); }); @@ -239,25 +239,25 @@ describe('WarpkeepMainMenu', () => { expect(patchNotes.getAttribute('aria-expanded')).toBe('false'); expect(patchNotes.getAttribute('aria-controls')).toBe('warpkeep-latest-patch-notes'); - expect(screen.queryByRole('region', { name: 'THE GATE HEEDS ONCE' })).toBeNull(); + expect(screen.queryByRole('region', { name: 'THE WAY BACK' })).toBeNull(); act(() => patchNotes.focus()); - expect(screen.queryByRole('region', { name: 'THE GATE HEEDS ONCE' })).toBeNull(); + expect(screen.queryByRole('region', { name: 'THE WAY BACK' })).toBeNull(); fireEvent.click(patchNotes, { detail: 0 }); - const notes = screen.getByRole('region', { name: 'THE GATE HEEDS ONCE' }); + const notes = screen.getByRole('region', { name: 'THE WAY BACK' }); expect(patchNotes.getAttribute('aria-expanded')).toBe('true'); - expect(notes.textContent).toContain('LATEST PATCH · ALPHA 0.3.37'); - expect(notes.textContent).toContain('before any network round trip'); - expect(notes.textContent).toContain('violet water-like echo'); - expect(notes.textContent).toContain('closed across refreshes'); + expect(notes.textContent).toContain('LATEST PATCH · ALPHA 0.3.38'); + expect(notes.textContent).toContain('native Back control'); + expect(notes.textContent).toContain('without signing the keeper out'); + expect(notes.textContent).toContain('no new map overlay'); expect(notes.getAttribute('tabindex')).toBe('0'); expect(within(notes).queryByRole('link')).toBeNull(); act(() => notes.focus()); expect(document.activeElement).toBe(notes); fireEvent.keyDown(document, { key: 'Escape' }); - expect(screen.queryByRole('region', { name: 'THE GATE HEEDS ONCE' })).toBeNull(); + expect(screen.queryByRole('region', { name: 'THE WAY BACK' })).toBeNull(); expect(document.activeElement).toBe(patchNotes); expect(onRequestReturn).not.toHaveBeenCalled(); @@ -270,19 +270,19 @@ describe('WarpkeepMainMenu', () => { const patchNotes = getPatchNotesTrigger(); fireEvent.pointerEnter(patchNotes, { pointerType: 'mouse' }); - expect(screen.getByRole('region', { name: 'THE GATE HEEDS ONCE' })).not.toBeNull(); + expect(screen.getByRole('region', { name: 'THE WAY BACK' })).not.toBeNull(); fireEvent.pointerDown(document.body, { pointerType: 'mouse' }); - expect(screen.queryByRole('region', { name: 'THE GATE HEEDS ONCE' })).toBeNull(); + expect(screen.queryByRole('region', { name: 'THE WAY BACK' })).toBeNull(); fireEvent.pointerDown(patchNotes, { pointerType: 'touch' }); patchNotes.focus(); fireEvent.click(patchNotes); - expect(screen.getByRole('region', { name: 'THE GATE HEEDS ONCE' })).not.toBeNull(); + expect(screen.getByRole('region', { name: 'THE WAY BACK' })).not.toBeNull(); fireEvent.pointerDown(patchNotes, { pointerType: 'touch' }); fireEvent.click(patchNotes); - expect(screen.queryByRole('region', { name: 'THE GATE HEEDS ONCE' })).toBeNull(); + expect(screen.queryByRole('region', { name: 'THE WAY BACK' })).toBeNull(); }); it('keeps hover notes reachable across the anchor gap and toggles by activation', () => { @@ -291,21 +291,21 @@ describe('WarpkeepMainMenu', () => { const patchNotes = getPatchNotesTrigger(); fireEvent.pointerEnter(patchNotes, { pointerType: 'mouse' }); - expect(screen.getByRole('region', { name: 'THE GATE HEEDS ONCE' })).not.toBeNull(); + expect(screen.getByRole('region', { name: 'THE WAY BACK' })).not.toBeNull(); fireEvent.pointerLeave(patchNotes, { pointerType: 'mouse' }); act(() => vi.advanceTimersByTime(250)); - const panel = screen.getByRole('region', { name: 'THE GATE HEEDS ONCE' }); + const panel = screen.getByRole('region', { name: 'THE WAY BACK' }); fireEvent.pointerEnter(panel, { pointerType: 'mouse' }); act(() => vi.advanceTimersByTime(200)); - expect(screen.getByRole('region', { name: 'THE GATE HEEDS ONCE' })).not.toBeNull(); + expect(screen.getByRole('region', { name: 'THE WAY BACK' })).not.toBeNull(); fireEvent.click(patchNotes); fireEvent.pointerLeave(patchNotes, { pointerType: 'mouse' }); act(() => vi.advanceTimersByTime(500)); - expect(screen.getByRole('region', { name: 'THE GATE HEEDS ONCE' })).not.toBeNull(); + expect(screen.getByRole('region', { name: 'THE WAY BACK' })).not.toBeNull(); fireEvent.click(patchNotes); - expect(screen.queryByRole('region', { name: 'THE GATE HEEDS ONCE' })).toBeNull(); + expect(screen.queryByRole('region', { name: 'THE WAY BACK' })).toBeNull(); }); it('keeps inactive menu controls hidden, inert, and outside the tab order', () => { diff --git a/tests/realmQualityRecreation.test.tsx b/tests/realmQualityRecreation.test.tsx index 3baeec9..47a6b6c 100644 --- a/tests/realmQualityRecreation.test.tsx +++ b/tests/realmQualityRecreation.test.tsx @@ -626,7 +626,7 @@ afterEach(() => { }); describe('live realm quality recreation', () => { - it('leaves root Back to the Mini App host and binds it only for nested Realm destinations', async () => { + it('keeps native Mini App Back available from the Realm root through nested destinations', async () => { installWebGlProbe(); const back = { show: vi.fn(async () => {}), @@ -681,16 +681,14 @@ describe('live realm quality recreation', () => { ); - await waitFor(() => expect(back.hide).toHaveBeenCalled()); + await waitFor(() => expect(back.show).toHaveBeenCalledOnce()); const realm = screen.getByRole('main', { name: 'Hegemony realm' }); await waitFor(() => expect(realm.dataset.realmChromeMode).toBe('miniapp')); const sceneOptions = mocked.createRealmScene.mock.calls.at(-1)?.[0]; act(() => sceneOptions?.onCastlesReady?.(2)); - expect(back.show).not.toHaveBeenCalled(); - expect(back.onback).toBeNull(); + expect(back.onback).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: /Open Realm menu/i })); - await waitFor(() => expect(back.show).toHaveBeenCalledOnce()); let menu = screen.getByRole('region', { name: 'REALM MENU' }); fireEvent.click(within(menu).getByRole('button', { name: /EXPLORE/i })); @@ -716,7 +714,7 @@ describe('live realm quality recreation', () => { act(() => back.onback?.()); await waitFor(() => { expect(screen.queryByRole('region', { name: 'REALM MENU' })).toBeNull(); - expect(back.onback).toBeNull(); + expect(back.onback).not.toBeNull(); }); expect(onRequestReturn).not.toHaveBeenCalled(); @@ -782,6 +780,9 @@ describe('live realm quality recreation', () => { expect(screen.queryByRole('button', { name: 'LOCATE IN REALM' })).toBeNull(); expect(document.activeElement).toBe(realm); }); + + act(() => back.onback?.()); + expect(onRequestReturn).toHaveBeenCalledOnce(); }); it('retires an assigned scene when initial interaction synchronization throws', () => {