/g, tabsBlock => {
+ .replace(/<(Config)?Tabs[\S\s]*?<\/(Config)?Tabs>/g, tabsBlock => {
const codeBlocks = Array.from(tabsBlock.matchAll(/```[\S\s]*?```/g)).map(match => match[0]);
if (codeBlocks.length === 0) {
return '';
@@ -128,6 +128,9 @@ function parseOptionsFromMarkdown(markdown, config) {
.replace(/Example:\s*\n\n[^\n]*\n/g, '')
.replace(/!\[([^\]]*)]\([^)]+\)/g, '')
.replace(/\[([^\]]+)]\([^)]+\)/g, '$1')
+ .replace(/([\S\s]*?)<\/div>/g, (_match, items) =>
+ items.replace(/^([\t ]*)-(?:[\t ]*\n){2}[\t ]*(`[^`]+`)/gm, '$1- $2')
+ )
.replace(/\n\s*(Android|iOS):\s*\n\s*(?=\n|$)/g, '')
.replace(/##### `([^`]+)`/g, '\n\n**$1**')
.replace(/##### ([^\n]+)/g, '\n\n**$1**')
diff --git a/packages/expo-audio/CHANGELOG.md b/packages/expo-audio/CHANGELOG.md
index b4ae476ebfb37a..777e8e01c457f3 100644
--- a/packages/expo-audio/CHANGELOG.md
+++ b/packages/expo-audio/CHANGELOG.md
@@ -22,6 +22,7 @@
- Fix `player.replace(null)` throwing due to a mismatch between native and TypeScript types. ([#48219](https://github.com/expo/expo/pull/48219) by [@zoontek](https://github.com/zoontek))
- [iOS] Activate the audio session once and keep it active instead of toggling. ([#48588](https://github.com/expo/expo/pull/48588) by [@alanjhughes](https://github.com/alanjhughes))
- [Android] Fix `createAudioPlayer`/`useAudioPlayer` throwing "Received 5 arguments, but 4 was expected" due to the native `AudioPlayer` constructor missing the iOS-only `allowsExternalPlayback` parameter. ([#48655](https://github.com/expo/expo/pull/48655) by [@RasmusKard](https://github.com/RasmusKard))
+- [iOS] Report `denied` instead of crashing the app when `NSMicrophoneUsageDescription` is missing. ([#48840](https://github.com/expo/expo/pull/48840) by [@ahmadaccino](https://github.com/ahmadaccino))
### 💡 Others
diff --git a/packages/expo-audio/ios/AudioRecordingRequester.swift b/packages/expo-audio/ios/AudioRecordingRequester.swift
index f3b860783978f0..fa50882a9f053b 100644
--- a/packages/expo-audio/ios/AudioRecordingRequester.swift
+++ b/packages/expo-audio/ios/AudioRecordingRequester.swift
@@ -1,5 +1,4 @@
import ExpoModulesCore
-internal import React
private let selector = ["request", "Record", "Permission", ":"]
@@ -9,15 +8,25 @@ public class AudioRecordingRequester: NSObject, EXPermissionsRequester {
}
public func getPermissions() -> [AnyHashable: Any] {
- let systemStatus = AVAudioSession.sharedInstance().recordPermission
+ return Self.permissions(
+ systemStatus: AVAudioSession.sharedInstance().recordPermission,
+ usageDescription: Bundle.main.object(forInfoDictionaryKey: "NSMicrophoneUsageDescription")
+ )
+ }
+
+ static func permissions(
+ systemStatus: AVAudioSession.RecordPermission,
+ usageDescription: Any?
+ ) -> [AnyHashable: Any] {
var status: EXPermissionStatus
- guard (Bundle.main.infoDictionary?["NSMicrophoneUsageDescription"]) != nil else {
- RCTFatal(RCTErrorWithMessage("""
- This app is missing NSMicrophoneUsageDescription, so audio services will fail.
- Add one of these keys to your bundle's Info.plist.
- """))
- return ["status": EXPermissionStatusDenied]
+ guard usageDescription != nil else {
+ log.error("""
+ This app is missing NSMicrophoneUsageDescription, so audio recording will fail. \
+ Add the key to the app's Info.plist, or set the `microphonePermission` option on the \
+ expo-audio config plugin.
+ """)
+ return ["status": EXPermissionStatusDenied.rawValue]
}
switch systemStatus {
diff --git a/packages/expo-audio/ios/Tests/AudioRecordingRequesterTests.swift b/packages/expo-audio/ios/Tests/AudioRecordingRequesterTests.swift
new file mode 100644
index 00000000000000..649dc8eed3f3c6
--- /dev/null
+++ b/packages/expo-audio/ios/Tests/AudioRecordingRequesterTests.swift
@@ -0,0 +1,34 @@
+#if !os(tvOS)
+import Testing
+import AVFoundation
+import ExpoModulesCore
+
+@testable import ExpoAudio
+
+@Suite("AudioRecordingRequester.permissions")
+struct AudioRecordingRequesterTests {
+ @Test
+ func `reports denied when the usage description is missing`() {
+ let permissions = AudioRecordingRequester.permissions(systemStatus: .granted, usageDescription: nil)
+
+ #expect(permissions["status"] as? Int == EXPermissionStatusDenied.rawValue)
+ }
+
+ @Test(arguments: [
+ (AVAudioSession.RecordPermission.granted, EXPermissionStatusGranted),
+ (.denied, EXPermissionStatusDenied),
+ (.undetermined, EXPermissionStatusUndetermined),
+ ] as [(AVAudioSession.RecordPermission, EXPermissionStatus)])
+ func `maps the system status when the usage description is present`(
+ systemStatus: AVAudioSession.RecordPermission,
+ expected: EXPermissionStatus
+ ) {
+ let permissions = AudioRecordingRequester.permissions(
+ systemStatus: systemStatus,
+ usageDescription: "Allow $(PRODUCT_NAME) to access your microphone"
+ )
+
+ #expect(permissions["status"] as? Int == expected.rawValue)
+ }
+}
+#endif
diff --git a/packages/expo-router/src/layouts/stack-utils/StackSearchBar.tsx b/packages/expo-router/src/layouts/stack-utils/StackSearchBar.tsx
index 62ce4706ef349d..0932b67cea8e34 100644
--- a/packages/expo-router/src/layouts/stack-utils/StackSearchBar.tsx
+++ b/packages/expo-router/src/layouts/stack-utils/StackSearchBar.tsx
@@ -27,7 +27,7 @@ export interface StackSearchBarProps extends SearchBarProps {}
* <>
* console.log(text)}
+ * onChangeText={(event) => console.log(event.nativeEvent.text)}
* />
*
* >