ReviewOS

also looking at this

craft-native/craft

fix(native): interaction, audio, document and checkpoint gaps stranded after #401

#415
Merged glennmichael123 wants to merge codex/issues-388-400-344 into main
19 files +987 -50

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/ios/README.mdmodified+1-1
Changes to packages/ios/README.md
@@ -174,7 +174,7 @@ and linear gradients are applied by UIKit with the same style keys as Android;
174174`elevation` remains Android-specific. Gradient layers follow the native view's
175175bounds and corners through layout changes.
176176
177The opt-in `native` renderer hosts compiled `.stx` screens in JavaScriptCore and renders their `View`, `SafeAreaView`, `KeyboardAvoidingView`, `StatusBar`, `RefreshControl`, `Text`, `Button`, `Link`, `Checkbox`, `TextInput`, `Picker`, `SegmentedControl`, `DatePicker`, `Switch`, `Slider`, `ActivityIndicator`, `Icon`, `Image`, `ImageBackground`, `Video`, `Map`, `Svg`, `G`, `SvgText`, `ScrollView`, `SectionList`, and recycling `FlatList` nodes as UIKit controls. `StatusBar` and `RefreshControl` are screen/scroll utility nodes rather than visible children. `Map` uses MapKit for trail polylines and marker taps; its overlays carry flat latitude/longitude `geometry` arrays, `strokeWidth` is expressed in logical points, and the camera supports controlled `region`, one-time `initialRegion`, and `fitToBounds`. `Svg` is a DOM-free drawing surface; grouped `G` and `SvgText` nodes are painted inside it. `Icon` uses SF Symbols with the shared symbol/name contract and accessibility metadata. `SectionList` uses the same native scroll host for ordinary `.stx` slot children; grouped section data should use `FlatList` until the section-data protocol is available. `FlatList` uses `UICollectionView` with stable keyed rows, incremental diffs, grids, horizontal and inverted layouts, list chrome, and end-reached pagination. Native views support `onLayout` callbacks with point-based frame measurements that refresh after safe-area or orientation changes. Flex items support `flex`, `flexGrow`, `flexShrink`, and numeric `flexBasis` for main-axis space distribution. Native screens also expose the versioned capability bridge: JSON storage and SQLite survive relaunch, lifecycle and deep-link events are scoped to the top route, and local notifications use `UNUserNotificationCenter`. Enable each capability in `craft.config.json`; disabled calls preserve Craft's `CAPABILITY_DISABLED` rejection code and every request has deterministic timeout/cancellation behavior. The default remains `web`; existing iOS apps are unchanged. `Checkbox` supports controlled `checked`/`value`, indeterminate state, and boolean `onValueChange`; `Picker` renders `<select>`/`<option>` values with `onValueChange` or `onChange`; `DatePicker` emits local date/time values plus ISO and millisecond `timestamp` fields; `Switch` supports state-aware `trackColor`, `thumbColor`, and iOS `ios_backgroundColor`; `Slider` supports track/thumb tint props and `step` snapping; `Image` reports load lifecycle events and structured failures; `Video` uses an `AVPlayerViewController` attached to the native screen for URI playback, native controls, and route-safe teardown. `craft.navigation.setOptions` also accepts a `tabAccessory` destination beside the tab pill; it uses an app-local `path`, `title`, `symbol`, optional `badge`, and `accessibilityLabel`, and `null` clears it. Dynamic `craft.shortcuts.set`/`clear` calls on an all-native screen resolve `false` (the typed `unsupported` result); configure static `appShortcuts` in `craft.config.json` when the app shell should publish home-screen quick actions. The portable component, capability, list, style, image-source, event, accessibility, and reconciliation behavior is specified in the [Native STX guide](../../docs/guides/native-stx.md).
177The opt-in `native` renderer hosts compiled `.stx` screens in JavaScriptCore and renders their `View`, `SafeAreaView`, `KeyboardAvoidingView`, `StatusBar`, `RefreshControl`, `Text`, `Button`, `Link`, `Checkbox`, `TextInput`, `Picker`, `SegmentedControl`, `DatePicker`, `Switch`, `Slider`, `ActivityIndicator`, `Icon`, `Image`, `ImageBackground`, `Video`, `Map`, `Svg`, `G`, `SvgText`, `ScrollView`, `SectionList`, and recycling `FlatList` nodes as UIKit controls. `StatusBar` and `RefreshControl` are screen/scroll utility nodes rather than visible children. `Map` uses MapKit for trail polylines and marker taps; its overlays carry flat latitude/longitude `geometry` arrays, `strokeWidth` is expressed in logical points, and the camera supports controlled `region`, one-time `initialRegion`, and `fitToBounds`. `Svg` is a DOM-free drawing surface; grouped `G` and `SvgText` nodes are painted inside it. `Icon` uses SF Symbols with the shared symbol/name contract and accessibility metadata. `SectionList` uses the same native scroll host for ordinary `.stx` slot children; grouped section data should use `FlatList` until the section-data protocol is available. `FlatList` uses `UICollectionView` with stable keyed rows, incremental diffs, grids, horizontal and inverted layouts, list chrome, and end-reached pagination. Native views support `onLayout` callbacks with point-based frame measurements that refresh after safe-area or orientation changes. Flex items support `flex`, `flexGrow`, `flexShrink`, and numeric `flexBasis` for main-axis space distribution. Native screens also expose the versioned capability bridge: JSON storage and SQLite survive relaunch, lifecycle and deep-link events are scoped to the top route, and local notifications use `UNUserNotificationCenter`. Enable each capability in `craft.config.json`; disabled calls preserve Craft's `CAPABILITY_DISABLED` rejection code and every request has deterministic timeout/cancellation behavior. The default remains `web`; existing iOS apps are unchanged. `Checkbox` supports controlled `checked`/`value`, indeterminate state, and boolean `onValueChange`; `Picker` renders `<select>`/`<option>` values with `onValueChange` or `onChange`; `DatePicker` emits local date/time values plus ISO and millisecond `timestamp` fields; `Switch` supports state-aware `trackColor`, `thumbColor`, and iOS `ios_backgroundColor`; `Slider` supports track/thumb tint props and `step` snapping; `Image` reports load lifecycle events and structured failures; `Video` uses an `AVPlayerViewController` attached to the native screen for URI playback, native controls, and route-safe teardown. `craft.navigation.setOptions` also accepts a `tabAccessory` destination beside the tab pill; it uses an app-local `path`, `title`, `symbol`, optional `badge`, and `accessibilityLabel`, and `null` clears it. Dynamic `craft.shortcuts.set`/`clear` are not installed by the all-native host yet; configure static `appShortcuts` in `craft.config.json` to publish home-screen quick actions. Runtime shortcut registration remains tracked in #412. The portable component, capability, list, style, image-source, event, accessibility, and reconciliation behavior is specified in the [Native STX guide](../../docs/guides/native-stx.md).
178178
179179`FlatList` rows are authored as keyed `:for` child rows in native STX; the
180180maintained protocol does not serialize `data`/`renderItem` callbacks.
packages/ios/fixtures/native-screen/NativeAudioUnitTests.swiftmodified+55-0
Changes to packages/ios/fixtures/native-screen/NativeAudioUnitTests.swift
@@ -1,4 +1,5 @@
11import JavaScriptCore
2import AVFoundation
23import XCTest
34@testable import NativeRender
45
@@ -7,6 +8,60 @@ import XCTest
78/// samples the engine would play.
89@MainActor
910final class NativeAudioUnitTests: XCTestCase {
11 private func withSound(_ test: (CraftNativeSounds) -> Void) throws {
12 let file = FileManager.default.temporaryDirectory.appendingPathComponent("sound-\(UUID().uuidString).wav")
13 defer { try? FileManager.default.removeItem(at: file) }
14 let format = try XCTUnwrap(AVAudioFormat(standardFormatWithSampleRate: 8000, channels: 1))
15 let buffer = try XCTUnwrap(AVAudioPCMBuffer(pcmFormat: format, frameCapacity: 16000))
16 buffer.frameLength = 16000
17 buffer.floatChannelData?[0].initialize(repeating: 0, count: 16000)
18 do {
19 let output = try AVAudioFile(forWriting: file, settings: format.settings)
20 try output.write(from: buffer)
21 }
22 let service = CraftNativeSounds()
23 defer { service.close() }
24 service.play(id: "tone", url: file, options: ["loop": true]) { _ in }
25 test(service)
26 }
27
28 private func interrupt(_ type: AVAudioSession.InterruptionType, resume: Bool = false) {
29 NotificationCenter.default.post(name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance(), userInfo: [
30 AVAudioSessionInterruptionTypeKey: NSNumber(value: type.rawValue),
31 AVAudioSessionInterruptionOptionKey: NSNumber(value: resume ? AVAudioSession.InterruptionOptions.shouldResume.rawValue : 0),
32 ])
33 }
34
35 func testSoundWithoutAutomaticResumePermissionCanBeResumedExplicitly() throws {
36 try withSound { service in
37 interrupt(.began)
38 interrupt(.ended)
39 // Another unrelated interruption must not auto-resume this handle.
40 interrupt(.began)
41 interrupt(.ended, resume: true)
42 XCTAssertTrue(service.resume(id: "tone"))
43 XCTAssertFalse(service.resume(id: "tone"))
44 }
45 }
46
47 func testExplicitSoundPauseSurvivesInterruptionResumePermission() throws {
48 try withSound { service in
49 interrupt(.began)
50 XCTAssertTrue(service.pause(id: "tone"))
51 interrupt(.ended, resume: true)
52 XCTAssertTrue(service.resume(id: "tone"))
53 }
54 }
55
56 func testSoundAutomaticallyResumesOnlyWithSystemPermission() throws {
57 try withSound { service in
58 interrupt(.began)
59 interrupt(.ended, resume: true)
60 XCTAssertFalse(service.resume(id: "tone"), "already resumed automatically")
61 XCTAssertFalse(service.resume(id: "missing"))
62 }
63 }
64
1065 private final class VoiceRecorder: CraftVoiceRecorder {
1166 private let lock = NSLock()
1267 private var records = 0
packages/ios/templates/CraftNativeServices.swiftmodified+5-2
Changes to packages/ios/templates/CraftNativeServices.swift
@@ -329,11 +329,14 @@ final class CraftNativeSounds {
329329 }
330330 guard type == AVAudioSession.InterruptionType.ended.rawValue else { return }
331331 let options = (note.userInfo?[AVAudioSessionInterruptionOptionKey] as? NSNumber)?.uintValue ?? 0
332 guard options & AVAudioSession.InterruptionOptions.shouldResume.rawValue != 0 else { return }
332 let shouldResume = options & AVAudioSession.InterruptionOptions.shouldResume.rawValue != 0
333333 self.sounds.values.forEach { sound in
334334 guard sound.interrupted else { return }
335335 sound.interrupted = false
336 sound.player.play()
336 // Without permission to auto-resume, keep a resumable paused
337 // handle. A later unrelated interruption must not restart it.
338 sound.paused = !shouldResume
339 if shouldResume { sound.player.play() }
337340 }
338341 }
339342 }