Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/farcaster-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion docs/releases/versioning.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
15 changes: 15 additions & 0 deletions src/components/menu/latestPatchNotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -289,6 +303,7 @@ const ALPHA_0_3_22_PATCH_NOTES: LatestPatchNotes = Object.freeze({

export const WARPKEEP_PATCH_NOTES_BY_VERSION: Readonly<Record<string, LatestPatchNotes>> =
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,
Expand Down
15 changes: 10 additions & 5 deletions src/components/realm/RealmMapScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 5 additions & 3 deletions tests/WarpkeepExperienceRealm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
4 changes: 2 additions & 2 deletions tests/buildInfo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ 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 };
const packageLock = JSON.parse(
readFileSync(resolve(process.cwd(), 'package-lock.json'), 'utf8')
) as { version?: unknown; packages?: Record<string, { version?: unknown }> };

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');
Expand Down
16 changes: 10 additions & 6 deletions tests/latestPatchNotes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
});
Expand Down
2 changes: 1 addition & 1 deletion tests/menuFarcasterAuthIntegration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
40 changes: 20 additions & 20 deletions tests/menuMainMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
});
}

Expand Down Expand Up @@ -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);
});

Expand Down Expand Up @@ -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();

Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand Down
13 changes: 7 additions & 6 deletions tests/realmQualityRecreation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {}),
Expand Down Expand Up @@ -681,16 +681,14 @@ describe('live realm quality recreation', () => {
</MiniAppHostProvider>
);

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 }));
Expand All @@ -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();

Expand Down Expand Up @@ -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', () => {
Expand Down