Compare commits

...

1 Commits

Author SHA1 Message Date
Zeyi (Rice) Fan 26b5ff5d75 🐛 fix(ios): Prevent media permission crashes
## Related issue

N/A

## Summary

- Add `NSCameraUsageDescription` and `NSSpeechRecognitionUsageDescription` usage strings (Debug + Release Info.plist) so iOS doesn't crash when the WebView requests camera or speech-recognition access.
- Gate WebKit media capture with `isAllowedMediaCaptureType`, allowing camera, microphone, and cameraAndMicrophone (previously microphone-only) and still only for the pinned app origin.
- Repair duplicate `PrivacyInfo.xcprivacy` object IDs in the Xcode project so the iOS target compiles.

## Test Plan

- Added `AppPrivacyInfoTests.testPrivacyUsageDescriptionsArePresent` asserting the camera, microphone, and speech-recognition usage strings are present and non-empty in the app bundle.
- Built the iOS target (duplicate object IDs previously broke the build) and exercised the camera/mic capture prompt via the WebView.

## Demo

N/A

## Type of change

- [x] Bug fix
- [ ] Feature
- [x] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage

- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

Unit test verifies the required iOS privacy usage strings are present. Manual verification: built the iOS target and confirmed the camera/microphone capture prompt no longer crashes and is granted only for the pinned origin.

## Changelog

[UI] Fix iOS crash when granting camera or voice-dictation permission in the app
2026-07-08 23:57:48 -07:00
5 changed files with 36 additions and 5 deletions
+4 -4
View File
@@ -23,7 +23,7 @@
B10000000000000000000012 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000012 /* SafariView.swift */; };
B1000000000000000000000D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A1000000000000000000000D /* Assets.xcassets */; };
B1000000000000000000000E /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = A1000000000000000000000E /* AppIcon.icon */; };
B10000000000000000000012 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000012 /* PrivacyInfo.xcprivacy */; };
B10000000000000000000013 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000013 /* PrivacyInfo.xcprivacy */; };
B20000000000000000000001 /* ServerURLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000001 /* ServerURLTests.swift */; };
B20000000000000000000002 /* SettingsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000002 /* SettingsStoreTests.swift */; };
B20000000000000000000003 /* WorkspaceURLExpanderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000003 /* WorkspaceURLExpanderTests.swift */; };
@@ -67,7 +67,7 @@
A1000000000000000000000E /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; name = AppIcon.icon; path = "../platform-assets/AppIcon.icon"; sourceTree = "<group>"; };
A1000000000000000000000F /* Info-Debug.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-Debug.plist"; sourceTree = "<group>"; };
A10000000000000000000010 /* Info-Release.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-Release.plist"; sourceTree = "<group>"; };
A10000000000000000000012 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
A10000000000000000000013 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
A20000000000000000000001 /* ServerURLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerURLTests.swift; sourceTree = "<group>"; };
A20000000000000000000002 /* SettingsStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStoreTests.swift; sourceTree = "<group>"; };
A20000000000000000000003 /* WorkspaceURLExpanderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceURLExpanderTests.swift; sourceTree = "<group>"; };
@@ -142,7 +142,7 @@
A10000000000000000000011 /* ChatTerminalBar.swift */,
A10000000000000000000012 /* SafariView.swift */,
A1000000000000000000000D /* Assets.xcassets */,
A10000000000000000000012 /* PrivacyInfo.xcprivacy */,
A10000000000000000000013 /* PrivacyInfo.xcprivacy */,
A1000000000000000000000F /* Info-Debug.plist */,
A10000000000000000000010 /* Info-Release.plist */,
);
@@ -282,7 +282,7 @@
files = (
B1000000000000000000000D /* Assets.xcassets in Resources */,
B1000000000000000000000E /* AppIcon.icon in Resources */,
B10000000000000000000012 /* PrivacyInfo.xcprivacy in Resources */,
B10000000000000000000013 /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
+4
View File
@@ -29,8 +29,12 @@
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>
<key>NSCameraUsageDescription</key>
<string>Omnigent uses the camera when you choose to take a photo for an attachment.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Omnigent uses the microphone for voice dictation in the message composer.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>Omnigent uses speech recognition to turn voice dictation into message text.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchScreen</key>
+4
View File
@@ -24,8 +24,12 @@
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Omnigent uses the camera when you choose to take a photo for an attachment.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Omnigent uses the microphone for voice dictation in the message composer.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>Omnigent uses speech recognition to turn voice dictation into message text.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchScreen</key>
+10 -1
View File
@@ -441,7 +441,7 @@ struct OmnigentWebView: UIViewRepresentable {
type: WKMediaCaptureType,
decisionHandler: @escaping (WKPermissionDecision) -> Void
) {
guard type == .microphone,
guard Self.isAllowedMediaCaptureType(type),
origin.omnigentOrigin == pinnedOrigin,
webView.url?.omnigentOrigin == pinnedOrigin
else {
@@ -451,6 +451,15 @@ struct OmnigentWebView: UIViewRepresentable {
decisionHandler(.grant)
}
private static func isAllowedMediaCaptureType(_ type: WKMediaCaptureType) -> Bool {
switch type {
case .camera, .microphone, .cameraAndMicrophone:
return true
@unknown default:
return false
}
}
private func isTrustedBridgeMessage(_ message: WKScriptMessage) -> Bool {
guard let pinnedOrigin else { return false }
guard message.frameInfo.securityOrigin.omnigentOrigin == pinnedOrigin else { return false }
@@ -27,3 +27,17 @@ final class ServerURLTests: XCTestCase {
}
}
}
final class AppPrivacyInfoTests: XCTestCase {
func testPrivacyUsageDescriptionsArePresent() throws {
for key in [
"NSCameraUsageDescription",
"NSMicrophoneUsageDescription",
"NSSpeechRecognitionUsageDescription",
] {
let value = try XCTUnwrap(Bundle.main.object(forInfoDictionaryKey: key) as? String)
XCTAssertFalse(value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
}
}