diff --git a/docs/components/plugins/api/APISectionUtils.tsx b/docs/components/plugins/api/APISectionUtils.tsx
index fdb596675cc3cb..21bdd4a0f94df1 100644
--- a/docs/components/plugins/api/APISectionUtils.tsx
+++ b/docs/components/plugins/api/APISectionUtils.tsx
@@ -239,11 +239,13 @@ export const resolveTypeName = (
sdkVersion,
});
} else if (type === 'array') {
- return resolveTypeName(elementType, sdkVersion) + '[]';
+ return <>{resolveTypeName(elementType, sdkVersion)}[]>;
}
return elementType.name + type;
+ } else if (type === 'rest' && elementType) {
+ return <>...{resolveTypeName(elementType, sdkVersion)}>;
} else if (elementType?.type === 'array') {
- return resolveTypeName(elementType, sdkVersion) + '[]';
+ return <>{resolveTypeName(elementType, sdkVersion)}[]>;
} else if (elementType?.declaration) {
if (type === 'array') {
const { parameters, type: paramType } = elementType.declaration.indexSignature ?? {};
@@ -395,8 +397,6 @@ export const resolveTypeName = (
return operator ?? 'undefined';
} else if (type === 'intrinsic') {
return name ?? 'undefined';
- } else if (type === 'rest' && elementType) {
- return <>...{resolveTypeName(elementType, sdkVersion)}>;
} else if (value === null) {
return 'null';
}
diff --git a/docs/components/plugins/api/__snapshots__/APISectionUtils.test.tsx.snap b/docs/components/plugins/api/__snapshots__/APISectionUtils.test.tsx.snap
index 9c3c3fa9fe8066..7542fed94f3151 100644
--- a/docs/components/plugins/api/__snapshots__/APISectionUtils.test.tsx.snap
+++ b/docs/components/plugins/api/__snapshots__/APISectionUtils.test.tsx.snap
@@ -619,7 +619,8 @@ exports[`APISectionUtils.resolveTypeName union of array values 1`] = `
exports[`APISectionUtils.resolveTypeName union with array 1`] = `
- number[]
+ number
+ []
diff --git a/docs/scripts/lint.js b/docs/scripts/lint.js
index 92dce76bad28d3..3f52869316be2c 100644
--- a/docs/scripts/lint.js
+++ b/docs/scripts/lint.js
@@ -21,7 +21,10 @@ const eslintArgs = [
function runTsc() {
return new Promise(resolve => {
const chunks = [];
- const proc = spawn('tsc', ['--noEmit', '--pretty'], { stdio: ['ignore', 'pipe', 'pipe'] });
+ const proc = spawn('tsc', ['--noEmit', '--pretty'], {
+ stdio: ['ignore', 'pipe', 'pipe'],
+ shell: true,
+ });
proc.stdout.on('data', d => chunks.push(d));
proc.stderr.on('data', d => chunks.push(d));
proc.on('close', status => {
@@ -34,6 +37,7 @@ function runTsc() {
function runEslint() {
const { status, stderr } = spawnSync('eslint', eslintArgs, {
stdio: ['inherit', 'inherit', 'pipe'],
+ shell: true,
});
// If ESLint fails with a fatal error, the cache may be stale. Clear it and retry once.
@@ -44,6 +48,7 @@ function runEslint() {
} catch {}
const retry = spawnSync('eslint', eslintArgs, {
stdio: ['inherit', 'inherit', 'pipe'],
+ shell: true,
});
return { status: retry.status, stderr: retry.stderr };
}
diff --git a/docs/yarn.lock b/docs/yarn.lock
index d08929b38ddde2..5ba5aaa77e82c0 100644
--- a/docs/yarn.lock
+++ b/docs/yarn.lock
@@ -12787,11 +12787,11 @@ __metadata:
linkType: hard
"qs@npm:^6.4.0":
- version: 6.14.1
- resolution: "qs@npm:6.14.1"
+ version: 6.15.0
+ resolution: "qs@npm:6.15.0"
dependencies:
side-channel: "npm:^1.1.0"
- checksum: 10c0/0e3b22dc451f48ce5940cbbc7c7d9068d895074f8c969c0801ac15c1313d1859c4d738e46dc4da2f498f41a9ffd8c201bd9fb12df67799b827db94cc373d2613
+ checksum: 10c0/ff341078a78a991d8a48b4524d52949211447b4b1ad907f489cac0770cbc346a28e47304455c0320e5fb000f8762d64b03331e3b71865f663bf351bcba8cdb4b
languageName: node
linkType: hard
diff --git a/packages/expo-dev-launcher/CHANGELOG.md b/packages/expo-dev-launcher/CHANGELOG.md
index 4af4adafbd19f3..ac59d44b79b8ee 100644
--- a/packages/expo-dev-launcher/CHANGELOG.md
+++ b/packages/expo-dev-launcher/CHANGELOG.md
@@ -8,6 +8,8 @@
### 🐛 Bug fixes
+- [android] fixed crash when returning from notification settings after disabling notification permissions ([#43217](https://github.com/expo/expo/pull/43217) by [@vonovak](https://github.com/vonovak))
+
### 💡 Others
## 55.0.7 — 2026-02-16
diff --git a/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/react/activitydelegates/DevLauncherReactActivityNOPDelegate.kt b/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/react/activitydelegates/DevLauncherReactActivityNOPDelegate.kt
index b8d2bc15a1f431..ad1f826161ab13 100644
--- a/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/react/activitydelegates/DevLauncherReactActivityNOPDelegate.kt
+++ b/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/react/activitydelegates/DevLauncherReactActivityNOPDelegate.kt
@@ -16,6 +16,7 @@ open class DevLauncherReactActivityNOPDelegate(activity: ReactActivity) :
override fun onNewIntent(intent: Intent?): Boolean = true
override fun onBackPressed(): Boolean = true
override fun onWindowFocusChanged(hasFocus: Boolean) {}
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {}
override fun onConfigurationChanged(newConfig: Configuration) {}
}
diff --git a/packages/expo-dev-launcher/ios/EXDevLauncherController.m b/packages/expo-dev-launcher/ios/EXDevLauncherController.m
index e6cb1a05599ba9..c9ca6fbfe758e6 100644
--- a/packages/expo-dev-launcher/ios/EXDevLauncherController.m
+++ b/packages/expo-dev-launcher/ios/EXDevLauncherController.m
@@ -197,13 +197,13 @@ - (void)start:(id)delegate launchOptions:(NSDic
};
#if TARGET_OS_SIMULATOR
- BOOL hasCompletedPermissionFlow = YES;
+ BOOL hasGrantedNetworkPermission = YES;
#else
- BOOL hasCompletedPermissionFlow = [[NSUserDefaults standardUserDefaults] boolForKey:@"expo.devlauncher.hasCompletedNetworkPermissionFlow"];
+ BOOL hasGrantedNetworkPermission = [[NSUserDefaults standardUserDefaults] boolForKey:@"expo.devlauncher.hasGrantedNetworkPermission"];
#endif
NSURL* initialUrl = [EXDevLauncherController initialUrlFromProcessInfo];
- if (initialUrl && hasCompletedPermissionFlow) {
+ if (initialUrl && hasGrantedNetworkPermission) {
[self loadApp:initialUrl withProjectUrl:nil onSuccess:nil onError:navigateToLauncher];
return;
}
@@ -211,7 +211,7 @@ - (void)start:(id)delegate launchOptions:(NSDic
NSNumber *devClientTryToLaunchLastBundleValue = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"DEV_CLIENT_TRY_TO_LAUNCH_LAST_BUNDLE"];
BOOL shouldTryToLaunchLastOpenedBundle = (devClientTryToLaunchLastBundleValue != nil) ? [devClientTryToLaunchLastBundleValue boolValue] : YES;
- if (!hasCompletedPermissionFlow) {
+ if (!hasGrantedNetworkPermission) {
shouldTryToLaunchLastOpenedBundle = NO;
}
diff --git a/packages/expo-dev-launcher/ios/SwiftUI/DevLauncherViewModel.swift b/packages/expo-dev-launcher/ios/SwiftUI/DevLauncherViewModel.swift
index 8f764fa35d8b99..26400e30654014 100644
--- a/packages/expo-dev-launcher/ios/SwiftUI/DevLauncherViewModel.swift
+++ b/packages/expo-dev-launcher/ios/SwiftUI/DevLauncherViewModel.swift
@@ -10,7 +10,12 @@ private let sessionKey = "expo-session-secret"
private let DEV_LAUNCHER_DEFAULT_SCHEME = "expo-dev-launcher"
private let BONJOUR_TYPE = "_expo._tcp"
-private let networkPermissionFlowKey = "expo.devlauncher.hasCompletedNetworkPermissionFlow"
+private let networkPermissionGrantedKey = "expo.devlauncher.hasGrantedNetworkPermission"
+
+enum LocalNetworkPermissionStatus: Equatable, Sendable {
+ case unknown
+ case denied
+}
@MainActor
class DevLauncherViewModel: ObservableObject {
@@ -205,41 +210,22 @@ class DevLauncherViewModel: ObservableObject {
return
}
- let hasCompletedPermissionFlow = UserDefaults.standard.bool(
- forKey: networkPermissionFlowKey
- )
-
- #if targetEnvironment(simulator)
- // Simulators don't need permission, continue
- #else
- if !hasCompletedPermissionFlow {
- return
- }
- #endif
-
- stopServerDiscovery()
- startDevServerBrowser()
- startLocalDevServerScanner()
- }
-
- func startDiscoveryForPermissionCheck() {
- permissionStatus = .checking
stopServerDiscovery()
startDevServerBrowser()
startLocalDevServerScanner()
}
- func markPermissionFlowCompleted() {
- UserDefaults.standard.set(true, forKey: networkPermissionFlowKey)
+ func markNetworkPermissionGranted() {
+ UserDefaults.standard.set(true, forKey: networkPermissionGrantedKey)
}
func resetPermissionFlowState() {
- UserDefaults.standard.removeObject(forKey: networkPermissionFlowKey)
+ UserDefaults.standard.removeObject(forKey: networkPermissionGrantedKey)
permissionStatus = .unknown
}
- var isFirstPermissionCheck: Bool {
- !UserDefaults.standard.bool(forKey: networkPermissionFlowKey)
+ var hasGrantedNetworkPermission: Bool {
+ UserDefaults.standard.bool(forKey: networkPermissionGrantedKey)
}
func stopServerDiscovery() {
@@ -283,7 +269,7 @@ class DevLauncherViewModel: ObservableObject {
guard let self else { return }
switch state {
case .ready:
- self.permissionStatus = .granted
+ self.markNetworkPermissionGranted()
case .waiting(let error):
if case .dns(let dnsError) = error, dnsError == kDNSServiceErr_PolicyDenied {
self.permissionStatus = .denied
diff --git a/packages/expo-dev-launcher/ios/SwiftUI/DevLauncherViews.swift b/packages/expo-dev-launcher/ios/SwiftUI/DevLauncherViews.swift
index c5b3cb7ca7b104..2d677a27d66a0c 100644
--- a/packages/expo-dev-launcher/ios/SwiftUI/DevLauncherViews.swift
+++ b/packages/expo-dev-launcher/ios/SwiftUI/DevLauncherViews.swift
@@ -9,8 +9,8 @@ public struct DevLauncherRootView: View {
init(viewModel: DevLauncherViewModel) {
self.viewModel = viewModel
- let shouldSkipPermissionFlow = Self.isSimulator
- || UserDefaults.standard.bool(forKey: "expo.devlauncher.hasCompletedNetworkPermissionFlow")
+ let shouldSkipPermissionFlow = Self.isSimulator
+ || UserDefaults.standard.bool(forKey: "expo.devlauncher.hasGrantedNetworkPermission")
_hasCompletedPermissionFlow = State(initialValue: shouldSkipPermissionFlow)
}
@@ -24,12 +24,9 @@ public struct DevLauncherRootView: View {
public var body: some View {
if !hasCompletedPermissionFlow {
- LocalNetworkPermissionView(
- viewModel: viewModel,
- onPermissionGranted: {
- hasCompletedPermissionFlow = true
- }
- )
+ LocalNetworkPermissionView {
+ hasCompletedPermissionFlow = true
+ }
} else {
mainContent
}
diff --git a/packages/expo-dev-launcher/ios/SwiftUI/LocalNetworkPermissionView.swift b/packages/expo-dev-launcher/ios/SwiftUI/LocalNetworkPermissionView.swift
index 4afb30805a0f32..d83f5f515fa12d 100644
--- a/packages/expo-dev-launcher/ios/SwiftUI/LocalNetworkPermissionView.swift
+++ b/packages/expo-dev-launcher/ios/SwiftUI/LocalNetworkPermissionView.swift
@@ -2,315 +2,76 @@
import SwiftUI
-enum LocalNetworkPermissionStatus: Equatable, Sendable {
- case unknown
- case checking
- case granted
- case denied
-}
-
struct LocalNetworkPermissionView: View {
- @ObservedObject var viewModel: DevLauncherViewModel
- let onPermissionGranted: () -> Void
- @State private var isCheckingPermission = false
- @State private var timeoutTask: Task?
- @State private var hasTimedOut = false
+ let onContinue: () -> Void
- private var isLoading: Bool {
- isCheckingPermission || viewModel.permissionStatus == .checking
- }
-
var body: some View {
VStack(spacing: 0) {
Spacer()
-
- if hasTimedOut {
- PermissionTimeoutView {
- retryPermissionCheck()
- } continueWithoutPermission: {
- continueWithoutPermission()
- }
- } else {
- switch viewModel.permissionStatus {
- case .unknown, .checking:
- RequestPermissionView(isLoading: isLoading) {
- triggerPermissionCheck()
- }
- case .granted:
- ProgressView()
- case .denied:
- PermissionsDeniedView(appName: appName) {
- openSettings()
- } continueWithoutPermission: {
- continueWithoutPermission()
- }
- }
- }
-
- Spacer()
-
- Footer()
- }
- .padding(.horizontal, 24)
- .padding(.vertical, 32)
- .background(Color.expoSystemBackground)
- .onDisappear {
- timeoutTask?.cancel()
- timeoutTask = nil
- }
- .onChange(of: viewModel.permissionStatus) { newStatus in
- timeoutTask?.cancel()
- timeoutTask = nil
- if newStatus == .granted {
- hasTimedOut = false
- viewModel.markPermissionFlowCompleted()
- onPermissionGranted()
- } else if newStatus == .denied {
- isCheckingPermission = false
- hasTimedOut = false
- }
- }
- }
-
- private var appName: String {
- Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
- ?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
- ?? "this app"
- }
-
- private func triggerPermissionCheck() {
- isCheckingPermission = true
- hasTimedOut = false
- viewModel.startDiscoveryForPermissionCheck()
+ VStack(spacing: 24) {
+ Image(systemName: "wifi")
+ .font(.system(size: 64))
+ .foregroundColor(.accentColor)
- timeoutTask?.cancel()
- timeoutTask = Task {
- do {
- try await Task.sleep(nanoseconds: 15_000_000_000)
- await MainActor.run {
- if viewModel.permissionStatus == .checking {
- hasTimedOut = true
- isCheckingPermission = false
- viewModel.stopServerDiscovery()
- viewModel.permissionStatus = .unknown
- }
- }
- } catch {}
- }
- }
+ VStack(spacing: 12) {
+ Text("Find Dev Servers")
+ .font(.title)
+ .fontWeight(.bold)
- private func retryPermissionCheck() {
- hasTimedOut = false
- triggerPermissionCheck()
- }
-
- private func openSettings() {
- #if os(iOS)
- if let url = URL(string: UIApplication.openSettingsURLString) {
- UIApplication.shared.open(url)
- }
- #endif
- }
-
- private func continueWithoutPermission() {
- viewModel.markPermissionFlowCompleted()
- onPermissionGranted()
- }
-}
+ Text("Expo Dev Launcher needs to access your local network to discover development servers running on your computer.")
+ .font(.body)
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.center)
+ }
-struct RequestPermissionView: View {
- let isLoading: Bool
- let triggerPermissionCheck: () -> Void
+ HStack(alignment: .center, spacing: 8) {
+ Image(systemName: "info.circle")
+ Text("You'll see a system prompt asking for local network access.\nTap \"Allow\" to continue.")
+ .multilineTextAlignment(.center)
+ }
+ .frame(maxWidth: .infinity)
+ .padding()
+ .background(Color.expoSecondarySystemBackground)
+ .cornerRadius(12)
+ .font(.callout)
+ .foregroundColor(.secondary)
- var body: some View {
- VStack(spacing: 24) {
- Image(systemName: "wifi")
- .font(.system(size: 64))
- .foregroundColor(.accentColor)
-
- VStack(spacing: 12) {
- Text("Find Dev Servers")
- .font(.title)
- .fontWeight(.bold)
-
- Text("Expo Dev Launcher needs to access your local network to discover development servers running on your computer.")
- .font(.body)
- .foregroundColor(.secondary)
- .multilineTextAlignment(.center)
- }
-
- VStack(spacing: 8) {
- Image(systemName: "info.circle")
- .foregroundColor(.secondary)
- Text("You'll see a system prompt asking for local network access. Tap \"Allow\" to continue.")
- .font(.footnote)
- .foregroundColor(.secondary)
- .multilineTextAlignment(.center)
- }
- .padding()
- .background(Color.expoSecondarySystemBackground)
- .cornerRadius(12)
-
- Button {
- triggerPermissionCheck()
- } label: {
- if isLoading {
- ProgressView()
- .progressViewStyle(CircularProgressViewStyle(tint: .white))
- .frame(maxWidth: .infinity)
- .padding()
- } else {
+ Button {
+ onContinue()
+ } label: {
Text("Continue")
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
.padding()
}
- }
- .background(Color.accentColor)
- .foregroundColor(.white)
- .cornerRadius(12)
- .disabled(isLoading)
- }
- }
-}
-
-struct PermissionsDeniedView: View {
- let appName: String
- let openSettings: () -> Void
- let continueWithoutPermission: () -> Void
-
- var body: some View {
- VStack(spacing: 24) {
- Image(systemName: "wifi.slash")
- .font(.system(size: 64))
- .foregroundColor(.orange)
-
- VStack(spacing: 12) {
- Text("Local Network Access Required")
- .font(.title)
- .fontWeight(.bold)
-
- Text("Without local network access, Dev Launcher can't find development servers on your network. You can still enter server URLs manually.")
- .font(.body)
- .foregroundColor(.secondary)
- .multilineTextAlignment(.center)
- }
-
- VStack(spacing: 12) {
- Button {
- openSettings()
- } label: {
- HStack {
- Image(systemName: "gear")
- Text("Open Settings")
- .fontWeight(.semibold)
- }
- .frame(maxWidth: .infinity)
- .padding()
- }
.background(Color.accentColor)
.foregroundColor(.white)
.cornerRadius(12)
-
- Button {
- continueWithoutPermission()
- } label: {
- Text("Continue Without Discovery")
- .fontWeight(.medium)
- .frame(maxWidth: .infinity)
- .padding()
- }
- .foregroundColor(.accentColor)
}
-
- Text("To enable later: Settings → Privacy & Security → Local Network → \(appName)")
- .font(.caption)
- .foregroundColor(.secondary)
- .multilineTextAlignment(.center)
- }
- }
-}
-
-struct PermissionTimeoutView: View {
- let retryPermissionCheck: () -> Void
- let continueWithoutPermission: () -> Void
-
- var body: some View {
- VStack(spacing: 24) {
- Image(systemName: "exclamationmark.triangle")
- .font(.system(size: 64))
- .foregroundColor(.orange)
- VStack(spacing: 12) {
- Text("Permission Check Timed Out")
- .font(.title)
- .fontWeight(.bold)
+ Spacer()
- Text("The permission check is taking longer than expected. This might happen if you dismissed the system dialog or if there's a network issue.")
- .font(.body)
+ VStack(spacing: 4) {
+ Text("Why is this needed?")
+ .font(.footnote)
+ .fontWeight(.medium)
+ Text("Dev servers advertise themselves on your local network using Bonjour. This permission allows the app to discover them automatically.")
+ .font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
-
- VStack(spacing: 12) {
- Button {
- retryPermissionCheck()
- }
- label: {
- HStack {
- Image(systemName: "arrow.clockwise")
- Text("Try Again")
- .fontWeight(.semibold)
- }
- .frame(maxWidth: .infinity)
- .padding()
- }
- .background(Color.accentColor)
- .foregroundColor(.white)
- .cornerRadius(12)
-
- Button {
- continueWithoutPermission()
- }
- label: {
- Text("Continue Without Discovery")
- .fontWeight(.medium)
- .frame(maxWidth: .infinity)
- .padding()
- }
- .foregroundColor(.accentColor)
- }
-
- Text("You can enable discovery later in Settings if needed.")
- .font(.caption)
- .foregroundColor(.secondary)
- .multilineTextAlignment(.center)
- }
- }
-}
-
-struct Footer: View {
- var body: some View {
- VStack(spacing: 4) {
- Text("Why is this needed?")
- .font(.footnote)
- .fontWeight(.medium)
- Text("Dev servers advertise themselves on your local network using Bonjour. This permission allows the app to discover them automatically.")
- .font(.caption)
- .foregroundColor(.secondary)
- .multilineTextAlignment(.center)
}
+ .padding(.horizontal, 24)
+ .padding(.vertical, 32)
+ .background(Color.expoSystemBackground)
}
}
#if DEBUG
struct LocalNetworkPermissionView_Previews: PreviewProvider {
static var previews: some View {
- LocalNetworkPermissionView(
- viewModel: DevLauncherViewModel(),
- onPermissionGranted: {}
- )
+ LocalNetworkPermissionView(onContinue: {})
}
}
#endif
diff --git a/packages/expo-dev-launcher/ios/SwiftUI/SettingsTabView.swift b/packages/expo-dev-launcher/ios/SwiftUI/SettingsTabView.swift
index b38554c8bd0d00..ede6a5bc3bf39c 100644
--- a/packages/expo-dev-launcher/ios/SwiftUI/SettingsTabView.swift
+++ b/packages/expo-dev-launcher/ios/SwiftUI/SettingsTabView.swift
@@ -259,7 +259,7 @@ struct SettingsTabView: View {
HStack {
Text("First Launch Check")
Spacer()
- Text(viewModel.isFirstPermissionCheck ? "Pending" : "Completed")
+ Text(viewModel.hasGrantedNetworkPermission ? "Granted" : "Pending")
.foregroundColor(.secondary)
}
.padding()
@@ -327,20 +327,18 @@ struct SettingsTabView: View {
private func checkNetworkPermission() {
isCheckingPermission = true
permissionCheckResult = "Checking..."
- viewModel.startDiscoveryForPermissionCheck()
+ viewModel.stopServerDiscovery()
+ viewModel.startServerDiscovery()
}
-
+
private func updatePermissionResultFromStatus() {
isCheckingPermission = false
- switch viewModel.permissionStatus {
- case .granted:
+ if viewModel.hasGrantedNetworkPermission {
permissionCheckResult = "✅ Granted"
- case .denied:
+ } else if viewModel.permissionStatus == .denied {
permissionCheckResult = "❌ Denied"
- case .unknown:
+ } else {
permissionCheckResult = "⚠️ Unknown"
- case .checking:
- permissionCheckResult = "🔄 Checking"
}
}