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" } }