ReviewOS

also looking at this

stacks/stx

fix(native): type tab visibility and back gesture options

#2093
Merged chris wants to merge codex/native-navigation-options into main
3 files +53 -0

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/stx/src/native/runtime/craft.tsmodified+4-0
Changes to packages/stx/src/native/runtime/craft.ts
@@ -47,6 +47,10 @@ export interface NativeNavigationOptions {
4747 title?: string
4848 largeTitle?: boolean
4949 hidden?: boolean
50 /** Hide the app's tab bar while this screen is visible. */
51 tabBarHidden?: boolean
52 /** Allow the iOS interactive back gesture. Defaults to true. */
53 swipeBack?: boolean
5054 backTitle?: string
5155 leftButtons?: NativeNavigationButton[]
5256 rightButtons?: NativeNavigationButton[]
packages/stx/test/native/bridge-protocol.test.tsmodified+19-0
Changes to packages/stx/test/native/bridge-protocol.test.ts
@@ -1,5 +1,7 @@
11import { describe, expect, it } from 'bun:test'
22import { NativeCapabilityError, STXBridge } from '../../src/native/bridge/protocol'
3import { installCraftBridge } from '../../src/native/runtime/craft-bridge'
4import type { NativeNavigationOptions } from '../../src/native/runtime/craft'
35
46function host() {
57 const sent: Array<Record<string, any>> = []
@@ -15,6 +17,23 @@ function host() {
1517}
1618
1719describe('typed native capability bridge', () => {
20 it.each(['ios', 'android'])('preserves screen chrome options and explicit navigation on %s', (platform) => {
21 const native = host()
22 const protocol = new STXBridge()
23 protocol.initialize(native.bridge)
24 const craft = installCraftBridge(protocol, { ...native.bridge, platform }, {})
25 const options: NativeNavigationOptions = { hidden: true, tabBarHidden: true, swipeBack: false }
26 craft.navigation.setOptions(options)
27 craft.navigation.open('/feed')
28 craft.navigation.setOptions({ hidden: false, tabBarHidden: false, swipeBack: true })
29 expect(native.sent.map(({ type, payload }) => ({ type, payload }))).toEqual([
30 { type: 'NAVIGATION_SET_OPTIONS', payload: options },
31 { type: 'NAVIGATE_OPEN', payload: { path: '/feed' } },
32 { type: 'NAVIGATION_SET_OPTIONS', payload: { hidden: false, tabBarHidden: false, swipeBack: true } },
33 ])
34 protocol.dispose()
35 })
36
1837 it('sends protocol v1 and resolves a correlated response', async () => {
1938 const native = host()
2039 const bridge = new STXBridge()
packages/stx/test/typecheck/native-navigation-options.test.tsadded+30-0
Changes to packages/stx/test/typecheck/native-navigation-options.test.ts
@@ -0,0 +1,30 @@
1import { afterAll, expect, test } from 'bun:test'
2import { join } from 'node:path'
3import { typecheckStxFiles } from '../../src/typecheck'
4import { createTempDir } from '../../src/temp-dir'
5import { allowForATypeScriptProgram } from '../../test-utils/checker-timeout'
6
7allowForATypeScriptProgram()
8const dir = createTempDir('stx-native-navigation-types-')
9afterAll(async () => { await Bun.$`rm -rf ${dir}`.quiet() })
10
11test('native screen chrome options accept booleans and reject string values', async () => {
12 const good = join(dir, 'auth.stx')
13 const bad = join(dir, 'invalid.stx')
14 const header = `import type { NativeCraft } from '@stacksjs/stx/native'
15const craft = (globalThis as typeof globalThis & { craft: NativeCraft }).craft`
16 await Bun.write(good, `<script client>
17${header}
18craft.navigation.setOptions({ hidden: true, tabBarHidden: true, swipeBack: false })
19craft.navigation.open('/feed')
20craft.navigation.setOptions({ hidden: false, tabBarHidden: false, swipeBack: true })
21</script>`)
22 await Bun.write(bad, `<script client>
23${header}
24craft.navigation.setOptions({ tabBarHidden: 'true' })
25craft.navigation.setOptions({ swipeBack: 'false' })
26</script>`)
27 const result = await typecheckStxFiles([good, bad], { templates: false })
28 expect(result.diagnostics.filter(d => d.file === good)).toEqual([])
29 expect(result.diagnostics.filter(d => d.file === bad && d.code === 2322)).toHaveLength(2)
30})