diff --git a/apps/bare-expo/ios/Podfile.lock b/apps/bare-expo/ios/Podfile.lock index 0fca4da59da7ce..0ae5c287c77618 100644 --- a/apps/bare-expo/ios/Podfile.lock +++ b/apps/bare-expo/ios/Podfile.lock @@ -2923,7 +2923,7 @@ PODS: - ReactNativeDependencies - RNWorklets - Yoga - - RNScreens (4.19.0): + - RNScreens (4.20.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2945,9 +2945,9 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies - - RNScreens/common (= 4.19.0) + - RNScreens/common (= 4.20.0) - Yoga - - RNScreens/common (4.19.0): + - RNScreens/common (4.20.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -3827,7 +3827,7 @@ SPEC CHECKSUMS: EXUpdates: 83e4d666a085b44149f3b21d5bd057ad37b2c3e5 EXUpdatesInterface: 1436757deb0d574b84bba063bd024c315e0ec08b FBLazyVector: 3a7ea85f6009224ad89f7daeda516f189e6b5759 - hermes-engine: 6bb3000824be2770010ae038914fa26721255c8e + hermes-engine: f631dcabc3dc2d46dc5f32c6c79d48d1d9e9aac6 libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7 libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 @@ -3845,7 +3845,7 @@ SPEC CHECKSUMS: React: 4bc1f928568ad4bcfd147260f907b4ea5873a03b React-callinvoker: 8dd44c31888a314694efd0ef5f19ab7e0e855ef8 React-Core: 0c1f3042e1204c0512b2e4263062c66550d7e6a3 - React-Core-prebuilt: 7894b037a2f0fa699a44de6c88d20acd4235b255 + React-Core-prebuilt: baa77ffa5636202bd80ae54a374df8b194f96ed6 React-CoreModules: f6a626221d52f21b5eb8df2d79780b96f20240e5 React-cxxreact: 2e3990595049d43dd1d59ccd6cb35545f0dc6f03 React-debug: 60be0767f5672afc81bfd6a50d996507430f7906 @@ -3915,14 +3915,14 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: bfb12ead469222b022a2024f32aba47ce50de512 ReactCodegen: b9b0ea5c72e425fa5a89a031ada3e64dfd839771 ReactCommon: 0084791d25c4deae3d8b77efd4440fb2d5c38746 - ReactNativeDependencies: 17a617edb4d5883a4c48339514ccb8b765f8af4f + ReactNativeDependencies: c5e58d6ef1758f36ac4ee8b97949e0fb78f31b21 RNCAsyncStorage: e85a99325df9eb0191a6ee2b2a842644c7eb29f4 RNCMaskedView: 3c9d7586e2b9bbab573591dcb823918bc4668005 RNCPicker: e0149590451d5eae242cf686014a6f6d808f93c7 RNDateTimePicker: 5e0666de98f1edfac67ee7dde6be8a5415e487a0 RNGestureHandler: 8e4a9372425d4caa9e3da5072a8dda7a54ed1097 RNReanimated: 61462806110686a6f5d7c45c6f910cf73cd57dd9 - RNScreens: 08ec2d3a35cbeeb9ddd063e5aa8fb6da5bf3a978 + RNScreens: dea74a9106daf3cbed39de1f5ce7ff4f96c5c722 RNSVG: 81c64c70e69ce2a1180a2f7355cada5f8aee001c RNWorklets: 78d1eb5df6aa27c762c1466aaaffba8774d807a8 SDWebImage: f29024626962457f3470184232766516dee8dfea diff --git a/apps/bare-expo/package.json b/apps/bare-expo/package.json index 1e66b5f41d59e4..f99c3b13089296 100644 --- a/apps/bare-expo/package.json +++ b/apps/bare-expo/package.json @@ -75,7 +75,7 @@ "react-native-pager-view": "6.9.1", "react-native-reanimated": "4.2.1", "react-native-safe-area-context": "5.6.2", - "react-native-screens": "4.19.0", + "react-native-screens": "4.20.0", "react-native-svg": "15.15.1", "react-native-view-shot": "4.0.3", "react-native-webview": "13.16.0", diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/DevSessionRow.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/DevSessionRow.kt index 65ca1459b91297..ce8a4ebc031ad8 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/DevSessionRow.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/DevSessionRow.kt @@ -12,26 +12,39 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage import host.exp.expoview.R @Composable fun DevSessionRow(session: DevSession) { val uriHandler = LocalUriHandler.current - val image = if (session.source == DevSessionSource.Desktop) { - painterResource(id = R.drawable.cli) - } else { - painterResource(id = R.drawable.snack) - } + ClickableItemRow( onClick = { uriHandler.openUri(session.url) }, icon = { - Image( - painter = image, - contentDescription = "Icon", - modifier = Modifier - .size(24.dp) - .clip(shape = RoundedCornerShape(4.dp)) - ) + if (session.iconUrl != null) { + AsyncImage( + session.iconUrl, + contentDescription = "Icon", + modifier = Modifier + .size(24.dp) + .clip(shape = RoundedCornerShape(4.dp)) + ) + } else { + Image( + painter = painterResource( + id = if (session.source == DevSessionSource.Desktop) { + R.drawable.cli + } else { + R.drawable.snack + } + ), + contentDescription = "Icon", + modifier = Modifier + .size(24.dp) + .clip(shape = RoundedCornerShape(4.dp)) + ) + } } ) { Column { diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/HomeAppViewModel.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/HomeAppViewModel.kt index 71f43b1befc76d..2db93601cfac36 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/HomeAppViewModel.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/HomeAppViewModel.kt @@ -3,6 +3,7 @@ package host.exp.exponent.home import android.app.Activity import android.app.Application import android.content.Context +import android.os.Build import androidx.activity.result.ActivityResultLauncher import androidx.core.content.edit import androidx.lifecycle.AndroidViewModel @@ -54,6 +55,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import okhttp3.OkHttpClient +import okhttp3.Request import java.util.Date import kotlin.reflect.typeOf import kotlin.time.DurationUnit @@ -76,13 +78,27 @@ data class DevSession( val source: DevSessionSource, val hostname: String? = null, val config: Map? = null, - val platform: DevSessionPlatform? = null + val platform: DevSessionPlatform? = null, + val iconUrl: String? = null ) data class DevSessionResponse( val data: List ) +private data class ManifestResponse( + val extra: ManifestExtra? +) + +private data class ManifestExtra( + val expoClient: ManifestExpoClient? +) + +private data class ManifestExpoClient( + val name: String?, + val iconUrl: String? +) + private const val USER_REVIEW_INFO_PREFS_KEY = "userReviewInfo" data class UserReviewState( @@ -169,8 +185,24 @@ class HomeAppViewModel( val expoVersion = expoViewKernel.versionName - val isDevice = - !(android.os.Build.MODEL.contains("google_sdk") || android.os.Build.MODEL.contains("Emulator")) + val isDevice = !( + (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) || + Build.FINGERPRINT.startsWith("generic") || + Build.FINGERPRINT.startsWith("unknown") || + Build.HARDWARE.contains("goldfish") || + Build.HARDWARE.contains("ranchu") || + Build.MODEL.contains("google_sdk") || + Build.MODEL.contains("Emulator") || + Build.MODEL.contains("Android SDK built for x86") || + Build.MANUFACTURER.contains("Genymotion") || + Build.PRODUCT.contains("sdk_google") || + Build.PRODUCT.contains("google_sdk") || + Build.PRODUCT.contains("sdk") || + Build.PRODUCT.contains("sdk_x86") || + Build.PRODUCT.contains("vbox86p") || + Build.PRODUCT.contains("emulator") || + Build.PRODUCT.contains("simulator") + ) val service = ApolloClientService(client, sessionRepository) private val restClient = @@ -204,7 +236,55 @@ class HomeAppViewModel( val feedbackState = MutableStateFlow(FeedbackState()) - val developmentServers: StateFlow> = flow { + private suspend fun checkDevelopmentServer(host: String, port: String): DevSession? { + return withContext(Dispatchers.IO) { + runCatching { + val url = "http://$host:$port" + val request = Request.Builder().url("$url/status").build() + val response = client.newCall(request).execute() + + if (!response.isSuccessful || response.body?.string()?.contains("packager-status:running") != true) { + return@runCatching null + } + + val manifestInfo = fetchLocalManifestInfo(url) + DevSession( + url = "exp://$host:$port", + description = manifestInfo?.extra?.expoClient?.name ?: "exp://$host:$port", + source = DevSessionSource.Desktop, + hostname = "exp://$host:$port", + platform = DevSessionPlatform.Native, + config = null, + iconUrl = manifestInfo?.extra?.expoClient?.iconUrl + ) + }.getOrNull() + } + } + + private suspend fun fetchLocalManifestInfo(baseUrl: String): ManifestResponse? { + return withContext(Dispatchers.IO) { + runCatching { + val manifestUrl = "$baseUrl/manifest" + val request = Request.Builder() + .url(manifestUrl) + .addHeader("Accept", "application/expo+json,application/json") + .addHeader("Expo-Platform", "android") + .build() + + client.newCall(request).execute().use { response -> + if (response.isSuccessful) { + response.body?.string()?.let { + gson.fromJson(it, ManifestResponse::class.java) + } + } else { + null + } + } + }.getOrNull() + } + } + + private val remoteDevelopmentServers: StateFlow> = flow { while (true) { try { val sessions = restClient.sendAuthenticatedApiV2Request( @@ -223,6 +303,57 @@ class HomeAppViewModel( initialValue = emptyList() ) + private val localDevelopmentServers: StateFlow> = flow { + val portsToCheck = listOf(8081, 8082, 8083, 19000, 19001, 19002) + val baseAddress = if (isDevice) "localhost" else "10.0.2.2" + while (true) { + val discoveredServers = mutableListOf() + withContext(Dispatchers.IO) { + portsToCheck.map { port -> + launch { + checkDevelopmentServer(baseAddress, "$port")?.let { + synchronized(discoveredServers) { + discoveredServers.add(it) + } + } + } + }.forEach { it.join() } + } + emit(discoveredServers) + delay(3000) + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = emptyList() + ) + + val developmentServers: StateFlow> = + combine( + localDevelopmentServers, + remoteDevelopmentServers + ) { local, remote -> + val merged = mutableMapOf() + + // Add all local servers first + for (server in local) { + val key = normalizeUrl(server.url).lowercase() + merged[key] = server + } + + // Add remote servers, preferring them over local ones if a conflict exists + for (server in remote) { + val key = normalizeUrl(server.url).lowercase() + merged[key] = server + } + merged.values.sortedBy { it.url } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = emptyList() + ) + fun branch(branchName: String, appId: String): RefreshableFlow { return refreshableFlow(scope = viewModelScope, fetcher = { service.branchDetails(branchName, appId) diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/ProjectDetailsScreen.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/ProjectDetailsScreen.kt index 32c57f19fa9e8c..e1f76bc68b5de0 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/ProjectDetailsScreen.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/ProjectDetailsScreen.kt @@ -1,24 +1,35 @@ package host.exp.exponent.home +import android.content.Context +import android.content.Intent import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import host.exp.exponent.graphql.ProjectsQuery +import host.exp.exponent.services.RESTApiClient +import host.exp.expoview.R import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -27,6 +38,17 @@ import kotlinx.coroutines.flow.Flow ExperimentalCoroutinesApi::class, ExperimentalMaterial3ExpressiveApi::class ) +private fun shareProjectUrl(context: Context, fullName: String, projectName: String?) { + // Construct the URL in the format: exp://expo.dev/@user/project-slug + val projectUrl = "exp://${RESTApiClient.HOST}/$fullName" + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_SUBJECT, projectName ?: "Check out this Expo project") + putExtra(Intent.EXTRA_TEXT, projectUrl) + } + context.startActivity(Intent.createChooser(intent, "Share Project")) +} + @Composable fun ProjectDetailsScreen( viewModel: HomeAppViewModel, @@ -42,24 +64,44 @@ fun ProjectDetailsScreen( .branches(app?.id, 5) .collectAsStateWithLifecycle(initialValue = null) + val context = LocalContext.current + // Filter the branches to only include those that have updates. val branchesToRender = branches?.filter { it.updates.isNotEmpty() } - if (app == null) { - // TODO: show a proper loading state - Text("No app found") - return - } - Scaffold( topBar = { TopAppBarWithBackIcon( app?.name ?: "Project", - onGoBack = onGoBack + onGoBack = onGoBack, + actions = { + IconButton(onClick = { + app?.let { + shareProjectUrl(context, it.fullName, it.name) + } + }) { + Icon( + painter = painterResource(id = R.drawable.share), + contentDescription = "Share Project" + ) + } + } ) }, bottomBar = bottomBar ) { padding -> + + if (app == null) { + return@Scaffold Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + // Use a simple Box for the layout, as pull-to-refresh is not needed Column( modifier = Modifier @@ -74,7 +116,7 @@ fun ProjectDetailsScreen( ) { Text( text = app?.name ?: "Unnamed Project", - style = MaterialTheme.typography.bodyLargeEmphasized + style = MaterialTheme.typography.bodyLarge ) Spacer(modifier = Modifier.height(4.dp)) diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/TopAppBarWithBackIcon.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/TopAppBarWithBackIcon.kt index bc08da179a21ca..e47d40787737b6 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/TopAppBarWithBackIcon.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/TopAppBarWithBackIcon.kt @@ -14,7 +14,8 @@ import host.exp.expoview.R @Composable fun TopAppBarWithBackIcon( label: String, - onGoBack: () -> Unit + onGoBack: () -> Unit, + actions: @Composable () -> Unit = { } ) { TopAppBar( navigationIcon = { @@ -27,6 +28,7 @@ fun TopAppBarWithBackIcon( }, title = { Text(label, fontWeight = FontWeight.Bold) - } + }, + actions = { actions() } ) } diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/services/RESTApiClient.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/services/RESTApiClient.kt index 493f3cacdd4d14..dfb85ee56a18d0 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/services/RESTApiClient.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/services/RESTApiClient.kt @@ -11,18 +11,21 @@ import java.io.IOException import kotlin.reflect.KType import kotlin.reflect.javaType -const val apiV2BaseUrl = "https://exp.host/--/api/v2/" - class RESTApiClient(private val sessionRepository: SessionRepository) { private val client = OkHttpClient() private val gson = Gson() + companion object { + const val HOST = "exp.host" + const val API_V2_BASE_URL = "https://$HOST/--/api/v2/" + } + @OptIn(ExperimentalStdlibApi::class) suspend fun sendAuthenticatedApiV2Request(route: String, type: KType): T { val sessionSecret = sessionRepository.getSessionSecret() ?: throw IllegalStateException("Must be logged in to perform request") - val url = apiV2BaseUrl + route + val url = API_V2_BASE_URL + route val request = Request.Builder() .url(url) @@ -51,7 +54,7 @@ class RESTApiClient(private val sessionRepository: SessionRepository) { @OptIn(ExperimentalStdlibApi::class) suspend fun sendUnauthenticatedApiV2Request(route: String, type: KType, body: B? = null): T { - val url = apiV2BaseUrl + route + val url = API_V2_BASE_URL + route val requestBuilder = Request.Builder().url(url) diff --git a/apps/expo-go/android/expoview/src/main/res/drawable/share.xml b/apps/expo-go/android/expoview/src/main/res/drawable/share.xml new file mode 100644 index 00000000000000..184c8eba82df00 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/res/drawable/share.xml @@ -0,0 +1,10 @@ + + + diff --git a/apps/expo-go/ios/Client/AppDelegate.swift b/apps/expo-go/ios/Client/AppDelegate.swift index 243d94366884f9..c9f15a80a60130 100644 --- a/apps/expo-go/ios/Client/AppDelegate.swift +++ b/apps/expo-go/ios/Client/AppDelegate.swift @@ -41,7 +41,7 @@ class AppDelegate: ExpoAppDelegate { return } ExpoKit.sharedInstance().registerRootViewControllerClass(EXRootViewController.self) - ExpoKit.sharedInstance().prepare(launchOptions: launchOptions) + ExpoKit.sharedInstance().prepare() let window = UIWindow(frame: UIScreen.main.bounds) self.window = window diff --git a/apps/expo-go/ios/Client/EXRootViewController.m b/apps/expo-go/ios/Client/EXRootViewController.m index 3830a1a84a63e9..ef0c2cf49a79d0 100644 --- a/apps/expo-go/ios/Client/EXRootViewController.m +++ b/apps/expo-go/ios/Client/EXRootViewController.m @@ -254,13 +254,6 @@ - (void)addHistoryItemWithUrl:(NSURL *)manifestUrl manifest:(EXManifestsManifest iconUrl:iconUrl]; } -- (void)getHistoryUrlForScopeKey:(NSString *)scopeKey completion:(void (^)(NSString *))completion -{ - if (completion) { - completion(nil); - } -} - - (void)setIsNuxFinished:(BOOL)isFinished { [[NSUserDefaults standardUserDefaults] setBool:isFinished forKey:kEXHomeIsNuxFinishedDefaultsKey]; diff --git a/apps/expo-go/ios/Client/SwiftUI/GraphQL/APIClient.swift b/apps/expo-go/ios/Client/SwiftUI/GraphQL/APIClient.swift index b60262d1f261af..67cf91337968f4 100644 --- a/apps/expo-go/ios/Client/SwiftUI/GraphQL/APIClient.swift +++ b/apps/expo-go/ios/Client/SwiftUI/GraphQL/APIClient.swift @@ -2,10 +2,10 @@ import Foundation -class APIClient { +actor APIClient { static let shared = APIClient() - private var useStaging: Bool { + private nonisolated var useStaging: Bool { return false } @@ -27,13 +27,13 @@ class APIClient { : "https://exp.host/--/graphql" } - var apiOrigin: String { + nonisolated var apiOrigin: String { return useStaging ? "https://staging.exp.host" : "https://exp.host" } - var websiteOrigin: String { + nonisolated var websiteOrigin: String { return useStaging ? "https://staging.expo.dev" : "https://expo.dev" diff --git a/apps/expo-go/ios/Client/SwiftUI/HomeViewModel.swift b/apps/expo-go/ios/Client/SwiftUI/HomeViewModel.swift index 0f4db9f383745f..d212b45568e905 100644 --- a/apps/expo-go/ios/Client/SwiftUI/HomeViewModel.swift +++ b/apps/expo-go/ios/Client/SwiftUI/HomeViewModel.swift @@ -119,11 +119,11 @@ class HomeViewModel: ObservableObject { func refreshData() async { guard let account = selectedAccount else { return } - async let task = dataService.fetchProjectsAndData(accountName: account.name) - serverService.discoverDevelopmentServers() - serverService.refreshRemoteSessions() + async let fetchTask: Void = dataService.fetchProjectsAndData(accountName: account.name) + async let discoveryTask: Void = serverService.discoverDevelopmentServers() + async let remoteTask: Void = serverService.refreshRemoteSessions() - await task + _ = await (fetchTask, discoveryTask, remoteTask) } func addToRecentlyOpened(url: String, name: String, iconUrl: String? = nil) { diff --git a/apps/expo-go/ios/Client/SwiftUI/Rows/BranchRow.swift b/apps/expo-go/ios/Client/SwiftUI/Rows/BranchRow.swift index 8940dc2b304b18..e791e5276ae12a 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Rows/BranchRow.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Rows/BranchRow.swift @@ -40,7 +40,7 @@ struct BranchRowContent: View { .lineLimit(1) } } - + Text("Published \(formattedDate(update.createdAt))") .font(.caption) .foregroundColor(.secondary) @@ -57,7 +57,7 @@ struct BranchRowContent: View { .background(Color.expoSecondarySystemBackground) .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) } - + private func formattedDate(_ value: String) -> String { let formatters = [ isoFormatter(withFractionalSeconds: true), diff --git a/apps/expo-go/ios/Client/SwiftUI/Services/AuthenticationService.swift b/apps/expo-go/ios/Client/SwiftUI/Services/AuthenticationService.swift index 00a5b0f7ea3fe4..8b8e81f968323a 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Services/AuthenticationService.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Services/AuthenticationService.swift @@ -41,10 +41,10 @@ class AuthenticationService: ObservableObject { isAuthenticated = !(sessionSecret?.isEmpty ?? true) if isAuthenticated { - if let sessionSecret { - APIClient.shared.setSession(sessionSecret) - } Task { + if let sessionSecret { + await APIClient.shared.setSession(sessionSecret) + } await loadUserInfo() } } else { @@ -72,8 +72,10 @@ class AuthenticationService: ObservableObject { isAuthenticating = true defer { isAuthenticating = false } - let success = try await performAuthentication(isSignUp: false) - if success { + if let sessionSecret = try await performAuthentication(isSignUp: false) { + UserDefaults.standard.set(sessionSecret, forKey: sessionKey) + await APIClient.shared.setSession(sessionSecret) + isAuthenticated = true await loadUserInfo() } } @@ -82,8 +84,10 @@ class AuthenticationService: ObservableObject { isAuthenticating = true defer { isAuthenticating = false } - let success = try await performAuthentication(isSignUp: true) - if success { + if let sessionSecret = try await performAuthentication(isSignUp: true) { + UserDefaults.standard.set(sessionSecret, forKey: sessionKey) + await APIClient.shared.setSession(sessionSecret) + isAuthenticated = true await loadUserInfo() } } @@ -91,7 +95,9 @@ class AuthenticationService: ObservableObject { func signOut() { UserDefaults.standard.removeObject(forKey: sessionKey) UserDefaults.standard.removeObject(forKey: selectedAccountKey) - APIClient.shared.setSession(nil) + Task { + await APIClient.shared.setSession(nil) + } user = nil selectedAccountId = nil isAuthenticated = false @@ -102,11 +108,11 @@ class AuthenticationService: ObservableObject { UserDefaults.standard.set(accountId, forKey: selectedAccountKey) } - private func performAuthentication(isSignUp: Bool) async throws -> Bool { + private func performAuthentication(isSignUp: Bool) async throws -> String? { let scheme = try getURLScheme() + let websiteOrigin = APIClient.shared.websiteOrigin return try await withCheckedThrowingContinuation { continuation in - let websiteOrigin = APIClient.shared.websiteOrigin let authType = isSignUp ? "signup" : "login" let redirectBase = "\(scheme)://auth" @@ -132,12 +138,7 @@ class AuthenticationService: ObservableObject { return } - UserDefaults.standard.set(sessionSecret, forKey: self.sessionKey) - APIClient.shared.setSession(sessionSecret) - Task { @MainActor in - self.isAuthenticated = true - } - continuation.resume(returning: true) + continuation.resume(returning: sessionSecret) } session.presentationContextProvider = presentationContext diff --git a/apps/expo-go/ios/Client/SwiftUI/Services/DataService.swift b/apps/expo-go/ios/Client/SwiftUI/Services/DataService.swift index 990937a7aac707..d77f01733f6475 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Services/DataService.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Services/DataService.swift @@ -1,8 +1,6 @@ // Copyright 2015-present 650 Industries. All rights reserved. import Foundation -import Combine - @MainActor class DataService: ObservableObject { @Published var projects: [ExpoProject] = [] @@ -10,29 +8,24 @@ class DataService: ObservableObject { @Published var isLoadingData = false @Published var dataError: APIError? - private var pollingCancellables = Set() private let pollingInterval: TimeInterval = 10.0 + private var pollingTask: Task? func startPolling(accountName: String) { stopPolling() - Task { - await fetchProjectsAndData(accountName: accountName) - } - - Timer.publish(every: pollingInterval, on: .main, in: .common) - .autoconnect() - .receive(on: DispatchQueue.global(qos: .background)) - .sink { [weak self] _ in - Task { - await self?.fetchProjectsAndData(accountName: accountName) - } + pollingTask = Task { [weak self] in + guard let self else { return } + while !Task.isCancelled { + await self.fetchProjectsAndData(accountName: accountName) + try? await Task.sleep(nanoseconds: UInt64(self.pollingInterval * 1_000_000_000)) } - .store(in: &pollingCancellables) + } } func stopPolling() { - pollingCancellables.removeAll() + pollingTask?.cancel() + pollingTask = nil } func fetchProjectsAndData(accountName: String) async { @@ -49,9 +42,15 @@ class DataService: ObservableObject { ] ) + if Task.isCancelled { + return + } + self.projects = response.data.account.byName.apps.map { $0.toExpoProject() } self.snacks = response.data.account.byName.snacks self.dataError = nil + } catch is CancellationError { + return } catch let error as APIError { self.dataError = error } catch { diff --git a/apps/expo-go/ios/Client/SwiftUI/Services/DevelopmentServerService.swift b/apps/expo-go/ios/Client/SwiftUI/Services/DevelopmentServerService.swift index e536674fb0aae3..a125bcf26eeeb5 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Services/DevelopmentServerService.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Services/DevelopmentServerService.swift @@ -1,14 +1,12 @@ // Copyright 2015-present 650 Industries. All rights reserved. import Foundation -import Combine import UIKit @MainActor class DevelopmentServerService: ObservableObject { @Published var developmentServers: [DevelopmentServer] = [] - private var discoveryCancellables = Set() private let discoveryInterval: TimeInterval = 2.0 private let remoteRefreshInterval: TimeInterval = 10.0 private let remoteCacheKey = "expo-dev-sessions-cache" @@ -17,71 +15,84 @@ class DevelopmentServerService: ObservableObject { private var localServers: [DevelopmentServer] = [] private var remoteServers: [DevelopmentServer] = [] private var sessionSecret: String? + private var discoveryTask: Task? + private var remoteRefreshTask: Task? + private var isDiscovering = false + private var isFetchingRemote = false func startDiscovery() { stopDiscovery() - discoverDevelopmentServers() loadCachedRemoteSessions() - refreshRemoteSessions() - - Timer.publish(every: discoveryInterval, on: .main, in: .common) - .autoconnect() - .receive(on: DispatchQueue.global(qos: .background)) - .sink { [weak self] _ in - self?.discoverDevelopmentServers() - } - .store(in: &discoveryCancellables) - - Timer.publish(every: remoteRefreshInterval, on: .main, in: .common) - .autoconnect() - .receive(on: DispatchQueue.global(qos: .background)) - .sink { [weak self] _ in - self?.refreshRemoteSessions() - } - .store(in: &discoveryCancellables) + startDiscoveryLoop() + startRemoteRefreshLoop() } func stopDiscovery() { - discoveryCancellables.removeAll() + discoveryTask?.cancel() + remoteRefreshTask?.cancel() + discoveryTask = nil + remoteRefreshTask = nil } func setSessionSecret(_ sessionSecret: String?) { self.sessionSecret = sessionSecret - refreshRemoteSessions() + startRemoteRefreshLoop() } - func discoverDevelopmentServers() { - Task { - var discoveredServers: [DevelopmentServer] = [] - // swiftlint:disable number_separator - let portsToCheck = [8081, 8082, 8083, 8084, 8085, 19000, 19001, 19002] - // swiftlint:enable number_separator - let baseAddress = "http://localhost" - - await withTaskGroup(of: DevelopmentServer?.self) { group in - for port in portsToCheck { - group.addTask { - await self.checkDevelopmentServer(url: "\(baseAddress):\(port)") - } + func refreshRemoteSessions() async { + await fetchRemoteSessions() + } + + func discoverDevelopmentServers() async { + guard !isDiscovering else { + return + } + isDiscovering = true + defer { isDiscovering = false } + + var discoveredServers: [DevelopmentServer] = [] + // swiftlint:disable number_separator + let portsToCheck = [8081, 8082, 8083, 8084, 8085, 19000, 19001, 19002] + // swiftlint:enable number_separator + let baseAddress = "http://localhost" + + await withTaskGroup(of: DevelopmentServer?.self) { group in + for port in portsToCheck { + group.addTask { + await self.checkDevelopmentServer(url: "\(baseAddress):\(port)") } + } - for await server in group { - if let server = server { - discoveredServers.append(server) - } + for await server in group { + if let server = server { + discoveredServers.append(server) } } + } + + localServers = discoveredServers + updateDevelopmentServers() + } - await MainActor.run { - self.localServers = discoveredServers - self.updateDevelopmentServers() + private func startDiscoveryLoop() { + discoveryTask?.cancel() + discoveryTask = Task { [weak self] in + guard let self else { return } + while !Task.isCancelled { + await self.discoverDevelopmentServers() + try? await Task.sleep(nanoseconds: UInt64(self.discoveryInterval * 1_000_000_000)) } } } - func refreshRemoteSessions() { - Task { - await fetchRemoteSessions() + private func startRemoteRefreshLoop() { + remoteRefreshTask?.cancel() + remoteRefreshTask = Task { [weak self] in + guard let self else { return } + while !Task.isCancelled { + await self.fetchRemoteSessions() + try? await Task.sleep(nanoseconds: UInt64(self.remoteRefreshInterval * 1_000_000_000)) + } } } @@ -128,7 +139,7 @@ class DevelopmentServerService: ObservableObject { request.setValue("application/expo+json,application/json", forHTTPHeaderField: "Accept") request.setValue("ios", forHTTPHeaderField: "Expo-Platform") request.setValue("client", forHTTPHeaderField: "Expo-Client-Environment") - request.setValue(EXVersions.sharedInstance().sdkVersion, forHTTPHeaderField: "Expo-SDK-Version") + request.setValue(Versions.sharedInstance.sdkVersion, forHTTPHeaderField: "Expo-SDK-Version") do { let (data, response) = try await URLSession.shared.data(for: request) @@ -186,15 +197,19 @@ class DevelopmentServerService: ObservableObject { } private func fetchRemoteSessions() async { + guard !isFetchingRemote else { + return + } + isFetchingRemote = true + defer { isFetchingRemote = false } + guard Date() >= nextRemoteFetchAllowedAt else { return } guard let sessionSecret, !sessionSecret.isEmpty else { - await MainActor.run { - self.remoteServers = [] - self.updateDevelopmentServers() - } + self.remoteServers = [] + self.updateDevelopmentServers() return } @@ -206,7 +221,7 @@ class DevelopmentServerService: ObservableObject { request.httpMethod = "GET" request.setValue(sessionSecret, forHTTPHeaderField: "Expo-Session") request.setValue("ios", forHTTPHeaderField: "Expo-Platform") - request.setValue(EXVersions.sharedInstance().sdkVersion, forHTTPHeaderField: "Expo-SDK-Version") + request.setValue(Versions.sharedInstance.sdkVersion, forHTTPHeaderField: "Expo-SDK-Version") do { let (data, response) = try await URLSession.shared.data(for: request) @@ -237,10 +252,8 @@ class DevelopmentServerService: ObservableObject { ) } - await MainActor.run { - self.remoteServers = mappedServers - self.updateDevelopmentServers() - } + self.remoteServers = mappedServers + self.updateDevelopmentServers() cacheRemoteSessions(sessions) remoteFailureCount = 0 nextRemoteFetchAllowedAt = .distantPast @@ -266,7 +279,7 @@ class DevelopmentServerService: ObservableObject { } return normalizeUrl(server.url).lowercased() } - + private func preferredServer(existing: DevelopmentServer, candidate: DevelopmentServer) -> DevelopmentServer { if isLocalhostURL(existing.url) && !isLocalhostURL(candidate.url) { return candidate diff --git a/apps/expo-go/ios/Client/SwiftUI/Services/FeedbackService.swift b/apps/expo-go/ios/Client/SwiftUI/Services/FeedbackService.swift index 65567d9e2d8828..ac6d2374480e07 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Services/FeedbackService.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Services/FeedbackService.swift @@ -18,8 +18,8 @@ struct FeedbackService { feedback: message, email: email, metadata: FeedbackMetadata( - os: "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)", - model: UIDevice.current.model, + os: "\(await UIDevice.current.systemName) \(await UIDevice.current.systemVersion)", + model: await UIDevice.current.model, expoGoVersion: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ) ) diff --git a/apps/expo-go/ios/Client/SwiftUI/Services/SettingsManager.swift b/apps/expo-go/ios/Client/SwiftUI/Services/SettingsManager.swift index be4d111e4c3748..3cb744706c4942 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Services/SettingsManager.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Services/SettingsManager.swift @@ -47,13 +47,13 @@ class SettingsManager: ObservableObject { } private func loadBuildInfo() { - let buildConstants = EXBuildConstants.sharedInstance() - let versions = EXVersions.sharedInstance() + let buildConstants = BuildConstants.sharedInstance + let versions = Versions.sharedInstance buildInfo = [ "appName": Bundle.main.infoDictionary?["CFBundleDisplayName"] ?? "Expo Go", "appVersion": getFormattedAppVersion(), - "expoRuntimeVersion": buildConstants?.expoRuntimeVersion ?? "Unknown", + "expoRuntimeVersion": buildConstants.expoRuntimeVersion, "supportedExpoSdks": versions.sdkVersion, "appIcon": getAppIcon() ] @@ -88,21 +88,27 @@ class SettingsManager: ObservableObject { } private func applyThemeChange(_ themeIndex: Int) { - DispatchQueue.main.async { - guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return } - - UIView.transition(with: windowScene.windows.first ?? UIView(), duration: 0.3, options: .transitionCrossDissolve) { - switch themeIndex { - case 0: // Automatic - windowScene.windows.first?.overrideUserInterfaceStyle = .unspecified - case 1: // Light - windowScene.windows.first?.overrideUserInterfaceStyle = .light - case 2: // Dark - windowScene.windows.first?.overrideUserInterfaceStyle = .dark - default: - windowScene.windows.first?.overrideUserInterfaceStyle = .unspecified - } - } + guard let window = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .flatMap({ $0.windows }) + .first(where: { $0.isKeyWindow }) else { + return + } + + let style: UIUserInterfaceStyle + switch themeIndex { + case 0: // Automatic + style = .unspecified + case 1: // Light + style = .light + case 2: // Dark + style = .dark + default: + style = .unspecified + } + + UIView.transition(with: window, duration: 0.3, options: .transitionCrossDissolve) { + window.overrideUserInterfaceStyle = style } } } diff --git a/apps/expo-go/ios/Client/SwiftUI/Utils/SDKUtils.swift b/apps/expo-go/ios/Client/SwiftUI/Utils/SDKUtils.swift index 2fc69f5724081d..4d5cdd2850ce1c 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Utils/SDKUtils.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Utils/SDKUtils.swift @@ -1,15 +1,13 @@ // Copyright 2015-present 650 Industries. All rights reserved. func getSupportedSDKVersion() -> String { - return EXVersions.sharedInstance().sdkVersion + return Versions.sharedInstance.sdkVersion } func getSDKMajorVersion(_ sdkVersion: String) -> String { - return sdkVersion.components(separatedBy: ".").first ?? sdkVersion + return Versions.majorVersion(from: sdkVersion) } func isSDKCompatible(_ sdkVersion: String?) -> Bool { - guard let sdkVersion = sdkVersion else { return false } - let supportedSDK = getSupportedSDKVersion() - return getSDKMajorVersion(sdkVersion) == getSDKMajorVersion(supportedSDK) + return Versions.sharedInstance.isCompatible(sdkVersion: sdkVersion) } diff --git a/apps/expo-go/ios/Client/SwiftUI/Views/SnacksSection.swift b/apps/expo-go/ios/Client/SwiftUI/Views/SnacksSection.swift index 3b14d23e6124e8..57474f415957f6 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Views/SnacksSection.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Views/SnacksSection.swift @@ -39,30 +39,22 @@ struct SnackRowWithAction: View { } private func openSnack() { - guard isSDKCompatible(snack.sdkVersion) else { - let snackSDKMajor = getSDKMajorVersion(snack.sdkVersion) - let supportedSDKMajor = getSDKMajorVersion(getSupportedSDKVersion()) + let versions = Versions.sharedInstance + + guard versions.isCompatible(sdkVersion: snack.sdkVersion) else { + let snackSDKMajor = Versions.majorVersion(from: snack.sdkVersion) viewModel.showError( "Selected Snack uses unsupported SDK (\(snackSDKMajor))\n\n" + - "The currently running version of Expo Go supports SDK \(supportedSDKMajor) only. " + + "The currently running version of Expo Go supports SDK \(versions.majorVersion) only. " + "Update your Snack to this version to run it." ) return } - let supportedSDK = getSupportedSDKVersion() let encodedFullName = snack.fullName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? snack.fullName - let url = "exp://exp.host/\(encodedFullName)?sdkVersion=\(supportedSDK).0.0" + let url = "exp://exp.host/\(encodedFullName)?sdkVersion=\(versions.sdkVersion).0.0" viewModel.openApp(url: url) viewModel.addToRecentlyOpened(url: url, name: snack.name, iconUrl: nil) } - - private func getSupportedSDKVersion() -> String { - return EXVersions.sharedInstance().sdkVersion - } - - private func getSDKMajorVersion(_ sdkVersion: String) -> String { - return sdkVersion.components(separatedBy: ".").first ?? sdkVersion - } } diff --git a/apps/expo-go/ios/Exponent-Bridging-Header.h b/apps/expo-go/ios/Exponent-Bridging-Header.h index b0137cfe5b5642..72ba84e7d369fb 100644 --- a/apps/expo-go/ios/Exponent-Bridging-Header.h +++ b/apps/expo-go/ios/Exponent-Bridging-Header.h @@ -15,8 +15,6 @@ #import "ExpoKit.h" #import "EXKernel.h" #import "EXKernelLinkingManager.h" -#import "EXBuildConstants.h" -#import "EXVersions.h" #import "EXRootViewController.h" #import "EXAppViewController.h" #import "EXVersionManagerObjC.h" @@ -27,7 +25,6 @@ #import "EXDisabledDevLoadingView.h" #import "EXStatusBarManager.h" #import "EXKernelDevKeyCommands.h" -#import "EXClientReleaseType.h" #import "ExpoGoReactNativeFactory.h" #import "EXUtil.h" #import "EXReactAppManager.h" diff --git a/apps/expo-go/ios/Exponent/ExpoKit/EXViewController.m b/apps/expo-go/ios/Exponent/ExpoKit/EXViewController.m index 256165de82d2b6..4b35cf461c5a6c 100644 --- a/apps/expo-go/ios/Exponent/ExpoKit/EXViewController.m +++ b/apps/expo-go/ios/Exponent/ExpoKit/EXViewController.m @@ -1,11 +1,7 @@ // Copyright 2015-present 650 Industries. All rights reserved. -#import "EXEnvironment.h" -#import "EXKernel.h" #import "EXViewController.h" -#import "EXAppViewController.h" #import "ExpoKit.h" -#import "EXUtil.h" @implementation EXViewController @@ -36,22 +32,6 @@ - (BOOL)extendedLayoutIncludesOpaqueBars - (void)createRootAppAndMakeVisible { - NSURL *standaloneAppUrl = [NSURL URLWithString:nil]; - NSDictionary *initialProps = [[EXKernel sharedInstance] initialAppPropsFromLaunchOptions:[ExpoKit sharedInstance].launchOptions]; - EXKernelAppRecord *appRecord = [[EXKernel sharedInstance] createNewAppWithUrl:standaloneAppUrl - initialProps:initialProps]; - - UIViewController *viewControllerToShow = (UIViewController *)appRecord.viewController; - - [viewControllerToShow willMoveToParentViewController:self]; - [self.view addSubview:viewControllerToShow.view]; - [viewControllerToShow didMoveToParentViewController:self]; - - _contentViewController = viewControllerToShow; - [self.view setNeedsLayout]; - if (_delegate) { - [_delegate viewController:self didNavigateAppToVisible:appRecord]; - } } - (void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^_Nullable)(void))completion diff --git a/apps/expo-go/ios/Exponent/ExpoKit/ExpoKit.h b/apps/expo-go/ios/Exponent/ExpoKit/ExpoKit.h index ca96bd9404c8ec..af7ea57ea8c817 100644 --- a/apps/expo-go/ios/Exponent/ExpoKit/ExpoKit.h +++ b/apps/expo-go/ios/Exponent/ExpoKit/ExpoKit.h @@ -29,17 +29,7 @@ NS_ASSUME_NONNULL_BEGIN /** * Set up dependencies that need to be initialized before app delegates. */ -- (void)prepareWithLaunchOptions:(nullable NSDictionary *)launchOptions; - -/** - * Keys to third-party integrations used inside ExpoKit. - * TODO: document this. - */ -@property (nonatomic, strong) NSDictionary *applicationKeys; - -@property (nonatomic, readonly) NSDictionary *launchOptions; - -@property (nonatomic, weak) Class moduleRegistryDelegateClass; +- (void)prepare; @end diff --git a/apps/expo-go/ios/Exponent/ExpoKit/ExpoKit.m b/apps/expo-go/ios/Exponent/ExpoKit/ExpoKit.m index cdd46009da96da..be42cafc500cbf 100644 --- a/apps/expo-go/ios/Exponent/ExpoKit/ExpoKit.m +++ b/apps/expo-go/ios/Exponent/ExpoKit/ExpoKit.m @@ -2,15 +2,10 @@ #import "ExpoKit.h" #import "EXViewController.h" -#import "EXBuildConstants.h" -#import "EXEnvironment.h" #import "EXKernel.h" #import "EXKernelUtil.h" -#import "EXKernelLinkingManager.h" #import "EXReactAppExceptionHandler.h" -#import - @@ -20,7 +15,6 @@ @interface ExpoKit () } @property (nonatomic, nullable, strong) EXViewController *rootViewController; -@property (nonatomic, strong) NSDictionary *launchOptions; @end @@ -42,7 +36,6 @@ - (instancetype)init { if (self = [super init]) { _rootViewControllerClass = [EXViewController class]; - [self _initDefaultKeys]; } return self; } @@ -77,23 +70,10 @@ - (UIViewController *)currentViewController return controller; } -- (void)prepareWithLaunchOptions:(nullable NSDictionary *)launchOptions +- (void)prepare { [DDLog addLogger:[DDOSLogger sharedInstance]]; RCTSetFatalHandler(handleFatalReactError); - - _launchOptions = launchOptions; -} - -#pragma mark - internal - -- (void)_initDefaultKeys -{ - // these are provided in the expo/expo open source repo as defaults; they can all be overridden by setting - // the `applicationKeys` property on ExpoKit. - if ([EXBuildConstants sharedInstance].defaultApiKeys) { - self.applicationKeys = [EXBuildConstants sharedInstance].defaultApiKeys; - } } @end diff --git a/apps/expo-go/ios/Exponent/Kernel/AppLoader/AppFetcher/EXAppFetcher.m b/apps/expo-go/ios/Exponent/Kernel/AppLoader/AppFetcher/EXAppFetcher.m index 41fa1adf0d77bb..a33bf5b91a4d5b 100644 --- a/apps/expo-go/ios/Exponent/Kernel/AppLoader/AppFetcher/EXAppFetcher.m +++ b/apps/expo-go/ios/Exponent/Kernel/AppLoader/AppFetcher/EXAppFetcher.m @@ -3,10 +3,8 @@ #import "EXAppFetcher+Private.h" #import "EXAbstractLoader.h" #import "EXEnvironment.h" -#import "EXErrorRecoveryManager.h" #import "EXJavaScriptResource.h" #import "EXKernel.h" -#import "EXVersions.h" #import diff --git a/apps/expo-go/ios/Exponent/Kernel/AppLoader/CachedResource/EXCachedResource.m b/apps/expo-go/ios/Exponent/Kernel/AppLoader/CachedResource/EXCachedResource.m index 88981bd8815a65..0d8b17e7d5c4a0 100644 --- a/apps/expo-go/ios/Exponent/Kernel/AppLoader/CachedResource/EXCachedResource.m +++ b/apps/expo-go/ios/Exponent/Kernel/AppLoader/CachedResource/EXCachedResource.m @@ -1,11 +1,10 @@ // Copyright 2015-present 650 Industries. All rights reserved. #import "EXCachedResource.h" -#import "EXEnvironment.h" #import "EXFileDownloader.h" -#import "EXKernelUtil.h" #import "EXUtil.h" -#import "EXVersions.h" + +#import "Expo_Go-Swift.h" #import diff --git a/apps/expo-go/ios/Exponent/Kernel/AppLoader/CachedResource/EXJavaScriptResource.m b/apps/expo-go/ios/Exponent/Kernel/AppLoader/CachedResource/EXJavaScriptResource.m index 7f2358d8314b9e..7adf120b440888 100644 --- a/apps/expo-go/ios/Exponent/Kernel/AppLoader/CachedResource/EXJavaScriptResource.m +++ b/apps/expo-go/ios/Exponent/Kernel/AppLoader/CachedResource/EXJavaScriptResource.m @@ -1,8 +1,6 @@ // Copyright 2015-present 650 Industries. All rights reserved. -#import "EXEnvironment.h" #import "EXJavaScriptResource.h" -#import "EXKernelUtil.h" #import diff --git a/apps/expo-go/ios/Exponent/Kernel/AppLoader/CachedResource/EXManifestResource.m b/apps/expo-go/ios/Exponent/Kernel/AppLoader/CachedResource/EXManifestResource.m index 0ce9509f2f579f..93d1770e60c8c7 100644 --- a/apps/expo-go/ios/Exponent/Kernel/AppLoader/CachedResource/EXManifestResource.m +++ b/apps/expo-go/ios/Exponent/Kernel/AppLoader/CachedResource/EXManifestResource.m @@ -4,8 +4,9 @@ #import "EXEnvironment.h" #import "EXFileDownloader.h" #import "EXKernelLinkingManager.h" -#import "EXKernelUtil.h" -#import "EXVersions.h" +#import "EXUtil.h" + +#import "Expo_Go-Swift.h" #import @@ -180,7 +181,7 @@ + (NSString *)cachePath - (BOOL)_isThirdPartyHosted { - return (self.remoteUrl && ![EXKernelLinkingManager isExpoHostedUrl:self.remoteUrl]); + return (self.remoteUrl && ![EXUtil isExpoHostedUrl:self.remoteUrl]); } - (BOOL)_isManifestVerificationBypassed: (id) manifestObj diff --git a/apps/expo-go/ios/Exponent/Kernel/AppLoader/EXAbstractLoader.h b/apps/expo-go/ios/Exponent/Kernel/AppLoader/EXAbstractLoader.h index 5d17ae730c3152..ed41738e56e6a2 100644 --- a/apps/expo-go/ios/Exponent/Kernel/AppLoader/EXAbstractLoader.h +++ b/apps/expo-go/ios/Exponent/Kernel/AppLoader/EXAbstractLoader.h @@ -72,11 +72,6 @@ typedef enum EXAppLoaderRemoteUpdateStatus { */ - (void)requestFromCache; -/** - * Tell this AppLoader that everything has finished successfully and its manifest resource can be cached. - */ -- (void)writeManifestToCache; - /** * Reset status to `kEXAppLoaderStatusHasManifest` and fetch the bundle at the existing * manifest. This is called when RN devtools reload an AppManager/RCTBridge directly @@ -92,11 +87,6 @@ typedef enum EXAppLoaderRemoteUpdateStatus { */ - (BOOL)supportsBundleReload; -/** - * Fetch manifest without any side effects or interaction with the timer. - */ -- (void)fetchManifestWithCacheBehavior:(EXManifestCacheBehavior)cacheBehavior success:(void (^)(EXManifestsManifest *))success failure:(void (^)(NSError *))failure; - @end NS_ASSUME_NONNULL_END diff --git a/apps/expo-go/ios/Exponent/Kernel/AppLoader/EXAbstractLoader.m b/apps/expo-go/ios/Exponent/Kernel/AppLoader/EXAbstractLoader.m index 5d850cc9410fca..78c69cc0b97fae 100644 --- a/apps/expo-go/ios/Exponent/Kernel/AppLoader/EXAbstractLoader.m +++ b/apps/expo-go/ios/Exponent/Kernel/AppLoader/EXAbstractLoader.m @@ -1,17 +1,6 @@ // Copyright 2015-present 650 Industries. All rights reserved. -#import "EXEnvironment.h" -#import "EXErrorRecoveryManager.h" -#import "EXFileDownloader.h" -#import "EXKernel.h" -#import "EXAppFetcher.h" #import "EXAbstractLoader.h" -#import "EXKernelAppRecord.h" -#import "EXKernelAppRegistry.h" -#import "EXKernelLinkingManager.h" -#import "EXManifestResource.h" - -#import @import EXManifests; @@ -34,11 +23,6 @@ - (instancetype)initWithLocalManifest:(EXManifestsManifest *)manifest return nil; } -- (void)fetchManifestWithCacheBehavior:(EXManifestCacheBehavior)cacheBehavior success:(void (^)(EXManifestsManifest * _Nonnull))success failure:(void (^)(NSError * _Nonnull))failure -{ - [self doesNotRecognizeSelector:_cmd]; -} - - (void)request { [self doesNotRecognizeSelector:_cmd]; @@ -60,11 +44,6 @@ - (BOOL)supportsBundleReload return NO; } -- (void)writeManifestToCache -{ - [self doesNotRecognizeSelector:_cmd]; -} - #pragma mark - #pragma mark EXAppFetcher delegate methods diff --git a/apps/expo-go/ios/Exponent/Kernel/AppLoader/EXAppLoaderExpoUpdates.m b/apps/expo-go/ios/Exponent/Kernel/AppLoader/EXAppLoaderExpoUpdates.m index 9975d24db2df36..4cb9a3439fe7a8 100644 --- a/apps/expo-go/ios/Exponent/Kernel/AppLoader/EXAppLoaderExpoUpdates.m +++ b/apps/expo-go/ios/Exponent/Kernel/AppLoader/EXAppLoaderExpoUpdates.m @@ -2,16 +2,12 @@ #import "EXAppFetcher.h" #import "EXAppLoaderExpoUpdates.h" -#import "EXClientReleaseType.h" #import "EXEnvironment.h" -#import "EXErrorRecoveryManager.h" #import "EXFileDownloader.h" #import "EXKernel.h" #import "EXKernelLinkingManager.h" #import "EXManifestResource.h" -#import "EXSession.h" #import "EXUpdatesDatabaseManager.h" -#import "EXVersions.h" #import "Expo_Go-Swift.h" diff --git a/apps/expo-go/ios/Exponent/Kernel/AppLoader/EXFileDownloader.m b/apps/expo-go/ios/Exponent/Kernel/AppLoader/EXFileDownloader.m index 2f4b38a79dbddd..d7d724ebe17aec 100644 --- a/apps/expo-go/ios/Exponent/Kernel/AppLoader/EXFileDownloader.m +++ b/apps/expo-go/ios/Exponent/Kernel/AppLoader/EXFileDownloader.m @@ -2,10 +2,8 @@ #import "EXEnvironment.h" #import "EXFileDownloader.h" -#import "EXSession.h" -#import "EXVersions.h" -#import "EXKernelUtil.h" -#import "EXClientReleaseType.h" + +#import "Expo_Go-Swift.h" #import diff --git a/apps/expo-go/ios/Exponent/Kernel/Core/EXAppBrowserController.h b/apps/expo-go/ios/Exponent/Kernel/Core/EXAppBrowserController.h index a3a1b622de2b4e..9e99ff70190d08 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Core/EXAppBrowserController.h +++ b/apps/expo-go/ios/Exponent/Kernel/Core/EXAppBrowserController.h @@ -10,7 +10,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)moveHomeToVisible; - (void)reloadVisibleApp; - (void)addHistoryItemWithUrl:(NSURL *)manifestUrl manifest:(EXManifestsManifest *)manifest; -- (void)getHistoryUrlForScopeKey:(NSString *)scopeKey completion:(void (^)(NSString * _Nullable))completion; - (BOOL)isNuxFinished; - (void)setIsNuxFinished:(BOOL)isFinished; - (void)appDidFinishLoadingSuccessfully:(EXKernelAppRecord *)appRecord; diff --git a/apps/expo-go/ios/Exponent/Kernel/Core/EXKernel.h b/apps/expo-go/ios/Exponent/Kernel/Core/EXKernel.h index faa850b0a04adb..251781fb0547ee 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Core/EXKernel.h +++ b/apps/expo-go/ios/Exponent/Kernel/Core/EXKernel.h @@ -31,11 +31,6 @@ typedef NS_ENUM(NSInteger, EXKernelErrorCode) { - (void)reloadAppFromCacheWithScopeKey:(NSString *)scopeKey; // called by Updates.reloadFromCache - (void)reloadVisibleApp; // called in development whenever the app is reloaded -/** - * Initial props to pass to an app based on LaunchOptions from UIApplicationDelegate. - */ -- (nullable NSDictionary *)initialAppPropsFromLaunchOptions:(NSDictionary *)launchOptions; - /** * Find and return the (potentially versioned) native module instance belonging to the * specified app manager. Module name is the exported name such as @"AppState". diff --git a/apps/expo-go/ios/Exponent/Kernel/Core/EXKernel.m b/apps/expo-go/ios/Exponent/Kernel/Core/EXKernel.m index 1f54a2c7823c32..f7d789f12df5fb 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Core/EXKernel.m +++ b/apps/expo-go/ios/Exponent/Kernel/Core/EXKernel.m @@ -2,13 +2,13 @@ #import "EXAppState.h" #import "EXAppViewController.h" -#import "EXBuildConstants.h" #import "EXKernel.h" + +#import "Expo_Go-Swift.h" #import "EXAbstractLoader.h" #import "EXKernelAppRecord.h" #import "EXKernelLinkingManager.h" #import "EXLinkingManager.h" -#import "EXVersions.h" #import "EXKernelDevKeyCommands.h" #import @@ -133,13 +133,6 @@ - (void)_postNotificationName: (NSNotificationName)name [[NSNotificationCenter defaultCenter] postNotificationName:name object:nil]; } -#pragma mark - App props - -- (nullable NSDictionary *)initialAppPropsFromLaunchOptions:(NSDictionary *)launchOptions -{ - return nil; -} - #pragma mark - App State - (EXKernelAppRecord *)createNewAppWithUrl:(NSURL *)url initialProps:(nullable NSDictionary *)initialProps diff --git a/apps/expo-go/ios/Exponent/Kernel/Core/EXKernelAppRegistry.m b/apps/expo-go/ios/Exponent/Kernel/Core/EXKernelAppRegistry.m index 69e76a97683602..167553a9fe5297 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Core/EXKernelAppRegistry.m +++ b/apps/expo-go/ios/Exponent/Kernel/Core/EXKernelAppRegistry.m @@ -2,7 +2,6 @@ #import "EXKernelAppRegistry.h" #import "EXAbstractLoader.h" -#import "EXEnvironment.h" #import "EXReactAppManager.h" #import "EXKernel.h" diff --git a/apps/expo-go/ios/Exponent/Kernel/Core/EXKernelServiceRegistry.m b/apps/expo-go/ios/Exponent/Kernel/Core/EXKernelServiceRegistry.m index 36e074814d71dd..8366f83d8f8735 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Core/EXKernelServiceRegistry.m +++ b/apps/expo-go/ios/Exponent/Kernel/Core/EXKernelServiceRegistry.m @@ -1,8 +1,9 @@ // Copyright 2015-present 650 Industries. All rights reserved. #import "EXKernelServiceRegistry.h" -#import "EXErrorRecoveryManager.h" #import "EXKernelAppRegistry.h" + +#import "Expo_Go-Swift.h" #import "EXKernelLinkingManager.h" #import "EXSensorManager.h" #import "EXUpdatesDatabaseManager.h" diff --git a/apps/expo-go/ios/Exponent/Kernel/DevSupport/EXKernelDevKeyCommands.m b/apps/expo-go/ios/Exponent/Kernel/DevSupport/EXKernelDevKeyCommands.m index fd8cef66fa4642..8da2dbc3de9f52 100644 --- a/apps/expo-go/ios/Exponent/Kernel/DevSupport/EXKernelDevKeyCommands.m +++ b/apps/expo-go/ios/Exponent/Kernel/DevSupport/EXKernelDevKeyCommands.m @@ -1,6 +1,5 @@ // Copyright 2015-present 650 Industries. All rights reserved. -#import "EXEnvironment.h" #import "EXKernelDevKeyCommands.h" #import "EXKernel.h" #import "EXKernelAppRegistry.h" diff --git a/apps/expo-go/ios/Exponent/Kernel/DevSupport/EXSession.h b/apps/expo-go/ios/Exponent/Kernel/DevSupport/EXSession.h deleted file mode 100644 index db76e55ba4df71..00000000000000 --- a/apps/expo-go/ios/Exponent/Kernel/DevSupport/EXSession.h +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2015-present 650 Industries. All rights reserved. - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface EXSession : NSObject - -+ (instancetype)sharedInstance; - -- (NSDictionary * _Nullable)session; -- (NSString * _Nullable)sessionSecret; -- (BOOL)saveSessionToKeychain:(NSDictionary *)session error:(NSError **)error; -- (BOOL)deleteSessionFromKeychainWithError:(NSError **)error; - -@end - -NS_ASSUME_NONNULL_END diff --git a/apps/expo-go/ios/Exponent/Kernel/DevSupport/EXSession.m b/apps/expo-go/ios/Exponent/Kernel/DevSupport/EXSession.m deleted file mode 100644 index 2538b2ed3b8796..00000000000000 --- a/apps/expo-go/ios/Exponent/Kernel/DevSupport/EXSession.m +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2015-present 650 Industries. All rights reserved. - -#import "EXSession.h" - -NSString * const kEXSessionKeychainKey = @"host.exp.exponent.session"; -NSString * const kEXSessionKeychainService = @"app"; - -@interface EXSession () - -@property (nonatomic, strong) NSDictionary *session; - -@end - -@implementation EXSession - -+ (nonnull instancetype)sharedInstance -{ - static EXSession *theSession; - static dispatch_once_t once; - dispatch_once(&once, ^{ - if (!theSession) { - theSession = [[EXSession alloc] init]; - } - }); - return theSession; -} - -- (NSDictionary * _Nullable)session -{ - if (_session) { - return _session; - } - NSMutableDictionary *query = [NSMutableDictionary dictionaryWithDictionary:@{ - (__bridge id)kSecMatchLimit:(__bridge id)kSecMatchLimitOne, - (__bridge id)kSecReturnData:(__bridge id)kCFBooleanTrue - }]; - [query addEntriesFromDictionary:[self _searchQuery]]; - - CFTypeRef foundDict = NULL; - OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &foundDict); - - if (status == noErr) { - NSData *result = (__bridge_transfer NSData *)foundDict; - NSError *jsonError; - id session = [NSJSONSerialization JSONObjectWithData:result - options:kNilOptions - error:&jsonError]; - if (!jsonError && [session isKindOfClass:[NSDictionary class]]) { - return (NSDictionary *)session; - } - } - return nil; -} - -- (NSString * _Nullable)sessionSecret -{ - NSDictionary *session = [self session]; - if (!session) { - return nil; - } - - id sessionSecret = session[@"sessionSecret"]; - if (sessionSecret && [sessionSecret isKindOfClass:[NSString class]]) { - return (NSString *)sessionSecret; - } - return nil; -} - -- (BOOL)saveSessionToKeychain:(NSDictionary *)session error:(NSError **)error -{ - NSError *jsonError; - NSData *encodedData = [NSJSONSerialization dataWithJSONObject:session - options:kNilOptions - error:&jsonError]; - if (jsonError) { - if (error) { - *error = [NSError errorWithDomain:@"EXKernelErrorDomain" - code:-1 - userInfo:@{ - NSLocalizedDescriptionKey: @"Could not serialize JSON to save session to keychain", - NSUnderlyingErrorKey: jsonError - }]; - } - return NO; - } - - NSDictionary *searchQuery = [self _searchQuery]; - NSDictionary *updateQuery = @{ (__bridge id)kSecValueData:encodedData }; - NSMutableDictionary *addQuery = [NSMutableDictionary dictionaryWithDictionary:searchQuery]; - [addQuery addEntriesFromDictionary:updateQuery]; - - OSStatus status = SecItemAdd((__bridge CFDictionaryRef)addQuery, NULL); - - if (status == errSecDuplicateItem) { - status = SecItemUpdate((__bridge CFDictionaryRef)searchQuery, (__bridge CFDictionaryRef)updateQuery); - } - - if (status == errSecSuccess) { - _session = session; - return YES; - } else { - if (error) { - *error = [NSError errorWithDomain:@"EXKernelErrorDomain" - code:-1 - userInfo:@{ NSLocalizedDescriptionKey: @"Could not save session to keychain" }]; - } - return NO; - } -} - -- (BOOL)deleteSessionFromKeychainWithError:(NSError **)error -{ - OSStatus status = SecItemDelete((__bridge CFDictionaryRef)[self _searchQuery]); - - if (status == errSecSuccess || status == errSecItemNotFound) { - _session = nil; - return YES; - } else { - if (error) { - *error = [NSError errorWithDomain:@"EXKernelErrorDomain" - code:-1 - userInfo:@{ NSLocalizedDescriptionKey: @"Could not delete session from keychain" }]; - } - return NO; - } -} - -- (NSDictionary *)_searchQuery -{ - NSData *encodedKey = [kEXSessionKeychainKey dataUsingEncoding:NSUTF8StringEncoding]; - return @{ - (__bridge id)kSecClass:(__bridge id)kSecClassGenericPassword, - (__bridge id)kSecAttrService:kEXSessionKeychainService, - (__bridge id)kSecAttrGeneric:encodedKey, - (__bridge id)kSecAttrAccount:encodedKey - }; -} - -@end diff --git a/apps/expo-go/ios/Exponent/Kernel/DevSupport/Session.swift b/apps/expo-go/ios/Exponent/Kernel/DevSupport/Session.swift new file mode 100644 index 00000000000000..cf15147129c897 --- /dev/null +++ b/apps/expo-go/ios/Exponent/Kernel/DevSupport/Session.swift @@ -0,0 +1,118 @@ +// Copyright 2015-present 650 Industries. All rights reserved. + +import Foundation +import Security + +@objc(EXSession) +@objcMembers +public final class Session: NSObject { + private static let keychainKey = "host.exp.exponent.session" + private static let keychainService = "app" + + public static let sharedInstance = Session() + private var cachedSession: NSDictionary? + + private override init() { + super.init() + } + + public func session() -> NSDictionary? { + if let cached = cachedSession { + return cached + } + + var query = defaultSearchQuery() + query[kSecMatchLimit as String] = kSecMatchLimitOne + query[kSecReturnData as String] = kCFBooleanTrue + + var foundItem: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &foundItem) + + guard status == errSecSuccess, + let data = foundItem as? Data else { + return nil + } + + do { + if let session = try JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary { + return session + } + } catch { + NSLog("[EXSession] Error deserializing session: %@", error.localizedDescription) + } + + return nil + } + + public func sessionSecret() -> String? { + guard let session = session() else { + return nil + } + + return session["sessionSecret"] as? String + } + + @objc(saveSessionToKeychain:error:) + public func saveSession(toKeychain session: NSDictionary) throws { + let data: Data + do { + data = try JSONSerialization.data(withJSONObject: session, options: []) + } catch { + throw NSError( + domain: "EXKernelErrorDomain", + code: -1, + userInfo: [ + NSLocalizedDescriptionKey: "Could not serialize JSON to save session to keychain", + NSUnderlyingErrorKey: error + ] + ) + } + + let searchQuery = self.defaultSearchQuery() + let updateQuery: [String: Any] = [kSecValueData as String: data] + + var addQuery = searchQuery + addQuery.merge(updateQuery) { _, new in new } + + var status = SecItemAdd(addQuery as CFDictionary, nil) + + if status == errSecDuplicateItem { + status = SecItemUpdate(searchQuery as CFDictionary, updateQuery as CFDictionary) + } + + if status == errSecSuccess { + cachedSession = session + } else { + throw NSError( + domain: "EXKernelErrorDomain", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "Could not save session to keychain"] + ) + } + } + + @objc(deleteSessionFromKeychainWithError:) + public func deleteSessionFromKeychain() throws { + let status = SecItemDelete(defaultSearchQuery() as CFDictionary) + + if status == errSecSuccess || status == errSecItemNotFound { + cachedSession = nil + } else { + throw NSError( + domain: "EXKernelErrorDomain", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "Could not delete session from keychain"] + ) + } + } + + private func defaultSearchQuery() -> [String: Any] { + let encodedKey = Self.keychainKey.data(using: .utf8)! + return [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: Self.keychainService, + kSecAttrGeneric as String: encodedKey, + kSecAttrAccount as String: encodedKey + ] + } +} diff --git a/apps/expo-go/ios/Exponent/Kernel/Environment/BuildConstants.swift b/apps/expo-go/ios/Exponent/Kernel/Environment/BuildConstants.swift new file mode 100644 index 00000000000000..274f27a312a73e --- /dev/null +++ b/apps/expo-go/ios/Exponent/Kernel/Environment/BuildConstants.swift @@ -0,0 +1,35 @@ +// Copyright 2015-present 650 Industries. All rights reserved. + +import Foundation + +@objc(EXBuildConstants) +@objcMembers +public final class BuildConstants: NSObject { + public static let sharedInstance = BuildConstants() + + public private(set) var apiServerEndpoint: URL? + public var sdkVersion: String? + public var expoRuntimeVersion: String = "" + + private override init() { + super.init() + loadConfig() + } + + private func loadConfig() { + guard let plistPath = Bundle.main.path(forResource: "EXBuildConstants", ofType: "plist"), + let config = NSDictionary(contentsOfFile: plistPath) as? [String: Any] else { + return + } + + if let apiServerString = config["API_SERVER_ENDPOINT"] as? String { + apiServerEndpoint = URL(string: apiServerString) + } + + sdkVersion = config["TEMPORARY_SDK_VERSION"] as? String + + if let runtimeVersion = config["EXPO_RUNTIME_VERSION"] as? String { + expoRuntimeVersion = runtimeVersion + } + } +} diff --git a/apps/expo-go/ios/Exponent/Kernel/Environment/ClientReleaseType.swift b/apps/expo-go/ios/Exponent/Kernel/Environment/ClientReleaseType.swift new file mode 100644 index 00000000000000..de3c88bab710b9 --- /dev/null +++ b/apps/expo-go/ios/Exponent/Kernel/Environment/ClientReleaseType.swift @@ -0,0 +1,38 @@ +// Copyright 2015-present 650 Industries. All rights reserved. + +import Foundation +import EXApplication + +@objc(EXClientReleaseType) +@objcMembers +public final class ClientReleaseType: NSObject { + public static func clientReleaseType() -> String { + // The only scenario in which we care about the app release type is when the App Store release of + // the Expo development client is run on a real device so the development client knows to restrict + // projects it can run. We always include expo-application in the App Store release of the + // development client, so we correctly return "APPLE_APP_STORE" in the aforementioned scenario. + // + // In all other scenarios, we don't restrict the projects the client can run and can return either + // the actual release type or "UNKNOWN" for the same behavior, so it doesn't matter whether + // expo-application is linked. + + let releaseType = EXProvisioningProfile.main().appReleaseType() + + switch releaseType { + case .typeUnknown: + return "UNKNOWN" + case .simulator: + return "SIMULATOR" + case .enterprise: + return "ENTERPRISE" + case .dev: + return "DEVELOPMENT" + case .adHoc: + return "ADHOC" + case .appStore: + return "APPLE_APP_STORE" + @unknown default: + return "UNKNOWN" + } + } +} diff --git a/apps/expo-go/ios/Exponent/Kernel/Environment/EXBuildConstants.h b/apps/expo-go/ios/Exponent/Kernel/Environment/EXBuildConstants.h deleted file mode 100644 index 7dff127ae771ff..00000000000000 --- a/apps/expo-go/ios/Exponent/Kernel/Environment/EXBuildConstants.h +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2015-present 650 Industries. All rights reserved. - -#import - -typedef enum EXKernelDevManifestSource { - kEXKernelDevManifestSourceNone, - kEXKernelDevManifestSourceLocal, - kEXKernelDevManifestSourcePublished, -} EXKernelDevManifestSource; - -@interface EXBuildConstants : NSObject - -+ (instancetype)sharedInstance; - -@property (nonatomic, readonly) BOOL isDevKernel; -@property (nonatomic, readonly) NSDictionary *defaultApiKeys; -@property (nonatomic, readonly) EXKernelDevManifestSource kernelDevManifestSource; -@property (nonatomic, readonly) NSString *kernelManifestAndAssetRequestHeadersJsonString; -@property (nonatomic, readonly) NSURL *apiServerEndpoint; -@property (nonatomic, strong) NSString *sdkVersion; -@property (nonatomic, strong) NSString *expoRuntimeVersion; - -@end diff --git a/apps/expo-go/ios/Exponent/Kernel/Environment/EXBuildConstants.m b/apps/expo-go/ios/Exponent/Kernel/Environment/EXBuildConstants.m deleted file mode 100644 index 4c6a2e2784c5ca..00000000000000 --- a/apps/expo-go/ios/Exponent/Kernel/Environment/EXBuildConstants.m +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2015-present 650 Industries. All rights reserved. - -#import "EXBuildConstants.h" - -@implementation EXBuildConstants - -+ (instancetype)sharedInstance -{ - static EXBuildConstants *theBuildConstants; - static dispatch_once_t once; - dispatch_once(&once, ^{ - if (!theBuildConstants) { - theBuildConstants = [[EXBuildConstants alloc] init]; - } - }); - return theBuildConstants; -} - -- (instancetype)init -{ - if (self = [super init]) { - [self _loadConfig]; - } - return self; -} - -#pragma mark - internal - -- (void)_reset -{ - _expoRuntimeVersion = @""; -} - -- (void)_loadConfig -{ - [self _reset]; - - NSString *plistPath = [[NSBundle mainBundle] pathForResource:@"EXBuildConstants" ofType:@"plist"]; - NSDictionary *config = (plistPath) ? [NSDictionary dictionaryWithContentsOfFile:plistPath] : [NSDictionary dictionary]; - _isDevKernel = [config[@"IS_DEV_KERNEL"] boolValue]; - _kernelDevManifestSource = [[self class] _kernelManifestSourceFromString:config[@"DEV_KERNEL_SOURCE"]]; - if (_kernelDevManifestSource == kEXKernelDevManifestSourceLocal) { - // local kernel. use manifest and assetRequestHeaders from local server. - _kernelManifestAndAssetRequestHeadersJsonString = config[@"BUILD_MACHINE_KERNEL_MANIFEST"]; - } else if (_kernelDevManifestSource == kEXKernelDevManifestSourcePublished) { - // dev published kernel. use published manifest and assetRequestHeaders. - _kernelManifestAndAssetRequestHeadersJsonString = config[@"DEV_PUBLISHED_KERNEL_MANIFEST"]; - } - _apiServerEndpoint = [NSURL URLWithString:config[@"API_SERVER_ENDPOINT"]]; - _sdkVersion = config[@"TEMPORARY_SDK_VERSION"]; - if (config[@"EXPO_RUNTIME_VERSION"]) { - _expoRuntimeVersion = config[@"EXPO_RUNTIME_VERSION"]; - } - if (config[@"DEFAULT_API_KEYS"]) { - _defaultApiKeys = config[@"DEFAULT_API_KEYS"]; - } -} - -+ (EXKernelDevManifestSource)_kernelManifestSourceFromString:(NSString *)sourceString -{ - if ([sourceString isEqualToString:@"LOCAL"]) { - return kEXKernelDevManifestSourceLocal; - } else if ([sourceString isEqualToString:@"PUBLISHED"]) { - return kEXKernelDevManifestSourcePublished; - } - return kEXKernelDevManifestSourceNone; -} - -@end diff --git a/apps/expo-go/ios/Exponent/Kernel/Environment/EXClientReleaseType.h b/apps/expo-go/ios/Exponent/Kernel/Environment/EXClientReleaseType.h deleted file mode 100644 index d75b8f218430c5..00000000000000 --- a/apps/expo-go/ios/Exponent/Kernel/Environment/EXClientReleaseType.h +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2015-present 650 Industries. All rights reserved. - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface EXClientReleaseType : NSObject - -+ (NSString *)clientReleaseType; - -@end - -NS_ASSUME_NONNULL_END diff --git a/apps/expo-go/ios/Exponent/Kernel/Environment/EXClientReleaseType.m b/apps/expo-go/ios/Exponent/Kernel/Environment/EXClientReleaseType.m deleted file mode 100644 index 5b1f521c3f044e..00000000000000 --- a/apps/expo-go/ios/Exponent/Kernel/Environment/EXClientReleaseType.m +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2015-present 650 Industries. All rights reserved. - -#import "EXClientReleaseType.h" -#if __has_include() -#import -#endif - -@implementation EXClientReleaseType - -+ (NSString *)clientReleaseType -{ - // The only scenario in which we care about the app release type is when the App Store release of - // the Expo development client is run on a real device so the development client knows to restrict - // projects it can run. We always include expo-application in the App Store release of the - // development client, so we correctly return "APPLE_APP_STORE" in the aforementioned scenario. - // - // In all other scenarios, we don't restrict the projects the client can run and can return either - // the actual release type or "UNKNOWN" for the same behavior, so it doesn't matter whether - // expo-application is linked. -#if __has_include() - EXAppReleaseType releaseType = [[EXProvisioningProfile mainProvisioningProfile] appReleaseType]; - switch (releaseType) { - case EXAppReleaseTypeUnknown: - return @"UNKNOWN"; - case EXAppReleaseSimulator: - return @"SIMULATOR"; - case EXAppReleaseEnterprise: - return @"ENTERPRISE"; - case EXAppReleaseDev: - return @"DEVELOPMENT"; - case EXAppReleaseAdHoc: - return @"ADHOC"; - case EXAppReleaseAppStore: - return @"APPLE_APP_STORE"; - } -#else - return @"UNKNOWN"; -#endif -} - -@end diff --git a/apps/expo-go/ios/Exponent/Kernel/Environment/EXEnvironment.m b/apps/expo-go/ios/Exponent/Kernel/Environment/EXEnvironment.m index 1bd9ab385f1e32..676315e308eb40 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Environment/EXEnvironment.m +++ b/apps/expo-go/ios/Exponent/Kernel/Environment/EXEnvironment.m @@ -1,10 +1,10 @@ // Copyright 2015-present 650 Industries. All rights reserved. -#import "EXBuildConstants.h" -#import "EXKernelUtil.h" #import "ExpoKit.h" #import "EXEnvironment.h" +#import "Expo_Go-Swift.h" + #import @implementation EXEnvironment diff --git a/apps/expo-go/ios/Exponent/Kernel/Environment/EXVersions.h b/apps/expo-go/ios/Exponent/Kernel/Environment/EXVersions.h deleted file mode 100644 index 742b0d56d4eea4..00000000000000 --- a/apps/expo-go/ios/Exponent/Kernel/Environment/EXVersions.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2015-present 650 Industries. All rights reserved. - -#import - -@class EXManifestsManifest; - -NS_ASSUME_NONNULL_BEGIN - -@interface EXVersions : NSObject - -+ (nonnull instancetype)sharedInstance; - -@property (nonatomic, readonly, nonnull) NSString *sdkVersion; - -- (NSString *)availableSdkVersionForManifest: (EXManifestsManifest * _Nullable)manifest; -- (BOOL)supportsVersion:(NSString *)sdkVersion; - -@end - -NS_ASSUME_NONNULL_END diff --git a/apps/expo-go/ios/Exponent/Kernel/Environment/EXVersions.m b/apps/expo-go/ios/Exponent/Kernel/Environment/EXVersions.m deleted file mode 100644 index bf130bf752b0b8..00000000000000 --- a/apps/expo-go/ios/Exponent/Kernel/Environment/EXVersions.m +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2015-present 650 Industries. All rights reserved. - -#import "EXBuildConstants.h" -#import "EXVersions.h" -#import "EXKernelUtil.h" - -@import EXManifests; - -@implementation EXVersions - -+ (nonnull instancetype)sharedInstance -{ - static EXVersions *theVersions; - static dispatch_once_t once; - dispatch_once(&once, ^{ - if (!theVersions) { - theVersions = [[EXVersions alloc] init]; - } - }); - return theVersions; -} - -- (instancetype)init -{ - if (self = [super init]) { - _sdkVersion = [EXBuildConstants sharedInstance].sdkVersion; - } - return self; -} - -- (NSString *)availableSdkVersionForManifest:(EXManifestsManifest * _Nullable)manifest -{ - if (manifest && manifest.expoGoSDKVersion) { - if ([manifest.expoGoSDKVersion isEqualToString:_sdkVersion]) { - return _sdkVersion; - } - } - return @""; -} - -- (BOOL)supportsVersion:(NSString *)sdkVersion -{ - return [_sdkVersion isEqualToString:sdkVersion]; -} - -@end diff --git a/apps/expo-go/ios/Exponent/Kernel/Environment/Versions.swift b/apps/expo-go/ios/Exponent/Kernel/Environment/Versions.swift new file mode 100644 index 00000000000000..f62df2dcbbe11e --- /dev/null +++ b/apps/expo-go/ios/Exponent/Kernel/Environment/Versions.swift @@ -0,0 +1,49 @@ +// Copyright 2015-present 650 Industries. All rights reserved. + +import Foundation +import EXManifests + +@objc(EXVersions) +@objcMembers +public final class Versions: NSObject { + + public static let sharedInstance = Versions() + + public private(set) var sdkVersion: String + + private override init() { + self.sdkVersion = BuildConstants.sharedInstance.sdkVersion ?? "" + super.init() + } + + @objc(availableSdkVersionForManifest:) + public func availableSdkVersion(for manifest: Manifest?) -> String { + guard let manifest = manifest, + let manifestSdkVersion = manifest.expoGoSDKVersion() else { + return "" + } + + if manifestSdkVersion == sdkVersion { + return sdkVersion + } + + return "" + } + + public func supportsVersion(_ sdkVersion: String) -> Bool { + return self.sdkVersion == sdkVersion + } + + @objc public var majorVersion: String { + return Self.majorVersion(from: sdkVersion) + } + + public static func majorVersion(from sdkVersion: String) -> String { + return sdkVersion.components(separatedBy: ".").first ?? sdkVersion + } + + public func isCompatible(sdkVersion: String?) -> Bool { + guard let sdkVersion else { return false } + return Self.majorVersion(from: sdkVersion) == majorVersion + } +} diff --git a/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppExceptionHandler.m b/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppExceptionHandler.m index b39216da698b56..63d8759df0e1ad 100644 --- a/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppExceptionHandler.m +++ b/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppExceptionHandler.m @@ -1,8 +1,9 @@ // Copyright 2015-present 650 Industries. All rights reserved. #import "EXAppViewController.h" -#import "EXErrorRecoveryManager.h" #import "EXKernel.h" + +#import "Expo_Go-Swift.h" #import "EXKernelAppRecord.h" #import "EXReactAppManager.h" #import "EXReactAppExceptionHandler.h" diff --git a/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppManager+Private.h b/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppManager+Private.h index 3e0240c05b6ab1..6519f810b6d90d 100644 --- a/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppManager+Private.h +++ b/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppManager+Private.h @@ -14,8 +14,6 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong) EXReactAppExceptionHandler *exceptionHandler; -- (NSDictionary *)launchOptionsForHost; - @end NS_ASSUME_NONNULL_END diff --git a/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppManager.mm b/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppManager.mm index c1f63332232b84..057fe3e08ef6c9 100644 --- a/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppManager.mm +++ b/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppManager.mm @@ -1,6 +1,4 @@ -#import "EXBuildConstants.h" #import "EXEnvironment.h" -#import "EXErrorRecoveryManager.h" #import "EXKernel.h" #import "EXAbstractLoader.h" #import "EXKernelLinkingManager.h" @@ -10,7 +8,6 @@ #import "EXReactAppManager.h" #import "EXReactAppManager+Private.h" #import "EXVersionManagerObjC.h" -#import "EXVersions.h" #import "EXAppViewController.h" #import #import @@ -185,7 +182,6 @@ - (NSDictionary *)extraParams @"testEnvironment": @([EXEnvironment sharedEnvironment].testEnvironment), @"services": [EXKernel sharedInstance].serviceRegistry.allServices, @"singletonModules": [EXModuleRegistryProvider singletonModules], - @"moduleRegistryDelegateClass": RCTNullIfNil([self moduleRegistryDelegateClass]), @"fileSystemDirectories": @{ @"documentDirectory": [self scopedDocumentDirectory], @"cachesDirectory": [self scopedCachesDirectory] @@ -530,16 +526,6 @@ - (void)selectDevMenuItemWithKey:(NSString *)key #pragma mark - RN configuration -- (NSDictionary *)launchOptionsForHost -{ - return @{}; -} - -- (Class)moduleRegistryDelegateClass -{ - return nil; -} - - (NSString *)applicationKeyForRootView { EXManifestsManifest *manifest = _appRecord.appLoader.manifest; diff --git a/apps/expo-go/ios/Exponent/Kernel/Services/EXErrorRecoveryManager.h b/apps/expo-go/ios/Exponent/Kernel/Services/EXErrorRecoveryManager.h deleted file mode 100644 index b332f1db584a40..00000000000000 --- a/apps/expo-go/ios/Exponent/Kernel/Services/EXErrorRecoveryManager.h +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2015-present 650 Industries. All rights reserved. -// -// Keeps track of experience error state, including between reloads, so that we can -// pass error recovery info to an experience which just reloaded. -// - -@protocol EXErrorRecoveryScopedModuleDelegate - -- (void)setDeveloperInfo:(NSDictionary *)developerInfo forScopedModule:(id)scopedModule; - -@end - -@class EXKernelAppRecord; - -@interface EXErrorRecoveryManager : NSObject - -/** - * Associate arbitrary developer info with this experience id. If the experience recovers from an - * error, we can pass this info to the new instance of the experience. - */ -- (void)setDeveloperInfo:(NSDictionary *)developerInfo forScopeKey:(NSString *)scopeKey; -- (NSDictionary *)developerInfoForScopeKey: (NSString *)scopeKey; - -/** - * Associate an error with an experience id. This will never be cleared until the next - * call to `experienceFinishedLoadingWithId:`. - */ -- (void)setError: (NSError *)error forScopeKey:(NSString *)scopeKey; - -/** - * Indicate that a JS bundle has successfully loaded for this experience. - */ -- (void)experienceFinishedLoadingWithScopeKey:(NSString *)scopeKey; - -/** - * True if any bridge for this experience had an error, and has not successfully loaded - * since the error was reported. - */ -- (BOOL)scopeKeyIsRecoveringFromError:(NSString *)scopeKey; - -/** - * True if this error object (by `isEqual:`) has been registered for any experience. - */ -- (BOOL)errorBelongsToExperience:(NSError *)error; - -/** - * Returns any existing app record for this error. Since error state persists between reloads until cleared, - * it's possible that there is no app record for this error. - */ -- (EXKernelAppRecord *)appRecordForError: (NSError *)error; - -/** - * Whether we want to auto-reload this experience if it encounters a fatal error. - */ -- (BOOL)experienceShouldReloadOnError:(NSString *)scopeKey; - -/** - * Back off to a less aggressive autoreload buffer time. - * The longer the time, the longer a experience must wait before a fatal JS error triggers auto reload - * via `experienceShouldReloadOnError:`. - */ -- (void)increaseAutoReloadBuffer; - -@end diff --git a/apps/expo-go/ios/Exponent/Kernel/Services/EXErrorRecoveryManager.m b/apps/expo-go/ios/Exponent/Kernel/Services/EXErrorRecoveryManager.m deleted file mode 100644 index 7f62f0f05ce2a4..00000000000000 --- a/apps/expo-go/ios/Exponent/Kernel/Services/EXErrorRecoveryManager.m +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright 2015-present 650 Industries. All rights reserved. - -#import "EXBuildConstants.h" -#import "EXErrorRecoveryManager.h" -#import "EXKernel.h" -#import "EXScopedBridgeModule.h" - -#import - -// if the app crashes and it has not yet been 5 seconds since it loaded, don't auto refresh. -#define EX_AUTO_REFRESH_BUFFER_BASE_SECONDS 5.0 - -@interface EXErrorRecoveryRecord : NSObject - -@property (nonatomic, assign) BOOL isRecovering; -@property (nonatomic, strong) NSError *error; -@property (nonatomic, strong) NSDate *dtmLastLoaded; -@property (nonatomic, strong) NSDictionary *developerInfo; - -@end - -@implementation EXErrorRecoveryRecord - -@end - -@interface EXErrorRecoveryManager () - -@property (nonatomic, strong) NSMutableDictionary *experienceInfo; -@property (nonatomic, assign) NSUInteger reloadBufferDepth; -@property (nonatomic, strong) NSDate *dtmAnyExperienceLoaded; - -@end - -@implementation EXErrorRecoveryManager - -- (instancetype)init -{ - if (self = [super init]) { - _reloadBufferDepth = 0; - _dtmAnyExperienceLoaded = [NSDate date]; - _experienceInfo = [NSMutableDictionary dictionary]; - } - return self; -} - -- (void)setDeveloperInfo:(NSDictionary *)developerInfo forScopeKey:(NSString *)scopeKey -{ - if (!scopeKey) { - NSAssert(scopeKey, @"Cannot associate recovery info with a nil scope key"); - } - EXErrorRecoveryRecord *record = [self _recordForScopeKey:scopeKey]; - if (!record) { - record = [[EXErrorRecoveryRecord alloc] init]; - @synchronized (_experienceInfo) { - _experienceInfo[scopeKey] = record; - } - } - record.developerInfo = developerInfo; -} - -- (NSDictionary *)developerInfoForScopeKey:(NSString *)scopeKey -{ - EXErrorRecoveryRecord *record = [self _recordForScopeKey:scopeKey]; - if (record) { - return record.developerInfo; - } - return nil; -} - -- (void)setDeveloperInfo:(NSDictionary *)developerInfo forScopedModule:(id)scopedModule -{ - [self setDeveloperInfo:developerInfo forScopeKey:((EXScopedBridgeModule *)scopedModule).scopeKey]; -} - -- (void)setError:(NSError *)error forScopeKey:(NSString *)scopeKey -{ - if (!scopeKey) { - NSString *kernelSuggestion = ([EXBuildConstants sharedInstance].isDevKernel) ? @"Make sure EXBuildConstants is configured to load a valid development Kernel JS bundle." : @""; - NSAssert(scopeKey, @"Cannot associate an error with a nil experience id. %@", kernelSuggestion); - } - EXErrorRecoveryRecord *record = [self _recordForScopeKey:scopeKey]; - if (error) { - if (!record) { - record = [[EXErrorRecoveryRecord alloc] init]; - @synchronized (_experienceInfo) { - _experienceInfo[scopeKey] = record; - } - } - // mark this experience id as having loading problems, so future attempts will bust the cache. - // this flag never gets unset until the app loads successfully, even if the error is nullified. - record.isRecovering = YES; - } - if (record) { - // if this record already shows an error, - // and the new error is about AppRegistry, - // don't override the previous error message. - if (record.error && - [error.localizedDescription rangeOfString:@"AppRegistry is not a registered callable module"].length != 0) { - DDLogWarn(@"Ignoring misleading error: %@", error); - } else { - record.error = error; - } - } -} - -- (BOOL)errorBelongsToExperience:(NSError *)error -{ - if (!error) { - return NO; - } - NSArray *scopeKeys; - @synchronized (_experienceInfo) { - scopeKeys = _experienceInfo.allKeys; - } - for (NSString *scopeKey in scopeKeys) { - EXErrorRecoveryRecord *record = [self _recordForScopeKey:scopeKey]; - if ([self isJSError:record.error equalToOtherJSError:error]) { - return YES; - } - } - return NO; -} - -- (EXKernelAppRecord *)appRecordForError:(NSError *)error -{ - if (!error) { - return nil; - } - NSArray *scopeKeys; - @synchronized (_experienceInfo) { - scopeKeys = _experienceInfo.allKeys; - } - for (NSString *scopeKey in scopeKeys) { - EXErrorRecoveryRecord *record = [self _recordForScopeKey:scopeKey]; - if ([self isJSError:record.error equalToOtherJSError:error]) { - return [[EXKernel sharedInstance].appRegistry newestRecordWithScopeKey:scopeKey]; - } - } - return nil; -} - -- (void)experienceFinishedLoadingWithScopeKey:(NSString *)scopeKey -{ - if (!scopeKey) { - NSAssert(scopeKey, @"Cannot mark an experience with nil id as loaded"); - } - EXErrorRecoveryRecord *record = [self _recordForScopeKey:scopeKey]; - if (!record) { - record = [[EXErrorRecoveryRecord alloc] init]; - @synchronized (_experienceInfo) { - _experienceInfo[scopeKey] = record; - } - } - record.dtmLastLoaded = [NSDate date]; - record.isRecovering = NO; - - // maintain a global record of when anything last loaded, used to calculate autoreload backoff. - _dtmAnyExperienceLoaded = [NSDate date]; -} - -- (BOOL)scopeKeyIsRecoveringFromError:(NSString *)scopeKey -{ - EXErrorRecoveryRecord *record = [self _recordForScopeKey:scopeKey]; - if (record) { - return record.isRecovering; - } - return NO; -} - -- (BOOL)experienceShouldReloadOnError:(NSString *)scopeKey -{ - EXErrorRecoveryRecord *record = [self _recordForScopeKey:scopeKey]; - if (record) { - return ([record.dtmLastLoaded timeIntervalSinceNow] < -[self reloadBufferSeconds]); - } - // if we have no knowledge of this experience, this is probably a manifest loading error - // so we should assume we'd just hit the same issue again next time. don't try to autoreload. - return NO; -} - -- (void)increaseAutoReloadBuffer -{ - _reloadBufferDepth++; -} - -#pragma mark - internal - -- (BOOL)isJSError:(NSError *)error1 equalToOtherJSError: (NSError *)error2 -{ - // use rangeOfString: to catch versioned RCTErrorDomain - if ([error1.domain rangeOfString:RCTErrorDomain].length > 0 && [error2.domain rangeOfString:RCTErrorDomain].length > 0) { - NSDictionary *userInfo1 = error1.userInfo; - NSDictionary *userInfo2 = error2.userInfo; - // could also possibly compare ([userInfo1[RCTJSStackTraceKey] isEqual:userInfo2[RCTJSStackTraceKey]]) if this isn't enough - return ([userInfo1[NSLocalizedDescriptionKey] isEqualToString:userInfo2[NSLocalizedDescriptionKey]]); - } - return [error1 isEqual:error2]; -} - -- (EXErrorRecoveryRecord *)_recordForScopeKey:(NSString *)scopeKey; -{ - EXErrorRecoveryRecord *result = nil; - if (scopeKey) { - @synchronized (_experienceInfo) { - result = _experienceInfo[scopeKey]; - } - } - return result; -} - -- (NSTimeInterval)reloadBufferSeconds -{ - NSTimeInterval interval = MIN(60.0 * 5.0, EX_AUTO_REFRESH_BUFFER_BASE_SECONDS * pow(1.5, _reloadBufferDepth)); - - // if nothing has loaded for twice our current backoff interval, reset backoff - if ([_dtmAnyExperienceLoaded timeIntervalSinceNow] < -(interval * 2.0)) { - _reloadBufferDepth = 0; - interval = EX_AUTO_REFRESH_BUFFER_BASE_SECONDS; - } - return interval; -} - -@end diff --git a/apps/expo-go/ios/Exponent/Kernel/Services/EXKernelLinkingManager.h b/apps/expo-go/ios/Exponent/Kernel/Services/EXKernelLinkingManager.h index d50445507540ae..a8f92c34a1a65f 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Services/EXKernelLinkingManager.h +++ b/apps/expo-go/ios/Exponent/Kernel/Services/EXKernelLinkingManager.h @@ -33,11 +33,6 @@ + (NSString *)stringByRemovingDeepLink:(NSString *)path; -/** - * Determine if an url is hosted by expo - */ -+ (BOOL)isExpoHostedUrl: (NSURL *)url; - /** * Grab the release channel from the query parameters of a uri */ diff --git a/apps/expo-go/ios/Exponent/Kernel/Services/EXKernelLinkingManager.m b/apps/expo-go/ios/Exponent/Kernel/Services/EXKernelLinkingManager.m index c9a09bf80d8706..aec4ec0ebdcefd 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Services/EXKernelLinkingManager.m +++ b/apps/expo-go/ios/Exponent/Kernel/Services/EXKernelLinkingManager.m @@ -1,7 +1,6 @@ // Copyright 2015-present 650 Industries. All rights reserved. #import "EXAbstractLoader.h" -#import "EXEnvironment.h" #import "EXKernel.h" #import "EXKernelLinkingManager.h" #import "EXUtil.h" @@ -9,7 +8,6 @@ #import "EXReactAppManager.h" #import -#import #import #import diff --git a/apps/expo-go/ios/Exponent/Kernel/Services/EXPermissionsManager.m b/apps/expo-go/ios/Exponent/Kernel/Services/EXPermissionsManager.m index 676c5f0225780f..5184924cdafae4 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Services/EXPermissionsManager.m +++ b/apps/expo-go/ios/Exponent/Kernel/Services/EXPermissionsManager.m @@ -3,7 +3,6 @@ #import #import "EXPermissionsManager.h" -#import "EXEnvironment.h" #import "EXUtil.h" NSString * const EXPermissionsKey = @"ExpoPermissions"; diff --git a/apps/expo-go/ios/Exponent/Kernel/Services/EXSensorManager.m b/apps/expo-go/ios/Exponent/Kernel/Services/EXSensorManager.m index db68b38248317f..308bb31d76e08b 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Services/EXSensorManager.m +++ b/apps/expo-go/ios/Exponent/Kernel/Services/EXSensorManager.m @@ -1,8 +1,9 @@ // Copyright 2015-present 650 Industries. All rights reserved. -#import "EXKernel.h" #import "EXSensorManager.h" + #import +#import @interface EXSensorManager () diff --git a/apps/expo-go/ios/Exponent/Kernel/Services/ErrorRecoveryManager.swift b/apps/expo-go/ios/Exponent/Kernel/Services/ErrorRecoveryManager.swift new file mode 100644 index 00000000000000..288657e1ff59b4 --- /dev/null +++ b/apps/expo-go/ios/Exponent/Kernel/Services/ErrorRecoveryManager.swift @@ -0,0 +1,173 @@ +// Copyright 2015-present 650 Industries. All rights reserved. + +import Foundation +import React + +@objc(EXErrorRecoveryScopedModuleDelegate) +public protocol ErrorRecoveryScopedModuleDelegate { + func setDeveloperInfo(_ developerInfo: NSDictionary, forScopedModule scopedModule: Any) +} + +private class ErrorRecoveryRecord { + var isRecovering: Bool = false + var error: NSError? + var dtmLastLoaded: Date? + var developerInfo: NSDictionary? +} + +@objc(EXErrorRecoveryManager) +@objcMembers +public final class ErrorRecoveryManager: NSObject, ErrorRecoveryScopedModuleDelegate { + private static let autoRefreshBufferBaseSeconds: TimeInterval = 5.0 + + private var experienceInfo: [String: ErrorRecoveryRecord] = [:] + private let lock = NSLock() + private var reloadBufferDepth: UInt = 0 + private var dtmAnyExperienceLoaded: Date = Date() + + public override init() { + super.init() + } + + public func setDeveloperInfo(_ developerInfo: NSDictionary?, forScopeKey scopeKey: String) { + assert(scopeKey.count > 0, "Cannot associate recovery info with an empty scope key") + + lock.lock() + defer { lock.unlock() } + + let record = getOrCreateRecord(forScopeKey: scopeKey) + record.developerInfo = developerInfo + } + + public func developerInfo(forScopeKey scopeKey: String) -> NSDictionary? { + lock.lock() + defer { lock.unlock() } + + return experienceInfo[scopeKey]?.developerInfo + } + + public func setDeveloperInfo(_ developerInfo: NSDictionary, forScopedModule scopedModule: Any) { + if let scopeKey = (scopedModule as AnyObject).value(forKey: "scopeKey") as? String { + setDeveloperInfo(developerInfo, forScopeKey: scopeKey) + } + } + + public func setError(_ error: NSError?, forScopeKey scopeKey: String) { + assert(scopeKey.count > 0, "Cannot associate an error with an empty scope key") + + lock.lock() + defer { lock.unlock() } + + if let error { + let record = getOrCreateRecord(forScopeKey: scopeKey) + record.isRecovering = true + + if record.error != nil, + error.localizedDescription.contains("AppRegistry is not a registered callable module") { + NSLog("[EXErrorRecoveryManager] Ignoring misleading error: %@", error) + } else { + record.error = error + } + } else if let record = experienceInfo[scopeKey] { + record.error = nil + } + } + + public func experienceFinishedLoading(withScopeKey scopeKey: String) { + assert(scopeKey.count > 0, "Cannot mark an experience with an empty scope key as loaded") + + lock.lock() + defer { lock.unlock() } + + let record = getOrCreateRecord(forScopeKey: scopeKey) + record.dtmLastLoaded = Date() + record.isRecovering = false + + dtmAnyExperienceLoaded = Date() + } + + public func scopeKeyIsRecoveringFromError(_ scopeKey: String) -> Bool { + lock.lock() + defer { lock.unlock() } + + return experienceInfo[scopeKey]?.isRecovering ?? false + } + + public func errorBelongsToExperience(_ error: NSError?) -> Bool { + guard let error else { return false } + + lock.lock() + let errors: [NSError] = experienceInfo.values.compactMap { $0.error } + lock.unlock() + + return errors.contains { isJSError($0, equalTo: error) } + } + + @objc(appRecordForError:) + public func appRecord(for error: NSError?) -> EXKernelAppRecord? { + guard let error else { return nil } + + lock.lock() + let scopeKeyAndErrors: [String: NSError] = experienceInfo.compactMapValues { $0.error } + lock.unlock() + + for (scopeKey, recordError) in scopeKeyAndErrors { + if isJSError(recordError, equalTo: error) { + return EXKernel.sharedInstance().appRegistry.newestRecord(withScopeKey: scopeKey) + } + } + return nil + } + + public func experienceShouldReload(onError scopeKey: String) -> Bool { + lock.lock() + defer { lock.unlock() } + + guard let record = experienceInfo[scopeKey], + let dtmLastLoaded = record.dtmLastLoaded else { + return false + } + + return dtmLastLoaded.timeIntervalSinceNow < -reloadBufferSeconds + } + + public func increaseAutoReloadBuffer() { + lock.lock() + defer { lock.unlock() } + reloadBufferDepth += 1 + } + + private func getOrCreateRecord(forScopeKey scopeKey: String) -> ErrorRecoveryRecord { + if let existing = experienceInfo[scopeKey] { + return existing + } + let record = ErrorRecoveryRecord() + experienceInfo[scopeKey] = record + return record + } + + private func isJSError(_ error1: NSError?, equalTo error2: NSError?) -> Bool { + guard let error1, let error2 else { + return error1 == nil && error2 == nil + } + + if error1.domain.contains(RCTErrorDomain) && error2.domain.contains(RCTErrorDomain) { + let desc1 = error1.userInfo[NSLocalizedDescriptionKey] as? String + let desc2 = error2.userInfo[NSLocalizedDescriptionKey] as? String + return desc1 == desc2 + } + + return error1.isEqual(error2) + } + + private var reloadBufferSeconds: TimeInterval { + var interval = min(60.0 * 5.0, Self.autoRefreshBufferBaseSeconds * pow(1.5, Double(reloadBufferDepth))) + + if dtmAnyExperienceLoaded.timeIntervalSinceNow < -(interval * 2.0) { + reloadBufferDepth = 0 + interval = Self.autoRefreshBufferBaseSeconds + } + + return interval + } +} diff --git a/apps/expo-go/ios/Exponent/Kernel/Views/EXAppViewController.mm b/apps/expo-go/ios/Exponent/Kernel/Views/EXAppViewController.mm index 1e799d6298ec1e..05b0f3312e23cd 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Views/EXAppViewController.mm +++ b/apps/expo-go/ios/Exponent/Kernel/Views/EXAppViewController.mm @@ -8,12 +8,10 @@ #import "EXAppLoadingCancelView.h" #import "Expo_Go-Swift.h" #import "EXEnvironment.h" -#import "EXErrorRecoveryManager.h" #import "EXErrorView.h" #import "EXFileDownloader.h" #import "EXKernel.h" #import "EXReactAppManager.h" -#import "EXVersions.h" #import "EXUpdatesManager.h" #import "EXUtil.h" @@ -69,8 +67,7 @@ @interface EXAppViewController () /** * SplashScreenViewProvider that is used only in managed workflow app. - * Managed app does not need any specific SplashScreenViewProvider as it uses generic one povided by the SplashScreen module. - * See also EXHomeAppSplashScreenViewProvider in self.viewDidLoad + * Managed app does not need any specific SplashScreenViewProvider as it uses generic one provided by the SplashScreen module. */ @property (nonatomic, strong, nullable) EXManagedAppSplashScreenViewProvider *managedAppSplashScreenViewProvider; @property (nonatomic, strong, nullable) EXManagedAppSplashScreenViewController *managedSplashScreenController; diff --git a/apps/expo-go/ios/Exponent/Kernel/Views/EXErrorView.m b/apps/expo-go/ios/Exponent/Kernel/Views/EXErrorView.m index bf6d0ca79a9ed8..2e73461ee58bef 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Views/EXErrorView.m +++ b/apps/expo-go/ios/Exponent/Kernel/Views/EXErrorView.m @@ -2,11 +2,9 @@ #import "EXAbstractLoader.h" #import "EXErrorView.h" -#import "EXEnvironment.h" #import "EXKernel.h" #import "EXKernelAppRecord.h" #import "EXManifestResource.h" -#import "EXUtil.h" #import "Expo_Go-Swift.h" @import EXManifests; diff --git a/apps/expo-go/ios/Exponent/Kernel/Views/Loading/HomeAppSplashScreenViewProvider.swift b/apps/expo-go/ios/Exponent/Kernel/Views/Loading/HomeAppSplashScreenViewProvider.swift deleted file mode 100644 index 708457cadff601..00000000000000 --- a/apps/expo-go/ios/Exponent/Kernel/Views/Loading/HomeAppSplashScreenViewProvider.swift +++ /dev/null @@ -1,4 +0,0 @@ -import UIKit - -@objc(EXHomeAppSplashScreenViewProvider) -class HomeAppSplashScreenViewProvider: SplashScreenViewNativeProvider {} diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/EXDevSettings.m b/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/EXDevSettings.m index db037ce8e975aa..be9910b63d7ae4 100644 --- a/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/EXDevSettings.m +++ b/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/EXDevSettings.m @@ -27,9 +27,4 @@ - (instancetype)initWithScopeKey:(NSString *)scopeKey isDevelopment:(BOOL)isDeve return [super initWithDataSource:dataSource]; } -- (NSArray *)supportedEvents -{ - return [super supportedEvents]; -} - @end diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/EXScopedNotificationSerializer.swift b/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/EXScopedNotificationSerializer.swift index 711ce86ab5a429..cf1e5a61e58f5a 100644 --- a/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/EXScopedNotificationSerializer.swift +++ b/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/EXScopedNotificationSerializer.swift @@ -16,7 +16,7 @@ public class EXScopedNotificationSerializer { let scopedSerializedContent = NotificationContentRecord(from: request) let categoryIdentifier = request.content.categoryIdentifier - if (!categoryIdentifier.isEmpty) { + if !categoryIdentifier.isEmpty { let unscopedCategoryId = EXScopedNotificationsUtils.getScopeAndIdentifierFromScopedIdentifier(categoryIdentifier).identifier scopedSerializedContent.categoryIdentifier = unscopedCategoryId } else { diff --git a/apps/expo-go/ios/Podfile.lock b/apps/expo-go/ios/Podfile.lock index 60980fbdd39fc8..b94849c7ad168a 100644 --- a/apps/expo-go/ios/Podfile.lock +++ b/apps/expo-go/ios/Podfile.lock @@ -273,6 +273,7 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - RNWorklets - SocketRocket - Yoga - ExpoModulesCore/Tests (3.0.16): @@ -304,6 +305,7 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - RNWorklets - SocketRocket - Yoga - ExpoModulesJSI (3.0.16): @@ -3703,7 +3705,7 @@ PODS: - RNWorklets - SocketRocket - Yoga - - RNScreens (4.19.0): + - RNScreens (4.20.0): - boost - DoubleConversion - fast_float @@ -3730,10 +3732,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNScreens/common (= 4.19.0) + - RNScreens/common (= 4.20.0) - SocketRocket - Yoga - - RNScreens/common (4.19.0): + - RNScreens/common (4.20.0): - boost - DoubleConversion - fast_float @@ -4686,7 +4688,7 @@ SPEC CHECKSUMS: ExpoMailComposer: 8771121c8d5e6e0e194537fab79360d0f443f70d ExpoMediaLibrary: 648cee3f5dcba13410ec9cc8ac9a426e89a61a31 ExpoMeshGradient: 763087d3b1e6e9a0974e9700ea24cb598816d93c - ExpoModulesCore: d843eb08a3bc89716cd4eb517f89e17afa451ffc + ExpoModulesCore: e7e2ae861d31285e26276f645f29dd32dd7571df ExpoModulesJSI: c470ea2ed825fce73bdc4ef060c8a22e3f664092 ExpoModulesTestCore: e65555b75a4ed7dd3bcf421ad01d7748bd372c88 ExpoNetwork: 97073786edfe405aba5d0987a544617ed0671ad1 @@ -4728,7 +4730,7 @@ SPEC CHECKSUMS: GoogleAppMeasurement: 8a82b93a6400c8e6551c0bcd66a9177f2e067aed GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d - hermes-engine: 452f2dd7422b2fd7973ae9ca103898d28d7744f0 + hermes-engine: 21f7021a3364f5f9dab02bdfd1fa0e21053e5dd5 libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7 libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 @@ -4826,7 +4828,7 @@ SPEC CHECKSUMS: RNDateTimePicker: e9e210197c267461f70f3f47bec705401ff72077 RNGestureHandler: 77eecab5fd636666ca73a55bb61e2f1a685b7e84 RNReanimated: 31da8d5f1605f5367e2392748ba9f4ba6eaf1178 - RNScreens: 69f68c95d395bc4261d27c3aae7b4a458b947b7e + RNScreens: b2a5c76af24a02a2fd71bfce42780fdd9c79cc6d RNSVG: 55fc5dc0eaa36a875ffb7d05c0f2cd5b2cbfc342 RNWorklets: 4e3230b74c2e466e608458b7f665a41825bca6c1 SDWebImage: f29024626962457f3470184232766516dee8dfea diff --git a/apps/expo-go/package.json b/apps/expo-go/package.json index f17c703fd2f8a0..2417072fba0165 100644 --- a/apps/expo-go/package.json +++ b/apps/expo-go/package.json @@ -77,7 +77,7 @@ "react-native-paper": "^5.12.5", "react-native-reanimated": "4.2.1", "react-native-safe-area-context": "5.6.2", - "react-native-screens": "4.19.0", + "react-native-screens": "4.20.0", "react-native-svg": "15.15.1", "react-native-view-shot": "4.0.3", "react-native-webview": "13.16.0", diff --git a/apps/minimal-tester/ios/minimaltester.xcodeproj/project.pbxproj b/apps/minimal-tester/ios/minimaltester.xcodeproj/project.pbxproj index 8d810f5ecc56be..70ab4307d405c9 100644 --- a/apps/minimal-tester/ios/minimaltester.xcodeproj/project.pbxproj +++ b/apps/minimal-tester/ios/minimaltester.xcodeproj/project.pbxproj @@ -7,57 +7,54 @@ objects = { /* Begin PBXBuildFile section */ - 0347E322C2B44B7F9AB46B9F /* ReactNativeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F797C012521340598E9C8800 /* ReactNativeView.swift */; }; - 06622B612090727540ED45CE /* Pods_minimaltester_minimaltesterbrownfield.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56DFB06B472889A36A5C4CAA /* Pods_minimaltester_minimaltesterbrownfield.framework */; }; - 124678CAE0EB3403D169D490 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D0062F01A46BE0B02344D4A7 /* PrivacyInfo.xcprivacy */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; - 2A59239B2BA66E0A970797C8 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCB7EC3E85AC3A9EFB821A00 /* ExpoModulesProvider.swift */; }; - 32B40BCD62DE40CF9A6BB39F /* ExpoAppDelegateWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AA46F724F924946A6C6CDF6 /* ExpoAppDelegateWrapper.swift */; }; - 346572B871564A6F957DA64C /* Messaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27F407CD82541FBBBCEEF87 /* Messaging.swift */; }; + 2BFB6924A0DE867335609929 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD6989B0D56002172F61C58 /* ExpoModulesProvider.swift */; }; 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; - 78BF7C54711ED17195852024 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C455BAA49D4A5E9BE0C154A5 /* ExpoModulesProvider.swift */; }; - 876FA08C072999C700D4484A /* Pods_minimaltester.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93A03FEABBDADEBD8C94AEED /* Pods_minimaltester.framework */; }; - 89C57776E93C460EAC77E8FF /* ReactNativeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27F7D69B9F44BD9A00AD155 /* ReactNativeViewController.swift */; }; - 8DF7AC7E10CD40609BA34F40 /* BrownfieldAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DDE692640834565B39002FF /* BrownfieldAppDelegate.swift */; }; - 9D457F228E2F42A280DB8B9F /* ReactNativeHostManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A4296295A5425B8BC77C4A /* ReactNativeHostManager.swift */; }; - 9D90DFF4E1CF4081B765B7E7 /* ReactNativeDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88EB009A9C9341388DE4EEC5 /* ReactNativeDelegate.swift */; }; + 43C7C811D508488599D681D7 /* ReactNativeDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C01F3C3FD7B4A40BF37403F /* ReactNativeDelegate.swift */; }; + 54DC818C26344A338FBD223F /* ReactNativeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4F3AE8BD9A4F8381D889FC /* ReactNativeView.swift */; }; + 6F9BD8E3EAB1402EA5158FFD /* ReactNativeHostManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB19C1B83684E0F93B77F74 /* ReactNativeHostManager.swift */; }; + 7559F23945462FBFE55EB02B /* Pods_minimaltester_minimaltesterbrownfield.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D4BCEA000CC8EF23719451B0 /* Pods_minimaltester_minimaltesterbrownfield.framework */; }; + A01F9B1FDF6D48F0822E6832 /* ReactNativeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1F738D79D2496DA090CA02 /* ReactNativeViewController.swift */; }; BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; + C9AE6CE96C58368B6AC63DF6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = B6B84DE78AFE8D9B0BF3A1AE /* PrivacyInfo.xcprivacy */; }; + CA92A3296199421E9509A2A9 /* ExpoAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F5B048DC50C43E3B2BEA897 /* ExpoAppDelegate.swift */; }; + E2D36015D7B944A1D6108F5E /* Pods_minimaltester.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7948F5DE45F4CCA13BC7CCE1 /* Pods_minimaltester.framework */; }; + E4BC6394748FE19E01A0354F /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A632793DB62AC94D8DD148CE /* ExpoModulesProvider.swift */; }; + EED24139D38A4326B603DA6F /* Messaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79FB3089D46146B2BAF9B3FD /* Messaging.swift */; }; F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 0723AF8BC7C24A8488C8A728 /* ReactNativeHostManager.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ReactNativeHostManager.swift; path = ReactNativeHostManager.swift; sourceTree = ""; }; + 034ED5E80B444A2B8FB1DFA3 /* ReactNativeHostManager.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ReactNativeHostManager.swift; path = ReactNativeHostManager.swift; sourceTree = ""; }; + 0ECB103E2B1E41ABB632C9C9 /* minimaltesterbrownfield.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = undefined; name = minimaltesterbrownfield.framework; path = minimaltesterbrownfield.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07F961A680F5B00A75B9A /* minimaltester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = minimaltester.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = minimaltester/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = minimaltester/Info.plist; sourceTree = ""; }; - 2675D3AF84C44D4CB5579B55 /* BrownfieldAppDelegate.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = BrownfieldAppDelegate.swift; path = BrownfieldAppDelegate.swift; sourceTree = ""; }; - 52ED535AE0BE4E1BB81BEB92 /* ReactNativeView.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ReactNativeView.swift; path = ReactNativeView.swift; sourceTree = ""; }; - 56DFB06B472889A36A5C4CAA /* Pods_minimaltester_minimaltesterbrownfield.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_minimaltester_minimaltesterbrownfield.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 63467FB7339F4BC2A590D5F3 /* minimaltesterbrownfield.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = undefined; name = minimaltesterbrownfield.framework; path = minimaltesterbrownfield.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 6951067E91FAB604A2570427 /* Pods-minimaltester-minimaltesterbrownfield.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minimaltester-minimaltesterbrownfield.debug.xcconfig"; path = "Target Support Files/Pods-minimaltester-minimaltesterbrownfield/Pods-minimaltester-minimaltesterbrownfield.debug.xcconfig"; sourceTree = ""; }; - 77064DBAC758F03A588FD466 /* Pods-minimaltester.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minimaltester.debug.xcconfig"; path = "Target Support Files/Pods-minimaltester/Pods-minimaltester.debug.xcconfig"; sourceTree = ""; }; - 88EB009A9C9341388DE4EEC5 /* ReactNativeDelegate.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ReactNativeDelegate.swift; path = minimaltesterbrownfield/ReactNativeDelegate.swift; sourceTree = ""; }; - 8AA46F724F924946A6C6CDF6 /* ExpoAppDelegateWrapper.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ExpoAppDelegateWrapper.swift; path = minimaltesterbrownfield/ExpoAppDelegateWrapper.swift; sourceTree = ""; }; - 93A03FEABBDADEBD8C94AEED /* Pods_minimaltester.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_minimaltester.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 9AD2C18B1B3E41F385A1269E /* Messaging.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = Messaging.swift; path = Messaging.swift; sourceTree = ""; }; - 9DDE692640834565B39002FF /* BrownfieldAppDelegate.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = BrownfieldAppDelegate.swift; path = minimaltesterbrownfield/BrownfieldAppDelegate.swift; sourceTree = ""; }; - A060B4793D1349D4B6D474B8 /* ExpoAppDelegateWrapper.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ExpoAppDelegateWrapper.swift; path = ExpoAppDelegateWrapper.swift; sourceTree = ""; }; - A777BE16EC069C69A1AE9CBB /* Pods-minimaltester-minimaltesterbrownfield.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minimaltester-minimaltesterbrownfield.release.xcconfig"; path = "Target Support Files/Pods-minimaltester-minimaltesterbrownfield/Pods-minimaltester-minimaltesterbrownfield.release.xcconfig"; sourceTree = ""; }; + 1BD6989B0D56002172F61C58 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-minimaltester/ExpoModulesProvider.swift"; sourceTree = ""; }; + 2115662F5C3EDF6CC7E5F44C /* Pods-minimaltester-minimaltesterbrownfield.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minimaltester-minimaltesterbrownfield.debug.xcconfig"; path = "Target Support Files/Pods-minimaltester-minimaltesterbrownfield/Pods-minimaltester-minimaltesterbrownfield.debug.xcconfig"; sourceTree = ""; }; + 4D9E3508515F41689B15D18B /* ReactNativeDelegate.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ReactNativeDelegate.swift; path = ReactNativeDelegate.swift; sourceTree = ""; }; + 5C3F4764CFF74FFA9E6218E0 /* ReactNativeView.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ReactNativeView.swift; path = ReactNativeView.swift; sourceTree = ""; }; + 632501C1139B40D7ACA9F6D6 /* Messaging.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = Messaging.swift; path = Messaging.swift; sourceTree = ""; }; + 77B96E96905541BB9C1F4A2D /* ExpoAppDelegate.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ExpoAppDelegate.swift; path = ExpoAppDelegate.swift; sourceTree = ""; }; + 7948F5DE45F4CCA13BC7CCE1 /* Pods_minimaltester.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_minimaltester.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 79FB3089D46146B2BAF9B3FD /* Messaging.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = Messaging.swift; path = minimaltesterbrownfield/Messaging.swift; sourceTree = ""; }; + 7B4F3AE8BD9A4F8381D889FC /* ReactNativeView.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ReactNativeView.swift; path = minimaltesterbrownfield/ReactNativeView.swift; sourceTree = ""; }; + 8C01F3C3FD7B4A40BF37403F /* ReactNativeDelegate.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ReactNativeDelegate.swift; path = minimaltesterbrownfield/ReactNativeDelegate.swift; sourceTree = ""; }; + 8F5B048DC50C43E3B2BEA897 /* ExpoAppDelegate.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ExpoAppDelegate.swift; path = minimaltesterbrownfield/ExpoAppDelegate.swift; sourceTree = ""; }; + A632793DB62AC94D8DD148CE /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-minimaltester-minimaltesterbrownfield/ExpoModulesProvider.swift"; sourceTree = ""; }; AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = minimaltester/SplashScreen.storyboard; sourceTree = ""; }; - AD3E045EB2784179B28082B4 /* ReactNativeViewController.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ReactNativeViewController.swift; path = ReactNativeViewController.swift; sourceTree = ""; }; - B1A4296295A5425B8BC77C4A /* ReactNativeHostManager.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ReactNativeHostManager.swift; path = minimaltesterbrownfield/ReactNativeHostManager.swift; sourceTree = ""; }; - B793CDED927E439788FF7995 /* ReactNativeDelegate.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ReactNativeDelegate.swift; path = ReactNativeDelegate.swift; sourceTree = ""; }; + B0CE53A9CE031D7D00E2F0FC /* Pods-minimaltester.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minimaltester.release.xcconfig"; path = "Target Support Files/Pods-minimaltester/Pods-minimaltester.release.xcconfig"; sourceTree = ""; }; + B6B84DE78AFE8D9B0BF3A1AE /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = minimaltester/PrivacyInfo.xcprivacy; sourceTree = ""; }; BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; - C27F407CD82541FBBBCEEF87 /* Messaging.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = Messaging.swift; path = minimaltesterbrownfield/Messaging.swift; sourceTree = ""; }; - C455BAA49D4A5E9BE0C154A5 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-minimaltester-minimaltesterbrownfield/ExpoModulesProvider.swift"; sourceTree = ""; }; - D0062F01A46BE0B02344D4A7 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = minimaltester/PrivacyInfo.xcprivacy; sourceTree = ""; }; - D624B426007DE7F11433CC43 /* Pods-minimaltester.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minimaltester.release.xcconfig"; path = "Target Support Files/Pods-minimaltester/Pods-minimaltester.release.xcconfig"; sourceTree = ""; }; - E27F7D69B9F44BD9A00AD155 /* ReactNativeViewController.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ReactNativeViewController.swift; path = minimaltesterbrownfield/ReactNativeViewController.swift; sourceTree = ""; }; + BFB19C1B83684E0F93B77F74 /* ReactNativeHostManager.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ReactNativeHostManager.swift; path = minimaltesterbrownfield/ReactNativeHostManager.swift; sourceTree = ""; }; + D4BCEA000CC8EF23719451B0 /* Pods_minimaltester_minimaltesterbrownfield.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_minimaltester_minimaltesterbrownfield.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D758FCF0F9A6905996BB74EA /* Pods-minimaltester.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minimaltester.debug.xcconfig"; path = "Target Support Files/Pods-minimaltester/Pods-minimaltester.debug.xcconfig"; sourceTree = ""; }; + DD1F738D79D2496DA090CA02 /* ReactNativeViewController.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ReactNativeViewController.swift; path = minimaltesterbrownfield/ReactNativeViewController.swift; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = minimaltester/AppDelegate.swift; sourceTree = ""; }; F11748442D0722820044C1D9 /* minimaltester-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "minimaltester-Bridging-Header.h"; path = "minimaltester/minimaltester-Bridging-Header.h"; sourceTree = ""; }; - F797C012521340598E9C8800 /* ReactNativeView.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ReactNativeView.swift; path = minimaltesterbrownfield/ReactNativeView.swift; sourceTree = ""; }; - FCB7EC3E85AC3A9EFB821A00 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-minimaltester/ExpoModulesProvider.swift"; sourceTree = ""; }; + F1897054E8EB449588AD543A /* ReactNativeViewController.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = ReactNativeViewController.swift; path = ReactNativeViewController.swift; sourceTree = ""; }; + F9E6C4DB59158EA20C04837B /* Pods-minimaltester-minimaltesterbrownfield.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minimaltester-minimaltesterbrownfield.release.xcconfig"; path = "Target Support Files/Pods-minimaltester-minimaltesterbrownfield/Pods-minimaltester-minimaltesterbrownfield.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -65,15 +62,15 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 876FA08C072999C700D4484A /* Pods_minimaltester.framework in Frameworks */, + E2D36015D7B944A1D6108F5E /* Pods_minimaltester.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - F3716401F81F7636F6498445 /* Frameworks */ = { + B4FF9BB0EA555935619B8519 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 06622B612090727540ED45CE /* Pods_minimaltester_minimaltesterbrownfield.framework in Frameworks */, + 7559F23945462FBFE55EB02B /* Pods_minimaltester_minimaltesterbrownfield.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -89,54 +86,58 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, - D0062F01A46BE0B02344D4A7 /* PrivacyInfo.xcprivacy */, + B6B84DE78AFE8D9B0BF3A1AE /* PrivacyInfo.xcprivacy */, ); name = minimaltester; sourceTree = ""; }; + 1C70D8B3D2AECC85061C377D /* ExpoModulesProviders */ = { + isa = PBXGroup; + children = ( + 5AE231EA721443113504B772 /* minimaltester */, + 35F2B4EDEDB311BD14C4455E /* minimaltesterbrownfield */, + ); + name = ExpoModulesProviders; + sourceTree = ""; + }; 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - 93A03FEABBDADEBD8C94AEED /* Pods_minimaltester.framework */, - 56DFB06B472889A36A5C4CAA /* Pods_minimaltester_minimaltesterbrownfield.framework */, + 7948F5DE45F4CCA13BC7CCE1 /* Pods_minimaltester.framework */, + D4BCEA000CC8EF23719451B0 /* Pods_minimaltester_minimaltesterbrownfield.framework */, ); name = Frameworks; sourceTree = ""; }; - 2EE406FE6DB042A68B221FCB /* minimaltesterbrownfield */ = { + 35F2B4EDEDB311BD14C4455E /* minimaltesterbrownfield */ = { isa = PBXGroup; children = ( - 0723AF8BC7C24A8488C8A728 /* ReactNativeHostManager.swift */, - 9AD2C18B1B3E41F385A1269E /* Messaging.swift */, - 52ED535AE0BE4E1BB81BEB92 /* ReactNativeView.swift */, - AD3E045EB2784179B28082B4 /* ReactNativeViewController.swift */, - A060B4793D1349D4B6D474B8 /* ExpoAppDelegateWrapper.swift */, - 2675D3AF84C44D4CB5579B55 /* BrownfieldAppDelegate.swift */, - B793CDED927E439788FF7995 /* ReactNativeDelegate.swift */, + A632793DB62AC94D8DD148CE /* ExpoModulesProvider.swift */, ); name = minimaltesterbrownfield; - path = "/Users/gabriel/Workspace/expo/expo/apps/minimal-tester/ios/minimaltesterbrownfield"; sourceTree = ""; }; - 4880DBE58E57649C1EA0B43A /* Pods */ = { + 5AE231EA721443113504B772 /* minimaltester */ = { isa = PBXGroup; children = ( - 77064DBAC758F03A588FD466 /* Pods-minimaltester.debug.xcconfig */, - D624B426007DE7F11433CC43 /* Pods-minimaltester.release.xcconfig */, - 6951067E91FAB604A2570427 /* Pods-minimaltester-minimaltesterbrownfield.debug.xcconfig */, - A777BE16EC069C69A1AE9CBB /* Pods-minimaltester-minimaltesterbrownfield.release.xcconfig */, + 1BD6989B0D56002172F61C58 /* ExpoModulesProvider.swift */, ); - name = Pods; - path = Pods; + name = minimaltester; sourceTree = ""; }; - 7F92CE3AC6F88E0FFB19B6F6 /* minimaltester */ = { + 7C6E022EDB3F41AC8263C8BC /* minimaltesterbrownfield */ = { isa = PBXGroup; children = ( - FCB7EC3E85AC3A9EFB821A00 /* ExpoModulesProvider.swift */, + 034ED5E80B444A2B8FB1DFA3 /* ReactNativeHostManager.swift */, + 632501C1139B40D7ACA9F6D6 /* Messaging.swift */, + 5C3F4764CFF74FFA9E6218E0 /* ReactNativeView.swift */, + F1897054E8EB449588AD543A /* ReactNativeViewController.swift */, + 77B96E96905541BB9C1F4A2D /* ExpoAppDelegate.swift */, + 4D9E3508515F41689B15D18B /* ReactNativeDelegate.swift */, ); - name = minimaltester; + name = minimaltesterbrownfield; + path = "/Users/gabriel/Workspace/expo/expo/apps/minimal-tester/ios/minimaltesterbrownfield"; sourceTree = ""; }; 832341AE1AAA6A7D00B99B32 /* Libraries */ = { @@ -153,9 +154,9 @@ 832341AE1AAA6A7D00B99B32 /* Libraries */, 83CBBA001A601CBA00E9B192 /* Products */, 2D16E6871FA4F8E400B85C8A /* Frameworks */, - 2EE406FE6DB042A68B221FCB /* minimaltesterbrownfield */, - 4880DBE58E57649C1EA0B43A /* Pods */, - D044C4A1BF97FC1D63D9219A /* ExpoModulesProviders */, + 7C6E022EDB3F41AC8263C8BC /* minimaltesterbrownfield */, + 9FAFD98695548F2A2EAC0848 /* Pods */, + 1C70D8B3D2AECC85061C377D /* ExpoModulesProviders */, ); indentWidth = 2; sourceTree = ""; @@ -166,35 +167,30 @@ isa = PBXGroup; children = ( 13B07F961A680F5B00A75B9A /* minimaltester.app */, - 63467FB7339F4BC2A590D5F3 /* minimaltesterbrownfield.framework */, + 0ECB103E2B1E41ABB632C9C9 /* minimaltesterbrownfield.framework */, ); name = Products; sourceTree = ""; }; - BB2F792B24A3F905000567C9 /* Supporting */ = { + 9FAFD98695548F2A2EAC0848 /* Pods */ = { isa = PBXGroup; children = ( - BB2F792C24A3F905000567C9 /* Expo.plist */, + D758FCF0F9A6905996BB74EA /* Pods-minimaltester.debug.xcconfig */, + B0CE53A9CE031D7D00E2F0FC /* Pods-minimaltester.release.xcconfig */, + 2115662F5C3EDF6CC7E5F44C /* Pods-minimaltester-minimaltesterbrownfield.debug.xcconfig */, + F9E6C4DB59158EA20C04837B /* Pods-minimaltester-minimaltesterbrownfield.release.xcconfig */, ); - name = Supporting; - path = minimaltester/Supporting; - sourceTree = ""; - }; - BC05FBD95401DE2F3AAF2A75 /* minimaltesterbrownfield */ = { - isa = PBXGroup; - children = ( - C455BAA49D4A5E9BE0C154A5 /* ExpoModulesProvider.swift */, - ); - name = minimaltesterbrownfield; + name = Pods; + path = Pods; sourceTree = ""; }; - D044C4A1BF97FC1D63D9219A /* ExpoModulesProviders */ = { + BB2F792B24A3F905000567C9 /* Supporting */ = { isa = PBXGroup; children = ( - 7F92CE3AC6F88E0FFB19B6F6 /* minimaltester */, - BC05FBD95401DE2F3AAF2A75 /* minimaltesterbrownfield */, + BB2F792C24A3F905000567C9 /* Expo.plist */, ); - name = ExpoModulesProviders; + name = Supporting; + path = minimaltester/Supporting; sourceTree = ""; }; /* End PBXGroup section */ @@ -205,13 +201,13 @@ buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "minimaltester" */; buildPhases = ( 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */, - 006F9E912CA2F1C79F1B73AE /* [Expo] Configure project */, + 2EE4F4E4ADB205C8F7863171 /* [Expo] Configure project */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */, - 5836067EA70A7EC062CDFB4A /* [CP] Embed Pods Frameworks */, + 1BD2848BBB4D8D82D8DF7D09 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -222,17 +218,17 @@ productReference = 13B07F961A680F5B00A75B9A /* minimaltester.app */; productType = "com.apple.product-type.application"; }; - A9823C7D79664D388F829354 /* minimaltesterbrownfield */ = { + 3631C697DDD143E1B0171DB5 /* minimaltesterbrownfield */ = { isa = PBXNativeTarget; - buildConfigurationList = 929AE2CD9F294DD18AEE967B /* Build configuration list for PBXNativeTarget "minimaltesterbrownfield" */; + buildConfigurationList = 7A53170E006649D7A9A3FAD8 /* Build configuration list for PBXNativeTarget "minimaltesterbrownfield" */; buildPhases = ( - A7F21D87A963C5AC56A1F887 /* [CP] Check Pods Manifest.lock */, + 85AC744550B9B15D53CC6C51 /* [CP] Check Pods Manifest.lock */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, - 3A0DEAAC979CCE8D5F812B69 /* [Expo] Configure project */, - A39C0FAB88DC416BB5C2CE22 /* Patch ExpoModulesProvider */, - 4B335470CDC8431E938CAFC0 /* Sources */, - F3716401F81F7636F6498445 /* Frameworks */, - 6234857DD0A7E25AF2D99202 /* [CP] Copy Pods Resources */, + 3D0664A7CA09D733B4EA52D5 /* [Expo] Configure project */, + 19216A66F977408FA385C723 /* Patch ExpoModulesProvider */, + 04C95FF9C8814C978E8F47FE /* Sources */, + B4FF9BB0EA555935619B8519 /* Frameworks */, + 6B7357FF9C023513B2FA6B8E /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -240,7 +236,7 @@ ); name = minimaltesterbrownfield; productName = minimaltesterbrownfield; - productReference = 63467FB7339F4BC2A590D5F3 /* minimaltesterbrownfield.framework */; + productReference = 0ECB103E2B1E41ABB632C9C9 /* minimaltesterbrownfield.framework */; productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */ @@ -270,7 +266,7 @@ projectRoot = ""; targets = ( 13B07F861A680F5B00A75B9A /* minimaltester */, - A9823C7D79664D388F829354 /* minimaltesterbrownfield */, + 3631C697DDD143E1B0171DB5 /* minimaltesterbrownfield */, ); }; /* End PBXProject section */ @@ -283,37 +279,13 @@ BB2F792D24A3F905000567C9 /* Expo.plist in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */, - 124678CAE0EB3403D169D490 /* PrivacyInfo.xcprivacy in Resources */, + C9AE6CE96C58368B6AC63DF6 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 006F9E912CA2F1C79F1B73AE /* [Expo] Configure project */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "$(SRCROOT)/.xcode.env", - "$(SRCROOT)/.xcode.env.local", - "$(SRCROOT)/minimaltester/minimaltester.entitlements", - "$(SRCROOT)/Pods/Target Support Files/Pods-minimaltester/expo-configure-project.sh", - ); - name = "[Expo] Configure project"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(SRCROOT)/Pods/Target Support Files/Pods-minimaltester/ExpoModulesProvider.swift", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-minimaltester/expo-configure-project.sh\"\n"; - }; 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -353,7 +325,39 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 3A0DEAAC979CCE8D5F812B69 /* [Expo] Configure project */ = { + 19216A66F977408FA385C723 /* Patch ExpoModulesProvider */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Patch ExpoModulesProvider"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "FILE=\"${SRCROOT}/Pods/Target Support Files/Pods-minimaltester-minimaltesterbrownfield/ExpoModulesProvider.swift\"\nTEMP_FILE=\"$FILE.temp\"\n\nif [ -f \"$FILE\" ]; then\n echo \"Patching $FILE to hide Expo from public interface\"\n sed \\\n -e 's/^import EX/internal import EX/' \\\n -e 's/^import Ex/internal import Ex/' \\\n -e 's/^import EAS/internal import EAS/' \\\n -e 's/public class ExpoModulesProvider/internal class ExpoModulesProvider/' \"$FILE\" > \"$TEMP_FILE\"\n mv \"$TEMP_FILE\" \"$FILE\"\nfi\n"; + }; + 1BD2848BBB4D8D82D8DF7D09 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-minimaltester/Pods-minimaltester-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermesvm.framework/hermesvm", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermesvm.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-minimaltester/Pods-minimaltester-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 2EE4F4E4ADB205C8F7863171 /* [Expo] Configure project */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -364,38 +368,44 @@ inputPaths = ( "$(SRCROOT)/.xcode.env", "$(SRCROOT)/.xcode.env.local", - "$(SRCROOT)/minimaltesterbrownfield/minimaltesterbrownfield.entitlements", - "$(SRCROOT)/Pods/Target Support Files/Pods-minimaltester-minimaltesterbrownfield/expo-configure-project.sh", + "$(SRCROOT)/minimaltester/minimaltester.entitlements", + "$(SRCROOT)/Pods/Target Support Files/Pods-minimaltester/expo-configure-project.sh", ); name = "[Expo] Configure project"; outputFileListPaths = ( ); outputPaths = ( - "$(SRCROOT)/Pods/Target Support Files/Pods-minimaltester-minimaltesterbrownfield/ExpoModulesProvider.swift", + "$(SRCROOT)/Pods/Target Support Files/Pods-minimaltester/ExpoModulesProvider.swift", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-minimaltester-minimaltesterbrownfield/expo-configure-project.sh\"\n"; + shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-minimaltester/expo-configure-project.sh\"\n"; }; - 5836067EA70A7EC062CDFB4A /* [CP] Embed Pods Frameworks */ = { + 3D0664A7CA09D733B4EA52D5 /* [Expo] Configure project */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-minimaltester/Pods-minimaltester-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermesvm.framework/hermesvm", + "$(SRCROOT)/.xcode.env", + "$(SRCROOT)/.xcode.env.local", + "$(SRCROOT)/minimaltesterbrownfield/minimaltesterbrownfield.entitlements", + "$(SRCROOT)/Pods/Target Support Files/Pods-minimaltester-minimaltesterbrownfield/expo-configure-project.sh", + ); + name = "[Expo] Configure project"; + outputFileListPaths = ( ); - name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermesvm.framework", + "$(SRCROOT)/Pods/Target Support Files/Pods-minimaltester-minimaltesterbrownfield/ExpoModulesProvider.swift", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-minimaltester/Pods-minimaltester-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-minimaltester-minimaltesterbrownfield/expo-configure-project.sh\"\n"; }; - 6234857DD0A7E25AF2D99202 /* [CP] Copy Pods Resources */ = { + 6B7357FF9C023513B2FA6B8E /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -475,21 +485,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-minimaltester/Pods-minimaltester-resources.sh\"\n"; showEnvVarsInLog = 0; }; - A39C0FAB88DC416BB5C2CE22 /* Patch ExpoModulesProvider */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Patch ExpoModulesProvider"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "FILE=\"${SRCROOT}/Pods/Target Support Files/Pods-minimaltester-minimaltesterbrownfield/ExpoModulesProvider.swift\"\nTEMP_FILE=\"$FILE.temp\"\n\nif [ -f \"$FILE\" ]; then\n echo \"Patching $FILE to hide Expo from public interface\"\n sed \\\n -e 's/^import EX/internal import EX/' \\\n -e 's/^import Ex/internal import Ex/' \\\n -e 's/public class ExpoModulesProvider/internal class ExpoModulesProvider/' \"$FILE\" > \"$TEMP_FILE\"\n mv \"$TEMP_FILE\" \"$FILE\"\nfi\n"; - }; - A7F21D87A963C5AC56A1F887 /* [CP] Check Pods Manifest.lock */ = { + 85AC744550B9B15D53CC6C51 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -514,27 +510,26 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 13B07F871A680F5B00A75B9A /* Sources */ = { + 04C95FF9C8814C978E8F47FE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */, - 2A59239B2BA66E0A970797C8 /* ExpoModulesProvider.swift in Sources */, + 6F9BD8E3EAB1402EA5158FFD /* ReactNativeHostManager.swift in Sources */, + EED24139D38A4326B603DA6F /* Messaging.swift in Sources */, + 54DC818C26344A338FBD223F /* ReactNativeView.swift in Sources */, + A01F9B1FDF6D48F0822E6832 /* ReactNativeViewController.swift in Sources */, + CA92A3296199421E9509A2A9 /* ExpoAppDelegate.swift in Sources */, + 43C7C811D508488599D681D7 /* ReactNativeDelegate.swift in Sources */, + E4BC6394748FE19E01A0354F /* ExpoModulesProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 4B335470CDC8431E938CAFC0 /* Sources */ = { + 13B07F871A680F5B00A75B9A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9D457F228E2F42A280DB8B9F /* ReactNativeHostManager.swift in Sources */, - 346572B871564A6F957DA64C /* Messaging.swift in Sources */, - 0347E322C2B44B7F9AB46B9F /* ReactNativeView.swift in Sources */, - 89C57776E93C460EAC77E8FF /* ReactNativeViewController.swift in Sources */, - 32B40BCD62DE40CF9A6BB39F /* ExpoAppDelegateWrapper.swift in Sources */, - 8DF7AC7E10CD40609BA34F40 /* BrownfieldAppDelegate.swift in Sources */, - 9D90DFF4E1CF4081B765B7E7 /* ReactNativeDelegate.swift in Sources */, - 78BF7C54711ED17195852024 /* ExpoModulesProvider.swift in Sources */, + F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */, + 2BFB6924A0DE867335609929 /* ExpoModulesProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -543,7 +538,7 @@ /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 77064DBAC758F03A588FD466 /* Pods-minimaltester.debug.xcconfig */; + baseConfigurationReference = D758FCF0F9A6905996BB74EA /* Pods-minimaltester.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -579,7 +574,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D624B426007DE7F11433CC43 /* Pods-minimaltester.release.xcconfig */; + baseConfigurationReference = B0CE53A9CE031D7D00E2F0FC /* Pods-minimaltester.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -607,52 +602,6 @@ }; name = Release; }; - 5880ABECF1DA427B9ACEE6B6 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = A777BE16EC069C69A1AE9CBB /* Pods-minimaltester-minimaltesterbrownfield.release.xcconfig */; - buildSettings = { - BUILD_LIBRARY_FOR_DISTRIBUTION = YES; - CODE_SIGN_ENTITLEMENTS = minimaltesterbrownfield/minimaltesterbrownfield.entitlements; - CURRENT_PROJECT_VERSION = 1; - ENABLE_MODULE_VERIFIER = NO; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = minimaltesterbrownfield/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = minimaltesterbrownfield; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; - PRODUCT_BUNDLE_IDENTIFIER = com.community.minimaltesterbrownfield; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = NO; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - USER_SCRIPT_SANDBOXING = NO; - }; - name = Release; - }; - 7110731EB8C64CE7A7437C0A /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 6951067E91FAB604A2570427 /* Pods-minimaltester-minimaltesterbrownfield.debug.xcconfig */; - buildSettings = { - BUILD_LIBRARY_FOR_DISTRIBUTION = YES; - CODE_SIGN_ENTITLEMENTS = minimaltesterbrownfield/minimaltesterbrownfield.entitlements; - CURRENT_PROJECT_VERSION = 1; - ENABLE_MODULE_VERIFIER = NO; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = minimaltesterbrownfield/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = minimaltesterbrownfield; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; - PRODUCT_BUNDLE_IDENTIFIER = com.community.minimaltesterbrownfield; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = NO; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - USER_SCRIPT_SANDBOXING = NO; - }; - name = Debug; - }; 83CBBA201A601CBA00E9B192 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -799,6 +748,52 @@ }; name = Release; }; + A7B705EC444A46C291FEAFFF /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2115662F5C3EDF6CC7E5F44C /* Pods-minimaltester-minimaltesterbrownfield.debug.xcconfig */; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_ENTITLEMENTS = minimaltesterbrownfield/minimaltesterbrownfield.entitlements; + CURRENT_PROJECT_VERSION = 1; + ENABLE_MODULE_VERIFIER = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = minimaltesterbrownfield/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = minimaltesterbrownfield; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = com.community.minimaltesterbrownfield; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + USER_SCRIPT_SANDBOXING = NO; + }; + name = Debug; + }; + B343B89984824EE18D297C5C /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F9E6C4DB59158EA20C04837B /* Pods-minimaltester-minimaltesterbrownfield.release.xcconfig */; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_ENTITLEMENTS = minimaltesterbrownfield/minimaltesterbrownfield.entitlements; + CURRENT_PROJECT_VERSION = 1; + ENABLE_MODULE_VERIFIER = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = minimaltesterbrownfield/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = minimaltesterbrownfield; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = com.community.minimaltesterbrownfield; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + USER_SCRIPT_SANDBOXING = NO; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -811,20 +806,20 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "minimaltester" */ = { + 7A53170E006649D7A9A3FAD8 /* Build configuration list for PBXNativeTarget "minimaltesterbrownfield" */ = { isa = XCConfigurationList; buildConfigurations = ( - 83CBBA201A601CBA00E9B192 /* Debug */, - 83CBBA211A601CBA00E9B192 /* Release */, + A7B705EC444A46C291FEAFFF /* Debug */, + B343B89984824EE18D297C5C /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 929AE2CD9F294DD18AEE967B /* Build configuration list for PBXNativeTarget "minimaltesterbrownfield" */ = { + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "minimaltester" */ = { isa = XCConfigurationList; buildConfigurations = ( - 7110731EB8C64CE7A7437C0A /* Debug */, - 5880ABECF1DA427B9ACEE6B6 /* Release */, + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/apps/minimal-tester/ios/minimaltesterbrownfield/BrownfieldAppDelegate.swift b/apps/minimal-tester/ios/minimaltesterbrownfield/BrownfieldAppDelegate.swift deleted file mode 100644 index ef88fb59d151b1..00000000000000 --- a/apps/minimal-tester/ios/minimaltesterbrownfield/BrownfieldAppDelegate.swift +++ /dev/null @@ -1,170 +0,0 @@ -import UIKit - -@objc -open class BrownfieldAppDelegate: UIResponder, UIApplicationDelegate { - private var expoWrapper: ExpoAppDelegateWrapper? { - ReactNativeHostManager.shared.expoDelegateWrapper - } - - // SECTION: Initializing the app - open func application( - _ application: UIApplication, - willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil - ) -> Bool { - expoWrapper?.application(application, willFinishLaunchingWithOptions: launchOptions) ?? true - } - - open func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil - ) -> Bool { - expoWrapper?.application(application, didFinishLaunchingWithOptions: launchOptions) ?? true - } - - // END SECTION: Initializing the app - - // SECTION: Responding to App Life-Cycle Events - open func applicationDidBecomeActive(_ application: UIApplication) { - expoWrapper?.applicationDidBecomeActive(application) - } - - open func applicationWillResignActive(_ application: UIApplication) { - expoWrapper?.applicationWillResignActive(application) - } - - open func applicationDidEnterBackground(_ application: UIApplication) { - expoWrapper?.applicationDidEnterBackground(application) - } - - open func applicationWillEnterForeground(_ application: UIApplication) { - expoWrapper?.applicationWillEnterForeground(application) - } - - open func applicationWillTerminate(_ application: UIApplication) { - expoWrapper?.applicationWillTerminate(application) - } - - // END SECTION: Responding to App Life-Cycle Events - - // SECTION: Responding to Environment Changes - open func applicationDidReceiveMemoryWarning(_ application: UIApplication) { - expoWrapper?.applicationDidReceiveMemoryWarning(application) - } - - // END SECTION: Responding to Environment Changes - - // SECTION: Downloading Data in the Background - open func application( - _ application: UIApplication, - handleEventsForBackgroundURLSession identifier: String, - completionHandler: @escaping () -> Void - ) { - expoWrapper?.application( - application, - handleEventsForBackgroundURLSession: identifier, - completionHandler: completionHandler - ) - } - - // END SECTION: Downloading Data in the Background - - // SECTION: Handling Remote Notification Registration - open func application( - _ application: UIApplication, - didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data - ) { - expoWrapper?.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) - } - - open func application( - _ application: UIApplication, - didFailToRegisterForRemoteNotificationsWithError error: Error - ) { - expoWrapper?.application(application, didFailToRegisterForRemoteNotificationsWithError: error) - } - - open func application( - _ application: UIApplication, - didReceiveRemoteNotification userInfo: [AnyHashable: Any], - fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void - ) { - expoWrapper?.application( - application, - didReceiveRemoteNotification: userInfo, - fetchCompletionHandler: completionHandler - ) - } - - // END SECTION: Handling Remote Notification Registration - - // SECTION: Continuing User Activity and Handling Quick Actions - open func application( - _ application: UIApplication, - willContinueUserActivityWithType userActivityType: String - ) -> Bool { - expoWrapper?.application(application, willContinueUserActivityWithType: userActivityType) ?? false - } - - open func application( - _ application: UIApplication, - continue userActivity: NSUserActivity, - restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void - ) -> Bool { - expoWrapper?.application(application, continue: userActivity, restorationHandler: restorationHandler) ?? false - } - - open func application( - _ application: UIApplication, - didUpdate userActivity: NSUserActivity - ) { - expoWrapper?.application(application, didUpdate: userActivity) - } - - open func application( - _ application: UIApplication, - didFailToContinueUserActivityWithType userActivityType: String, - error: Error - ) { - expoWrapper?.application(application, didFailToContinueUserActivityWithType: userActivityType, error: error) - } - - open func application( - _ application: UIApplication, - performActionFor shortcutItem: UIApplicationShortcutItem, - completionHandler: @escaping (Bool) -> Void - ) { - expoWrapper?.application(application, performActionFor: shortcutItem, completionHandler: completionHandler) - } - - // END SECTION: Continuing User Activity and Handling Quick Actions - - // SECTION: Background Fetch - open func application( - _ application: UIApplication, - performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void - ) { - expoWrapper?.application(application, performFetchWithCompletionHandler: completionHandler) - } - - // END SECTION: Background Fetch - - // SECTION: Opening a URL-Specified Resource - open func application( - _ app: UIApplication, - open url: URL, - options: [UIApplication.OpenURLOptionsKey: Any] = [:] - ) -> Bool { - expoWrapper?.application(app, open: url, options: options) ?? false - } - - // END SECTION: Opening a URL-Specified Resource - - // SECTION: Managing Interface Geometry - open func application( - _ application: UIApplication, - supportedInterfaceOrientationsFor window: UIWindow? - ) -> UIInterfaceOrientationMask { - expoWrapper?.application(application, supportedInterfaceOrientationsFor: window) ?? .all - } - // END SECTION: Managing Interface Geometry -} diff --git a/apps/minimal-tester/ios/minimaltesterbrownfield/ExpoAppDelegate.swift b/apps/minimal-tester/ios/minimaltesterbrownfield/ExpoAppDelegate.swift new file mode 100644 index 00000000000000..f1e5a19d7cd8ac --- /dev/null +++ b/apps/minimal-tester/ios/minimaltesterbrownfield/ExpoAppDelegate.swift @@ -0,0 +1,247 @@ +import UIKit +internal import ExpoModulesCore + +/** + Allows classes extending `ExpoAppDelegateSubscriber` to hook into project's app delegate + by forwarding `UIApplicationDelegate` events to the subscribers. + + Keep functions and markers in sync with https://developer.apple.com/documentation/uikit/uiapplicationdelegate + */ +@objc +open class ExpoBrownfieldAppDelegate: UIResponder, UIApplicationDelegate { + override public init() { + // The subscribers are initializing and registering before the main code starts executing. + // Here we're letting them know when the `AppDelegate` is being created, + // which happens at the beginning of the main code execution and before launching the app. + ExpoAppDelegateSubscriberRepository.subscribers.forEach { + $0.appDelegateWillBeginInitialization?() + } + super.init() + } + +#if os(macOS) + required public init?(coder: NSCoder) { + super.init(coder: coder) + } +#endif + + // MARK: - Initializing the App +#if os(iOS) || os(tvOS) + + open func application( + _ application: UIApplication, + willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + return ExpoAppDelegateSubscriberManager.application(application, willFinishLaunchingWithOptions: launchOptions) + } + + open func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + return ExpoAppDelegateSubscriberManager.application(application, didFinishLaunchingWithOptions: launchOptions) + } + +#elseif os(macOS) + open func applicationWillFinishLaunching(_ notification: Notification) { + ExpoAppDelegateSubscriberManager.applicationWillFinishLaunching(notification) + } + + open func applicationDidFinishLaunching(_ notification: Notification) { + ExpoAppDelegateSubscriberManager.applicationDidFinishLaunching(notification) + } + + // TODO: - Configuring and Discarding Scenes +#endif + + // MARK: - Responding to App Life-Cycle Events + +#if os(iOS) || os(tvOS) + + @objc + open func applicationDidBecomeActive(_ application: UIApplication) { + ExpoAppDelegateSubscriberManager.applicationDidBecomeActive(application) + } + + @objc + open func applicationWillResignActive(_ application: UIApplication) { + ExpoAppDelegateSubscriberManager.applicationWillResignActive(application) + } + + @objc + open func applicationDidEnterBackground(_ application: UIApplication) { + ExpoAppDelegateSubscriberManager.applicationDidEnterBackground(application) + } + + open func applicationWillEnterForeground(_ application: UIApplication) { + ExpoAppDelegateSubscriberManager.applicationWillEnterForeground(application) + } + + open func applicationWillTerminate(_ application: UIApplication) { + ExpoAppDelegateSubscriberManager.applicationWillTerminate(application) + } + +#elseif os(macOS) + @objc + open func applicationDidBecomeActive(_ notification: Notification) { + ExpoAppDelegateSubscriberManager.applicationDidBecomeActive(notification) + } + + @objc + open func applicationWillResignActive(_ notification: Notification) { + ExpoAppDelegateSubscriberManager.applicationWillResignActive(notification) + } + + @objc + open func applicationDidHide(_ notification: Notification) { + ExpoAppDelegateSubscriberManager.applicationDidHide(notification) + } + + open func applicationWillUnhide(_ notification: Notification) { + ExpoAppDelegateSubscriberManager.applicationWillUnhide(notification) + } + + open func applicationWillTerminate(_ notification: Notification) { + ExpoAppDelegateSubscriberManager.applicationWillTerminate(notification) + } +#endif + + // MARK: - Responding to Environment Changes + +#if os(iOS) || os(tvOS) + + open func applicationDidReceiveMemoryWarning(_ application: UIApplication) { + ExpoAppDelegateSubscriberManager.applicationDidReceiveMemoryWarning(application) + } + +#endif + + // TODO: - Managing App State Restoration + + // MARK: - Downloading Data in the Background + +#if os(iOS) || os(tvOS) + open func application( + _ application: UIApplication, + handleEventsForBackgroundURLSession identifier: String, + completionHandler: @escaping () -> Void + ) { + ExpoAppDelegateSubscriberManager.application(application, handleEventsForBackgroundURLSession: identifier, completionHandler: completionHandler) + } + +#endif + + // MARK: - Handling Remote Notification Registration + + open func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + ExpoAppDelegateSubscriberManager.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) + } + + open func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + ExpoAppDelegateSubscriberManager.application(application, didFailToRegisterForRemoteNotificationsWithError: error) + } + +#if os(iOS) || os(tvOS) + open func application( + _ application: UIApplication, + didReceiveRemoteNotification userInfo: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { + ExpoAppDelegateSubscriberManager.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler) + } + +#elseif os(macOS) + open func application( + _ application: NSApplication, + didReceiveRemoteNotification userInfo: [String: Any] + ) { + ExpoAppDelegateSubscriberManager.application(application, didReceiveRemoteNotification: userInfo) + } +#endif + + // MARK: - Continuing User Activity and Handling Quick Actions + + open func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool { + return ExpoAppDelegateSubscriberManager.application(application, willContinueUserActivityWithType: userActivityType) + } + +#if os(iOS) || os(tvOS) + open func application( + _ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void + ) -> Bool { + return ExpoAppDelegateSubscriberManager.application(application, continue: userActivity, restorationHandler: restorationHandler) + } +#elseif os(macOS) + open func application( + _ application: NSApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([any NSUserActivityRestoring]) -> Void + ) -> Bool { + return ExpoAppDelegateSubscriberManager.application(application, continue: userActivity, restorationHandler: restorationHandler) + } +#endif + + open func application(_ application: UIApplication, didUpdate userActivity: NSUserActivity) { + return ExpoAppDelegateSubscriberManager.application(application, didUpdate: userActivity) + } + + open func application(_ application: UIApplication, didFailToContinueUserActivityWithType userActivityType: String, error: Error) { + return ExpoAppDelegateSubscriberManager.application(application, didFailToContinueUserActivityWithType: userActivityType, error: error) + } + +#if os(iOS) + open func application( + _ application: UIApplication, + performActionFor shortcutItem: UIApplicationShortcutItem, + completionHandler: @escaping (Bool) -> Void + ) { + ExpoAppDelegateSubscriberManager.application(application, performActionFor: shortcutItem, completionHandler: completionHandler) + } +#endif + + // MARK: - Background Fetch + +#if os(iOS) || os(tvOS) + open func application( + _ application: UIApplication, + performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { + ExpoAppDelegateSubscriberManager.application(application, performFetchWithCompletionHandler: completionHandler) + } + + // TODO: - Interacting With WatchKit + + // TODO: - Interacting With HealthKit +#endif + + // MARK: - Opening a URL-Specified Resource +#if os(iOS) || os(tvOS) + + open func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + return ExpoAppDelegateSubscriberManager.application(app, open: url, options: options) + } +#elseif os(macOS) + open func application(_ app: NSApplication, open urls: [URL]) { + ExpoAppDelegateSubscriberManager.application(app, open: urls) + } +#endif + // TODO: - Disallowing Specified App Extension Types + + // TODO: - Handling SiriKit Intents + + // TODO: - Handling CloudKit Invitations + + // MARK: - Managing Interface Geometry +#if os(iOS) + + /** + * Sets allowed orientations for the application. It will use the values from `Info.plist`as the orientation mask unless a subscriber requested + * a different orientation. + */ + open func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { + return ExpoAppDelegateSubscriberManager.application(application, supportedInterfaceOrientationsFor: window) + } +#endif +} diff --git a/apps/minimal-tester/ios/minimaltesterbrownfield/ExpoAppDelegateWrapper.swift b/apps/minimal-tester/ios/minimaltesterbrownfield/ExpoAppDelegateWrapper.swift deleted file mode 100644 index 9587b89a7d4188..00000000000000 --- a/apps/minimal-tester/ios/minimaltesterbrownfield/ExpoAppDelegateWrapper.swift +++ /dev/null @@ -1,199 +0,0 @@ -internal import Expo -internal import React - -public class ExpoAppDelegateWrapper { - private let expoDelegate: ExpoAppDelegate - - init(factory: RCTReactNativeFactory) { - expoDelegate = ExpoAppDelegate() - } - - // Below sections match sections from ExpoAppDelegate.swift: - // https://github.com/expo/expo/blob/main/packages/expo/ios/AppDelegates/ExpoAppDelegate.swift - // SECTION: Initializing the app - func application( - _ application: UIApplication, - willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - expoDelegate.application(application, willFinishLaunchingWithOptions: launchOptions) - } - - func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - expoDelegate.application(application, didFinishLaunchingWithOptions: launchOptions) - } - - // END SECTION: Initializing the app - - // SECTION: Configuring and discarding scenes - // TODO: Not defined in ExpoAppDelegate yet - // END SECTION: Configuring and discarding scenes - - // SECTION: Responding to App Life-Cycle Events - func applicationDidBecomeActive(_ application: UIApplication) { - expoDelegate.applicationDidBecomeActive(application) - } - - func applicationWillResignActive(_ application: UIApplication) { - expoDelegate.applicationWillResignActive(application) - } - - func applicationDidEnterBackground(_ application: UIApplication) { - expoDelegate.applicationDidEnterBackground(application) - } - - func applicationWillEnterForeground(_ application: UIApplication) { - expoDelegate.applicationWillEnterForeground(application) - } - - func applicationWillTerminate(_ application: UIApplication) { - expoDelegate.applicationWillTerminate(application) - } - - // END SECTION: Responding to App Life-Cycle Events - - // SECTION: Responding to Environment Changes - func applicationDidReceiveMemoryWarning(_ application: UIApplication) { - expoDelegate.applicationDidReceiveMemoryWarning(application) - } - - // END SECTION: Responding to Environment Changes - - // SECTION: Managing App State Restoration - // TODO: Not defined in ExpoAppDelegate yet - // END SECTION: Managing App State Restoration - - // SECTION: Downloading Data in the Background - func application( - _ application: UIApplication, - handleEventsForBackgroundURLSession identifier: String, - completionHandler: @escaping () -> Void - ) { - expoDelegate.application( - application, - handleEventsForBackgroundURLSession: identifier, - completionHandler: completionHandler - ) - } - - // END SECTION: Downloading Data in the Background - - // SECTION: Handling Remote Notification Registration - func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - expoDelegate.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) - } - - func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - expoDelegate.application(application, didFailToRegisterForRemoteNotificationsWithError: error) - } - - func application( - _ application: UIApplication, - didReceiveRemoteNotification userInfo: [AnyHashable: Any], - fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void - ) { - expoDelegate.application( - application, - didReceiveRemoteNotification: userInfo, - fetchCompletionHandler: completionHandler - ) - } - - // END SECTION: Handling Remote Notification Registration - - // SECTION: Continuing User Activity and Handling Quick Actions - func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool { - expoDelegate.application(application, willContinueUserActivityWithType: userActivityType) - } - - // func application( - // Expo implementation - // _ application: UIApplication, - // continue userActivity: NSUserActivity, - // restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void - // ) -> Bool { - // return expoDelegate.application(application, continue: userActivity, restorationHandler: restorationHandler) - // } - // Expo iOS app implementation - Universal Links - func application( - _ application: UIApplication, - continue userActivity: NSUserActivity, - restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void - ) -> Bool { - let result = RCTLinkingManager.application( - application, - continue: userActivity, - restorationHandler: restorationHandler - ) - return expoDelegate - .application(application, continue: userActivity, restorationHandler: restorationHandler) || result - } - - func application(_ application: UIApplication, didUpdate userActivity: NSUserActivity) { - expoDelegate.application(application, didUpdate: userActivity) - } - - func application( - _ application: UIApplication, - didFailToContinueUserActivityWithType userActivityType: String, - error: Error - ) { - expoDelegate.application(application, didFailToContinueUserActivityWithType: userActivityType, error: error) - } - - func application( - _ application: UIApplication, - performActionFor shortcutItem: UIApplicationShortcutItem, - completionHandler: @escaping (Bool) -> Void - ) { - expoDelegate.application(application, performActionFor: shortcutItem, completionHandler: completionHandler) - } - - // END SECTION: Continuing User Activity and Handling Quick Actions - - // SECTION: Background Fetch - func application( - _ application: UIApplication, - performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void - ) { - expoDelegate.application(application, performFetchWithCompletionHandler: completionHandler) - } - - // TODO: Interacting with WatchKit and HealthKit is not yet implemented - // in ExpoAppDelegate - // END SECTION: Background Fetch - - // SECTION: Opening a URL-Specified Resource - // Expo implementation - // func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> - // Bool { - // return expoDelegate.application(app, open: url, options: options) - // } - // Expo iOS app implementation - Linking API - func application( - _ app: UIApplication, - open url: URL, - options: [UIApplication.OpenURLOptionsKey: Any] = [:] - ) -> Bool { - expoDelegate.application(app, open: url, options: options) || - RCTLinkingManager.application(app, open: url, options: options) || false - } - - // TODO: Disallowing Specified App Extension Types, handling SiriKit Intents - // and CloudKit Invitations are not yet implemented in ExpoAppDelegate - // END SECTION: Opening a URL-Specified Resource - - // SECTION: Managing Interface Geometry - // Sets allowed orientations for the application. It will use the values from `Info.plist`as the orientation mask - // unless a subscriber requested - // a different orientation. - func application( - _ application: UIApplication, - supportedInterfaceOrientationsFor window: UIWindow? - ) -> UIInterfaceOrientationMask { - expoDelegate.application(application, supportedInterfaceOrientationsFor: window) - } - // END SECTION: Managing Interface Geometry -} diff --git a/apps/minimal-tester/ios/minimaltesterbrownfield/ReactNativeHostManager.swift b/apps/minimal-tester/ios/minimaltesterbrownfield/ReactNativeHostManager.swift index 77e9f2092c3d61..e15920c2d3e61d 100644 --- a/apps/minimal-tester/ios/minimaltesterbrownfield/ReactNativeHostManager.swift +++ b/apps/minimal-tester/ios/minimaltesterbrownfield/ReactNativeHostManager.swift @@ -9,10 +9,11 @@ public class ReactNativeHostManager { private var reactNativeDelegate: ExpoReactNativeFactoryDelegate? private var reactNativeFactory: RCTReactNativeFactory? - public private(set) var expoDelegateWrapper: ExpoAppDelegateWrapper? - /// Initializes the React Native host manager shared instance. - /// Prevents multiple initializations of the React Native host manager shared instance. + /** + * Initializes ReactNativeHostManager instance + * Instance can be initialized only once + */ public func initialize() { // Prevent multiple initializations guard reactNativeDelegate == nil else { @@ -26,13 +27,13 @@ public class ReactNativeHostManager { reactNativeDelegate = delegate reactNativeFactory = factory - expoDelegateWrapper = ExpoAppDelegateWrapper(factory: factory) - // Ensure this won't get stripped by the Swift compiler _ = ExpoModulesProvider() } - /// Loads and presents the React Native view. + /** + * Creates the React Native view using RCTReactNativeFactory + */ public func loadView( moduleName: String, initialProps: [AnyHashable: Any]?, diff --git a/apps/native-component-list/package.json b/apps/native-component-list/package.json index 972983c0e2e8ac..0132a080f0fe14 100644 --- a/apps/native-component-list/package.json +++ b/apps/native-component-list/package.json @@ -151,7 +151,7 @@ "react-native-worklets": "0.7.1", "react-native-reanimated": "4.2.1", "react-native-safe-area-context": "5.6.2", - "react-native-screens": "4.19.0", + "react-native-screens": "4.20.0", "react-native-svg": "15.15.1", "react-native-view-shot": "4.0.3", "react-native-web": "~0.21.0", diff --git a/apps/notification-tester/package.json b/apps/notification-tester/package.json index 20685cd9ffea7a..5a3e41a859f690 100644 --- a/apps/notification-tester/package.json +++ b/apps/notification-tester/package.json @@ -36,7 +36,7 @@ "react": "19.2.0", "react-native": "0.83.1", "react-native-safe-area-context": "5.6.2", - "react-native-screens": "4.19.0" + "react-native-screens": "4.20.0" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/apps/router-e2e/__e2e__/server-loader/app/index.tsx b/apps/router-e2e/__e2e__/server-loader/app/index.tsx index 386eb133ec622d..70e107675c377a 100644 --- a/apps/router-e2e/__e2e__/server-loader/app/index.tsx +++ b/apps/router-e2e/__e2e__/server-loader/app/index.tsx @@ -18,6 +18,7 @@ export default function Index() { Go to Second Go to Env Go to Request + Go to Response Go to static Post 1 Go to static Post 2 diff --git a/apps/router-e2e/__e2e__/server-loader/app/response.tsx b/apps/router-e2e/__e2e__/server-loader/app/response.tsx new file mode 100644 index 00000000000000..a73b0aeda8790f --- /dev/null +++ b/apps/router-e2e/__e2e__/server-loader/app/response.tsx @@ -0,0 +1,32 @@ +import { useLoaderData, usePathname } from 'expo-router'; +import { Container } from '../components/Container'; +import { Table, TableRow } from '../components/Table'; +import { SiteLinks, SiteLink } from '../components/SiteLink'; + +export async function loader() { + return Response.json({ foo: 'bar' }, { + headers: { + 'Cache-Control': 'public, max-age=3600', + 'X-Custom-Header': 'test-value', + }, + }); +} + +export default function ResponseRoute() { + const pathname = usePathname(); + const data = useLoaderData(); + + return ( + + + + +
+ + + Go to Index + Go to Second + +
+ ); +} \ No newline at end of file diff --git a/apps/router-e2e/__e2e__/server-loader/workerd/config.capnp b/apps/router-e2e/__e2e__/server-loader/workerd/config.capnp index d218ed437fe318..fc898364685a5a 100644 --- a/apps/router-e2e/__e2e__/server-loader/workerd/config.capnp +++ b/apps/router-e2e/__e2e__/server-loader/workerd/config.capnp @@ -17,6 +17,7 @@ const server :Workerd.Worker = ( (name = "_expo/loaders/posts/[postId].js", commonJsModule = embed "_expo/loaders/posts/[postId].js"), (name = "_expo/loaders/nullish/[value].js", commonJsModule = embed "_expo/loaders/nullish/[value].js"), (name = "_expo/loaders/request.js", commonJsModule = embed "_expo/loaders/request.js"), + (name = "_expo/loaders/response.js", commonJsModule = embed "_expo/loaders/response.js"), ], bindings = [ (name = "TEST_SECRET_KEY", text = "test-secret-key"), diff --git a/apps/router-e2e/package.json b/apps/router-e2e/package.json index dd4b48581d384b..9b0fcb7b0ebdec 100644 --- a/apps/router-e2e/package.json +++ b/apps/router-e2e/package.json @@ -69,7 +69,7 @@ "react": "19.2.0", "react-native": "0.83.1", "react-native-safe-area-context": "5.6.2", - "react-native-screens": "4.19.0", + "react-native-screens": "4.20.0", "react-native-webview": "13.16.0" }, "devDependencies": { diff --git a/docs/components/plugins/api/APISectionMethods.tsx b/docs/components/plugins/api/APISectionMethods.tsx index ddf1669036020f..aba369684a44b7 100644 --- a/docs/components/plugins/api/APISectionMethods.tsx +++ b/docs/components/plugins/api/APISectionMethods.tsx @@ -152,6 +152,7 @@ export const renderMethod = ( parameters, typeParameter )} + comment={method?.comment ?? comment} platforms={platforms.length > 0 ? platforms : parentPlatforms} baseNestingLevel={baseNestingLevel} // only show first overload in sidebar to avoid duplicates diff --git a/docs/constants/navigation.js b/docs/constants/navigation.js index ffb622a40ee835..4a2aee192bfbea 100644 --- a/docs/constants/navigation.js +++ b/docs/constants/navigation.js @@ -273,6 +273,7 @@ export const general = [ makeGroup('Web', [ makePage('router/web/api-routes.mdx'), makePage('router/web/middleware.mdx'), + makePage('router/web/server-headers.mdx'), makePage('router/web/static-rendering.mdx'), makePage('router/web/async-routes.mdx'), ]), diff --git a/docs/pages/eas/ai/mcp.mdx b/docs/pages/eas/ai/mcp.mdx index 14f2abd8fc717e..40e26d993d4bd4 100644 --- a/docs/pages/eas/ai/mcp.mdx +++ b/docs/pages/eas/ai/mcp.mdx @@ -56,7 +56,7 @@ The complete table of [MCP capabilities](#available-mcp-capabilities) documents Before using Expo MCP Server, ensure you have: - An Expo account with an EAS paid plan -- An Expo project with Expo SDK 54 and the latest `expo` package version +- An Expo project created either with `npx create-expo-app@latest` or has the latest `expo` package version installed - AI-assisted tools with remote MCP server support (Claude Code, Cursor, VS Code, and so on) ## Installation and setup @@ -113,6 +113,8 @@ Click the following link to install the MCP server for Cursor: +The above command adds the MCP server to your Codex configuration file and prompts you to authenticate with your Expo account. + @@ -127,7 +129,8 @@ After installing the MCP server, you'll need to authenticate using one of two me Generate a **Personal access token** from your Expo account and use it during the OAuth flow. -To generate an access token, open your EAS dashboard, navigate to **Credentials** > **Access tokens** > **Personal access tokens**, and then click **Create token**. +- To generate an access token, open [Access tokens](https://expo.dev/accounts/[account]/settings/access-tokens) settings page in EAS dashboard. +- Under **Personal access tokens**, click **Create token**. Copy the token and use it during the OAuth flow. #### Credentials diff --git a/docs/pages/guides/publishing-websites.mdx b/docs/pages/guides/publishing-websites.mdx index ec93b13d905bd6..979c87b6048d59 100644 --- a/docs/pages/guides/publishing-websites.mdx +++ b/docs/pages/guides/publishing-websites.mdx @@ -45,6 +45,8 @@ Expo Router supports three output targets for web apps. | `server` | | | Creates **client** and **server** directories. Client files are output as separate HTML files. API routes as separate JavaScript files for hosting with a custom Node.js server. | | `static` | | | Outputs separate HTML files for every route in the **app** directory. | +> **info** **Note**: For `static` and `server` output modes, you can configure [global HTTP headers](/router/web/server-headers) that are applied to all route responses via the `expo-router` plugin. + ## Create a build Creating a build of the project is the first step to publishing a web app. Whether you want to serve it locally or deploy to a hosting service, you'll need to export all JavaScript and assets of a project. This is known as a static bundle. It can be exported by running the following command: diff --git a/docs/pages/router/web/server-headers.mdx b/docs/pages/router/web/server-headers.mdx new file mode 100644 index 00000000000000..2df3de5f1f35e3 --- /dev/null +++ b/docs/pages/router/web/server-headers.mdx @@ -0,0 +1,210 @@ +--- +title: Server headers +description: Learn how to set custom HTTP headers for all server route responses in Expo Router. +--- + +import { BookOpen02Icon } from '@expo/styleguide-icons/outline/BookOpen02Icon'; + +import { BoxLink } from '~/ui/components/BoxLink'; +import { Collapsible } from '~/ui/components/Collapsible'; +import { Terminal } from '~/ui/components/Snippet'; +import { Step } from '~/ui/components/Step'; + +> **important** Server headers are available in SDK 54 and later, and requires [`expo-server`](/versions/latest/sdk/server/) to serve your exported application. + +Server headers in Expo Router allow you to set custom HTTP headers for security, caching, cookies, and custom metadata on route responses. Headers **only** apply to HTML and API route responses, and are not applicable to static assets such as images, fonts, or JavaScript bundles. + +## Setup + + + +Configure headers in the `expo-router` plugin in your [app config](/versions/latest/config/app/): + +```json app.json +{ + "expo": { + "plugins": [ + [ + "expo-router", + { + "headers": { + "X-Frame-Options": "DENY" + } + } + ] + ] + } +} +``` + + + + + +Start the development server or export for production: + + + +Headers are automatically applied to all HTML and API route responses. + + + +## Configuration + +Headers are configured as an object where keys are header names and values are either strings or arrays of strings. + +```json app.json +{ + "expo": { + "plugins": [ + [ + "expo-router", + { + "headers": { + "X-Frame-Options": "DENY", + "X-Content-Type-Options": "nosniff", + "Set-Cookie": ["session=abc123; HttpOnly", "preference=dark; Path=/"] + } + } + ] + ] + } +} +``` + +## Examples + + + +Add common security headers to protect your application: + +```json app.json +{ + "expo": { + "plugins": [ + [ + "expo-router", + { + "headers": { + "X-Frame-Options": "DENY", + "X-Content-Type-Options": "nosniff", + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-XSS-Protection": "1; mode=block" + } + } + ] + ] + } +} +``` + + + + + +Some web APIs like [`SharedArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) require specific Cross-Origin headers. This is required for features like [`expo-sqlite` on web](/versions/latest/sdk/sqlite/#web-setup). + +```json app.json +{ + "expo": { + "plugins": [ + [ + "expo-router", + { + "headers": { + "Cross-Origin-Embedder-Policy": "credentialless", + "Cross-Origin-Opener-Policy": "same-origin" + } + } + ] + ] + } +} +``` + + + + + +Set caching policies for your responses: + +```json app.json +{ + "expo": { + "plugins": [ + [ + "expo-router", + { + "headers": { + "Cache-Control": "public, max-age=3600, s-maxage=86400" + } + } + ] + ] + } +} +``` + + + + + +Add custom headers with metadata about your app: + +```json app.json +{ + "expo": { + "plugins": [ + [ + "expo-router", + { + "headers": { + "X-App-Version": "1.0.0", + "X-Environment": "production" + } + } + ] + ] + } +} +``` + + + +## How it works + +### Output modes + +Server headers work with both output modes configured in your app config: + +- **`static`**: Headers are applied when serving pre-rendered HTML files with [`expo-server`](/versions/latest/sdk/server/) +- **`server`**: Headers are applied to dynamically rendered responses + +### Header precedence + +Headers defined in the `expo-router` plugin are applied globally but do not override headers set by API routes. If an API route returns a response with a header that is also defined in the plugin configuration, the route-specific header takes precedence. + +For example, if you configure `Cache-Control: public, max-age=3600` globally, but an API route that returns real-time data sets `Cache-Control: no-store`, the API route's header takes precedence. + +## Known limitations + +- **Redirects**: Headers do not apply to redirect responses +- **Static assets**: Headers are only applied to HTML and API route responses, not to static assets like images, fonts, or JavaScript bundles + +## Related + + + + diff --git a/docs/pages/versions/unversioned/sdk/blob.mdx b/docs/pages/versions/unversioned/sdk/blob.mdx index 12ac6b492a8394..d16413b79e0551 100644 --- a/docs/pages/versions/unversioned/sdk/blob.mdx +++ b/docs/pages/versions/unversioned/sdk/blob.mdx @@ -4,7 +4,6 @@ description: A web standards-compliant Blob implementation for React Native. sourceCodeUrl: 'https://github.com/expo/expo/tree/main/packages/expo-blob' packageName: 'expo-blob' platforms: ['android', 'ios', 'web', 'expo-go'] -isNew: true --- import APISection from '~/components/plugins/APISection'; diff --git a/docs/pages/versions/unversioned/sdk/filesystem.mdx b/docs/pages/versions/unversioned/sdk/filesystem.mdx index 3d2918df57c122..469c05809d1094 100644 --- a/docs/pages/versions/unversioned/sdk/filesystem.mdx +++ b/docs/pages/versions/unversioned/sdk/filesystem.mdx @@ -5,7 +5,6 @@ sourceCodeUrl: 'https://github.com/expo/expo/tree/main/packages/expo-file-system packageName: 'expo-file-system' iconUrl: '/static/images/packages/expo-file-system.png' platforms: ['android', 'ios', 'tvos', 'expo-go'] -isNew: true --- import APISection from '~/components/plugins/APISection'; diff --git a/docs/pages/versions/unversioned/sdk/glass-effect.mdx b/docs/pages/versions/unversioned/sdk/glass-effect.mdx index c197dbaa5610b2..74e0ade35bc453 100644 --- a/docs/pages/versions/unversioned/sdk/glass-effect.mdx +++ b/docs/pages/versions/unversioned/sdk/glass-effect.mdx @@ -4,7 +4,6 @@ description: React components that render a liquid glass effect using iOS's nati sourceCodeUrl: 'https://github.com/expo/expo/tree/main/packages/expo-glass-effect' packageName: 'expo-glass-effect' platforms: ['ios', 'tvos', 'expo-go'] -isNew: true --- import APISection from '~/components/plugins/APISection'; diff --git a/docs/pages/versions/unversioned/sdk/router-native-tabs.mdx b/docs/pages/versions/unversioned/sdk/router-native-tabs.mdx index 488e04e7c5b73b..929aee2718e1ca 100644 --- a/docs/pages/versions/unversioned/sdk/router-native-tabs.mdx +++ b/docs/pages/versions/unversioned/sdk/router-native-tabs.mdx @@ -4,7 +4,6 @@ description: An Expo Router submodule that provides native tabs layout. sourceCodeUrl: 'https://github.com/expo/expo/tree/main/packages/expo-router' packageName: 'expo-router' platforms: ['android', 'ios', 'tvos', 'web', 'expo-go'] -isNew: true --- import { BookOpen02Icon } from '@expo/styleguide-icons/outline/BookOpen02Icon'; diff --git a/docs/pages/versions/unversioned/sdk/router-split-view.mdx b/docs/pages/versions/unversioned/sdk/router-split-view.mdx index ee364ca11d9e48..b792dbbc11b554 100644 --- a/docs/pages/versions/unversioned/sdk/router-split-view.mdx +++ b/docs/pages/versions/unversioned/sdk/router-split-view.mdx @@ -5,6 +5,7 @@ sourceCodeUrl: 'https://github.com/expo/expo/tree/main/packages/expo-router' packageName: 'expo-router' platforms: ['ios'] isAlpha: true +isNew: true --- import { BookOpen02Icon } from '@expo/styleguide-icons/outline/BookOpen02Icon'; diff --git a/docs/pages/versions/unversioned/sdk/server.mdx b/docs/pages/versions/unversioned/sdk/server.mdx index 2011ce2211393a..13cb0ac41c7720 100644 --- a/docs/pages/versions/unversioned/sdk/server.mdx +++ b/docs/pages/versions/unversioned/sdk/server.mdx @@ -4,7 +4,6 @@ description: Server-side API and runtime for Expo Router projects. sourceCodeUrl: 'https://github.com/expo/expo/tree/main/packages/expo-server' packageName: 'expo-server' platforms: ['server'] -isNew: true --- import { BookOpen02Icon } from '@expo/styleguide-icons/outline/BookOpen02Icon'; diff --git a/packages/@expo/cli/CHANGELOG.md b/packages/@expo/cli/CHANGELOG.md index 03b437c220493a..38eae1e4cb878d 100644 --- a/packages/@expo/cli/CHANGELOG.md +++ b/packages/@expo/cli/CHANGELOG.md @@ -22,6 +22,7 @@ - Add support for server data loaders in server export mode ([#41934](https://github.com/expo/expo/pull/41934) by [@hassankhan](https://github.com/hassankhan)) - Add `EXPO_UNSTABLE_BONJOUR` to activate Bonjour advertising ([#42138](https://github.com/expo/expo/pull/42138) by [@kitten](https://github.com/kitten)) - Respect `web.output` configuration in dev server for loaders ([#42147](https://github.com/expo/expo/pull/42147) by [@hassankhan](https://github.com/hassankhan)) +- Allow returning `Response` objects from loader functions ([#42051](https://github.com/expo/expo/pull/42051) by [@hassankhan](https://github.com/hassankhan)) ### 🐛 Bug fixes @@ -40,6 +41,7 @@ - Use `ImmutableRequest` for loader functions ([#42149](https://github.com/expo/expo/pull/42149) by [@hassankhan](https://github.com/hassankhan)) - Avoid module ID collision between loader and render bundles ([#42245](https://github.com/expo/expo/pull/42245) by [@hassankhan](https://github.com/hassankhan)) - Correctly show output mode in CLI log when exporting app ([#42269](https://github.com/expo/expo/pull/42269) by [@hassankhan](https://github.com/hassankhan)) +- Preserve search params for loader data fetches ([#42227](https://github.com/expo/expo/pull/42227) by [@hassankhan](https://github.com/hassankhan)) ### 💡 Others diff --git a/packages/@expo/cli/e2e/__tests__/export/server-loader.test.ts b/packages/@expo/cli/e2e/__tests__/export/server-loader.test.ts index 8d259fcd724c0f..7751f50bd9b153 100644 --- a/packages/@expo/cli/e2e/__tests__/export/server-loader.test.ts +++ b/packages/@expo/cli/e2e/__tests__/export/server-loader.test.ts @@ -53,6 +53,7 @@ describe.each( expect(files).toContain('_expo/loaders/second.js'); expect(files).toContain('_expo/loaders/posts/[postId].js'); expect(files).toContain('_expo/loaders/nullish/[value].js'); + expect(files).toContain('_expo/loaders/response.js'); }); (server.isExpoStart ? it.skip : it)('routes.json has loader paths', async () => { @@ -92,6 +93,17 @@ describe.each( expect(data.params).toHaveProperty('postId', 'my-test-post'); }); + it('loader endpoint returns `Response` with headers', async () => { + const response = await server.fetchAsync('/_expo/loaders/response'); + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toContain('application/json'); + expect(response.headers.get('cache-control')).toBe('public, max-age=3600'); + expect(response.headers.get('x-custom-header')).toBe('test-value'); + + const data = await response.json(); + expect(data).toEqual({ foo: 'bar' }); + }); + it('loader can access server environment variables', async () => { const response = await server.fetchAsync('/_expo/loaders/env'); expect(response.status).toBe(200); @@ -138,4 +150,30 @@ describe.each( expect(data.method).toBe('GET'); expect(Array.isArray(data.headers)).toBe(true); }); + + it.each([ + { + name: 'page', + url: '/request?foo=bar', + getData: async (response: Response) => { + const html = getHtml(await response.text()); + return JSON.parse(html.querySelector('[data-testid="loader-result"]')!.textContent); + }, + }, + { + name: 'loader endpoint', + url: '/_expo/loaders/request?foo=bar', + getData: (response: Response) => { + return response.json(); + }, + }, + ])('$name $url receives search params', async ({ getData, url }) => { + const response = await server.fetchAsync(url); + expect(response.status).toBe(200); + const data = await getData(response); + + expect(data.url).toContain('/request?foo=bar'); + expect(data.method).toBe('GET'); + expect(Array.isArray(data.headers)).toBe(true); + }); }); diff --git a/packages/@expo/cli/e2e/__tests__/export/static-loader.test.ts b/packages/@expo/cli/e2e/__tests__/export/static-loader.test.ts index bbe0821acae657..566084cd5335e6 100644 --- a/packages/@expo/cli/e2e/__tests__/export/static-loader.test.ts +++ b/packages/@expo/cli/e2e/__tests__/export/static-loader.test.ts @@ -49,6 +49,7 @@ describe.each( expect(files).toContain('_expo/loaders/posts/static-post-2'); expect(files).toContain('_expo/loaders/nullish/undefined'); expect(files).toContain('_expo/loaders/nullish/null'); + expect(files).toContain('_expo/loaders/response'); }); it('loader endpoint returns JSON', async () => { @@ -70,6 +71,19 @@ describe.each( expect(data.params).toHaveProperty('postId', 'static-post-1'); }); + it('loader endpoint returns `Response` body', async () => { + const response = await server.fetchAsync('/_expo/loaders/response'); + expect(response.status).toBe(200); + // NOTE(@hassankhan): expo-server returns `application/octet-stream` for extensionless files, + // but the content is still valid JSON. + // expect(response.headers.get('content-type')).toContain('application/json'); + expect(response.headers.get('cache-control')).not.toBe('public, max-age=3600'); + expect(response.headers.get('x-custom-header')).not.toBe('test-value'); + + const data = await response.json(); + expect(data).toEqual({ foo: 'bar' }); + }); + it('loader endpoint returns `{}` for `undefined` loader data', async () => { const response = await server.fetchAsync('/_expo/loaders/nullish/undefined'); expect(response.status).toBe(200); diff --git a/packages/@expo/cli/src/export/exportStaticAsync.ts b/packages/@expo/cli/src/export/exportStaticAsync.ts index 4b9492f4ebcb12..640fdb1aab11e2 100644 --- a/packages/@expo/cli/src/export/exportStaticAsync.ts +++ b/packages/@expo/cli/src/export/exportStaticAsync.ts @@ -252,18 +252,19 @@ export async function exportFromServerAsync( let renderOpts; if (useServerLoaders) { - const loaderResult = await executeLoaderAsync(normalizedPathname, route); + const loaderResponse = await executeLoaderAsync(normalizedPathname, route); - if (loaderResult !== undefined) { + if (loaderResponse !== undefined) { + const data = await loaderResponse.json(); const loaderPath = getLoaderModulePath(normalizedPathname); const fileSystemPath = loaderPath.startsWith('/') ? loaderPath.slice(1) : loaderPath; files.set(fileSystemPath, { - contents: JSON.stringify(loaderResult.data, null, 2), + contents: JSON.stringify(data, null, 2), targetDomain: 'client', loaderId: normalizedPathname, }); - renderOpts = { loader: { data: loaderResult.data } }; + renderOpts = { loader: { data } }; } } diff --git a/packages/@expo/cli/src/start/server/metro/MetroBundlerDevServer.ts b/packages/@expo/cli/src/start/server/metro/MetroBundlerDevServer.ts index 5ea316e0cf3adb..2d31efa1175a9b 100644 --- a/packages/@expo/cli/src/start/server/metro/MetroBundlerDevServer.ts +++ b/packages/@expo/cli/src/start/server/metro/MetroBundlerDevServer.ts @@ -469,7 +469,7 @@ export class MetroBundlerDevServer extends BundlerDevServer { route: RouteNode, opts?: GetStaticContentOptions ) => Promise; - executeLoaderAsync: (path: string, route: RouteNode) => Promise<{ data: unknown } | undefined>; + executeLoaderAsync: (path: string, route: RouteNode) => Promise; }> { const { routerRoot } = this.instanceMetroOptions; assert( @@ -640,7 +640,9 @@ export class MetroBundlerDevServer extends BundlerDevServer { if (!loaderResult) { return await getStaticContent(location); } - return await getStaticContent(location, { loader: { data: loaderResult.data } }); + + const loaderData = await loaderResult.json(); + return await getStaticContent(location, { loader: { data: loaderData } }); }; const [{ artifacts: resources }, staticHtml] = await Promise.all([ @@ -1706,9 +1708,9 @@ export class MetroBundlerDevServer extends BundlerDevServer { route: ResolvedLoaderRoute, // The `request` object is only available when using SSR request?: ImmutableRequest - ): Promise<{ data: unknown } | undefined> { + ): Promise { const { exp } = getConfig(this.projectRoot); - const { unstable_useServerDataLoaders } = exp.extra?.router; + const { unstable_useServerDataLoaders, unstable_useServerRendering } = exp.extra?.router; if (!unstable_useServerDataLoaders) { throw new CommandError( @@ -1742,14 +1744,29 @@ export class MetroBundlerDevServer extends BundlerDevServer { // Register this module for loader HMR this.setupLoaderHmr(modulePath); - const data = await routeModule.loader({ + const maybeResponse = await routeModule.loader({ params: route.params, request, }); + let data: unknown; + if (maybeResponse instanceof Response) { + debug('Loader returned Response for location:', location.pathname); + + // In SSR, preserve `Response` from the loader + if (exp.web?.output === 'server' && unstable_useServerRendering) { + return maybeResponse; + } + + // In SSG, extract body + data = await maybeResponse.json(); + } else { + data = maybeResponse; + } + const normalizedData = data === undefined ? {} : data; debug('Loader data:', normalizedData, ' for location:', location.pathname); - return { data: normalizedData }; + return Response.json(normalizedData); } debug('No loader found for location:', location.pathname); diff --git a/packages/@expo/cli/src/start/server/metro/createServerRouteMiddleware.ts b/packages/@expo/cli/src/start/server/metro/createServerRouteMiddleware.ts index 61b5860535fd6b..e6dc30458cd5d3 100644 --- a/packages/@expo/cli/src/start/server/metro/createServerRouteMiddleware.ts +++ b/packages/@expo/cli/src/start/server/metro/createServerRouteMiddleware.ts @@ -39,7 +39,7 @@ export function createRouteHandlerMiddleware( executeLoaderAsync: ( route: RouteInfo, request: ImmutableRequest - ) => Promise<{ data: unknown } | undefined>; + ) => Promise; config: ProjectConfig; headers: Record; } & import('@expo/router-server/build/routes-manifest').Options @@ -63,7 +63,7 @@ export function createRouteHandlerMiddleware( // In development, set `loader` property on all HTML routes. We can't know which routes // have loaders without bundling via Metro to detect exports. In production, this is // populated by `exportStaticAsync.ts` after bundling. - // At runtime, `getLoaderData()` returns `undefined` if no loader exists. + // At runtime, `getLoaderData()` returns a 404 response if no loader exists. for (const route of manifest.htmlRoutes) { route.loader = `_expo/loaders${route.page}.js`; } @@ -233,7 +233,8 @@ export function createRouteHandlerMiddleware( } }, async getLoaderData(request, route) { - return options.executeLoaderAsync(route, new ImmutableRequest(request)); + const response = await options.executeLoaderAsync(route, new ImmutableRequest(request)); + return response ?? new Response(null, { status: 404 }); }, } ); diff --git a/packages/@expo/router-server/CHANGELOG.md b/packages/@expo/router-server/CHANGELOG.md index 96636339972c29..f3f8895e54e6bd 100644 --- a/packages/@expo/router-server/CHANGELOG.md +++ b/packages/@expo/router-server/CHANGELOG.md @@ -12,6 +12,7 @@ ### 🐛 Bug fixes - resolve "Illegal invocation" errors in `workerd` runtime ([#41502](https://github.com/expo/expo/pull/41502) by [@hassankhan](https://github.com/hassankhan)) +- Preserve search params for loader data fetches ([#42227](https://github.com/expo/expo/pull/42227) by [@hassankhan](https://github.com/hassankhan)) ### 💡 Others diff --git a/packages/@expo/router-server/build/static/renderStaticContent.d.ts.map b/packages/@expo/router-server/build/static/renderStaticContent.d.ts.map index 5c49c9bbf08158..68b3e1738d7660 100644 --- a/packages/@expo/router-server/build/static/renderStaticContent.d.ts.map +++ b/packages/@expo/router-server/build/static/renderStaticContent.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"renderStaticContent.d.ts","sourceRoot":"","sources":["../../src/static/renderStaticContent.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,qBAAqB,CAAC;AA0B7B,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,CAAC,EAAE;QACP,IAAI,CAAC,EAAE,GAAG,CAAC;KACZ,CAAC;IACF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kEAAkE;IAClE,MAAM,CAAC,EAAE;QACP,GAAG,EAAE,MAAM,EAAE,CAAC;QACd,EAAE,EAAE,MAAM,EAAE,CAAC;KACd,CAAC;CACH,CAAC;AAEF,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,GAAG,EACb,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,MAAM,CAAC,CAkFjB;AAmBD,OAAO,EAAE,+BAA+B,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC"} \ No newline at end of file +{"version":3,"file":"renderStaticContent.d.ts","sourceRoot":"","sources":["../../src/static/renderStaticContent.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,qBAAqB,CAAC;AA0B7B,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,CAAC,EAAE;QACP,IAAI,CAAC,EAAE,GAAG,CAAC;KACZ,CAAC;IACF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kEAAkE;IAClE,MAAM,CAAC,EAAE;QACP,GAAG,EAAE,MAAM,EAAE,CAAC;QACd,EAAE,EAAE,MAAM,EAAE,CAAC;KACd,CAAC;CACH,CAAC;AAEF,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,GAAG,EACb,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,MAAM,CAAC,CAoFjB;AAmBD,OAAO,EAAE,+BAA+B,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC"} \ No newline at end of file diff --git a/packages/@expo/router-server/build/static/renderStaticContent.js b/packages/@expo/router-server/build/static/renderStaticContent.js index abc14669de757c..51fc3c1ddb83d3 100644 --- a/packages/@expo/router-server/build/static/renderStaticContent.js +++ b/packages/@expo/router-server/build/static/renderStaticContent.js @@ -83,7 +83,9 @@ async function getStaticContent(location, options) { // This MUST be run before `ReactDOMServer.renderToString` to prevent // "Warning: Detected multiple renderers concurrently rendering the same context provider. This is currently unsupported." resetReactNavigationContexts(); - const loadedData = options?.loader?.data !== undefined ? { [location.pathname]: options.loader.data } : null; + const loadedData = options?.loader?.data !== undefined + ? { [location.pathname + location.search]: options.loader.data } + : null; const html = server_node_1.default.renderToString( {element} ); diff --git a/packages/@expo/router-server/build/static/renderStaticContent.js.map b/packages/@expo/router-server/build/static/renderStaticContent.js.map index ccf1e8eb1e866b..1cacdf7a7c25c0 100644 --- a/packages/@expo/router-server/build/static/renderStaticContent.js.map +++ b/packages/@expo/router-server/build/static/renderStaticContent.js.map @@ -1 +1 @@ -{"version":3,"file":"renderStaticContent.js","sourceRoot":"","sources":["../../src/static/renderStaticContent.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * Copyright © 2023 650 Industries.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\nimport '@expo/metro-runtime';\n\nimport * as Font from 'expo-font/build/server';\nimport { ExpoRoot } from 'expo-router';\nimport { ctx } from 'expo-router/_ctx';\nimport Head from 'expo-router/head';\nimport { InnerRoot, registerStaticRootComponent } from 'expo-router/internal/static';\nimport React from 'react';\nimport ReactDOMServer from 'react-dom/server.node';\n\nimport { getRootComponent } from './getRootComponent';\nimport { PreloadedDataScript } from './html';\nimport { createDebug } from '../utils/debug';\n\nconst debug = createDebug('expo:router:server:renderStaticContent');\n\nfunction resetReactNavigationContexts() {\n // https://github.com/expo/router/discussions/588\n // https://github.com/react-navigation/react-navigation/blob/9fe34b445fcb86e5666f61e144007d7540f014fa/packages/elements/src/getNamedContext.tsx#LL3C1-L4C1\n\n // React Navigation is storing providers in a global, this is fine for the first static render\n // but subsequent static renders of Stack or Tabs will cause React to throw a warning. To prevent this warning, we'll reset the globals before rendering.\n const contexts = '__react_navigation__elements_contexts';\n (globalThis as any)[contexts] = new Map>();\n}\n\nexport type GetStaticContentOptions = {\n loader?: {\n data?: any;\n };\n request?: Request;\n /** Asset manifest for hydration bundles (JS/CSS). Used in SSR. */\n assets?: {\n css: string[];\n js: string[];\n };\n};\n\nexport async function getStaticContent(\n location: URL,\n options?: GetStaticContentOptions\n): Promise {\n const headContext: { helmet?: any } = {};\n const Root = getRootComponent();\n\n const {\n // NOTE: The `element` that's returned adds two extra Views and\n // the seemingly unused `RootTagContext.Provider`.\n element,\n getStyleElement,\n } = registerStaticRootComponent(ExpoRoot, {\n location,\n context: ctx,\n wrapper: ({ children }: React.ComponentProps) => (\n \n
{children}
\n
\n ),\n });\n\n // Clear any existing static resources from the global scope to attempt to prevent leaking between pages.\n // This could break if pages are rendered in parallel or if fonts are loaded outside of the React tree\n Font.resetServerContext();\n\n // This MUST be run before `ReactDOMServer.renderToString` to prevent\n // \"Warning: Detected multiple renderers concurrently rendering the same context provider. This is currently unsupported.\"\n resetReactNavigationContexts();\n\n const loadedData =\n options?.loader?.data !== undefined ? { [location.pathname]: options.loader.data } : null;\n\n const html = ReactDOMServer.renderToString(\n \n {element}\n \n );\n\n // Eval the CSS after the HTML is rendered so that the CSS is in the same order\n const css = ReactDOMServer.renderToStaticMarkup(getStyleElement());\n\n let output = mixHeadComponentsWithStaticResults(headContext.helmet, html);\n\n output = output.replace('', `${css}`);\n\n const fonts = Font.getServerResources();\n debug(`Pushing static fonts: (count: ${fonts.length})`, fonts);\n // debug('Push static fonts:', fonts)\n // Inject static fonts loaded with expo-font\n output = output.replace('', `${fonts.join('')}`);\n if (loadedData) {\n const loaderDataScript = ReactDOMServer.renderToStaticMarkup(\n \n );\n output = output.replace('', `${loaderDataScript}`);\n }\n\n // Inject hydration assets (JS/CSS bundles). Used in SSR mode\n if (options?.assets) {\n if (options.assets.css.length > 0) {\n /**\n * For each CSS file, inject two link elements; one for preloading and one as the actual\n * stylesheet. This matches what we do for SSG\n *\n * @see @expo/cli/src/start/server/metro/serializeHtml.ts\n */\n const injectedCSS = options.assets.css\n .flatMap((href) => [\n ``,\n ``,\n ])\n .join('\\n');\n output = output.replace('', `${injectedCSS}\\n`);\n }\n\n if (options.assets.js.length > 0) {\n const injectedJS = options.assets.js\n .map((src) => ``)\n .join('\\n');\n output = output.replace('', `${injectedJS}\\n`);\n }\n }\n\n return '' + output;\n}\n\nfunction mixHeadComponentsWithStaticResults(helmet: any, html: string) {\n // Head components\n for (const key of ['title', 'priority', 'meta', 'link', 'script', 'style'].reverse()) {\n const result = helmet?.[key]?.toString();\n if (result) {\n html = html.replace('', `${result}`);\n }\n }\n\n // attributes\n html = html.replace('>();\n}\n\nexport type GetStaticContentOptions = {\n loader?: {\n data?: any;\n };\n request?: Request;\n /** Asset manifest for hydration bundles (JS/CSS). Used in SSR. */\n assets?: {\n css: string[];\n js: string[];\n };\n};\n\nexport async function getStaticContent(\n location: URL,\n options?: GetStaticContentOptions\n): Promise {\n const headContext: { helmet?: any } = {};\n const Root = getRootComponent();\n\n const {\n // NOTE: The `element` that's returned adds two extra Views and\n // the seemingly unused `RootTagContext.Provider`.\n element,\n getStyleElement,\n } = registerStaticRootComponent(ExpoRoot, {\n location,\n context: ctx,\n wrapper: ({ children }: React.ComponentProps) => (\n \n
{children}
\n
\n ),\n });\n\n // Clear any existing static resources from the global scope to attempt to prevent leaking between pages.\n // This could break if pages are rendered in parallel or if fonts are loaded outside of the React tree\n Font.resetServerContext();\n\n // This MUST be run before `ReactDOMServer.renderToString` to prevent\n // \"Warning: Detected multiple renderers concurrently rendering the same context provider. This is currently unsupported.\"\n resetReactNavigationContexts();\n\n const loadedData =\n options?.loader?.data !== undefined\n ? { [location.pathname + location.search]: options.loader.data }\n : null;\n\n const html = ReactDOMServer.renderToString(\n \n {element}\n \n );\n\n // Eval the CSS after the HTML is rendered so that the CSS is in the same order\n const css = ReactDOMServer.renderToStaticMarkup(getStyleElement());\n\n let output = mixHeadComponentsWithStaticResults(headContext.helmet, html);\n\n output = output.replace('', `${css}`);\n\n const fonts = Font.getServerResources();\n debug(`Pushing static fonts: (count: ${fonts.length})`, fonts);\n // debug('Push static fonts:', fonts)\n // Inject static fonts loaded with expo-font\n output = output.replace('', `${fonts.join('')}`);\n if (loadedData) {\n const loaderDataScript = ReactDOMServer.renderToStaticMarkup(\n \n );\n output = output.replace('', `${loaderDataScript}`);\n }\n\n // Inject hydration assets (JS/CSS bundles). Used in SSR mode\n if (options?.assets) {\n if (options.assets.css.length > 0) {\n /**\n * For each CSS file, inject two link elements; one for preloading and one as the actual\n * stylesheet. This matches what we do for SSG\n *\n * @see @expo/cli/src/start/server/metro/serializeHtml.ts\n */\n const injectedCSS = options.assets.css\n .flatMap((href) => [\n ``,\n ``,\n ])\n .join('\\n');\n output = output.replace('', `${injectedCSS}\\n`);\n }\n\n if (options.assets.js.length > 0) {\n const injectedJS = options.assets.js\n .map((src) => ``)\n .join('\\n');\n output = output.replace('', `${injectedJS}\\n`);\n }\n }\n\n return '' + output;\n}\n\nfunction mixHeadComponentsWithStaticResults(helmet: any, html: string) {\n // Head components\n for (const key of ['title', 'priority', 'meta', 'link', 'script', 'style'].reverse()) {\n const result = helmet?.[key]?.toString();\n if (result) {\n html = html.replace('', `${result}`);\n }\n }\n\n // attributes\n html = html.replace(' diff --git a/packages/expo-brownfield/CHANGELOG.md b/packages/expo-brownfield/CHANGELOG.md index 9ebed32d06db29..3473458b08aff5 100644 --- a/packages/expo-brownfield/CHANGELOG.md +++ b/packages/expo-brownfield/CHANGELOG.md @@ -18,3 +18,4 @@ - Initialized the package for `expo-brownfield` with code from [expo-brownfield-target](https://github.com/software-mansion-labs/expo-brownfield-target) in [#42012](https://github.com/expo/expo/pull/42012) by [@pmleczek](https://github.com/pmleczek), [@gabrieldonadel](https://github.com/gabrieldonadel) - Updated `minimal-tester` to use `expo-brownfield` (includes 2 minor iOS improvments in the package) in [#42048](https://github.com/expo/expo/pull/42048) by [@gabrieldonadel](https://github.com/gabrieldonadel) - Updated build configurations and resolved leftover TODOs in [#42072](https://github.com/expo/expo/pull/42072) by [@pmleczek](https://github.com/pmleczek) +- [iOS] Symlink ExpoAppDelegate to iOS template ([#42240](https://github.com/expo/expo/pull/42240) by [@gabrieldonadel](https://github.com/gabrieldonadel)) diff --git a/packages/expo-brownfield/package.json b/packages/expo-brownfield/package.json index a00aad0277892a..9abbabcdfa3db8 100644 --- a/packages/expo-brownfield/package.json +++ b/packages/expo-brownfield/package.json @@ -22,6 +22,7 @@ "lint:fix": "expo-module lint --fix && expo-module lint plugin --fix && expo-module lint cli --fix", "test": "expo-module test", "prepare": "expo-module prepare", + "prepack": "cp -L plugin/templates/ios/ExpoAppDelegate.swift plugin/templates/ios/ExpoAppDelegate.swift.tmp && mv plugin/templates/ios/ExpoAppDelegate.swift.tmp plugin/templates/ios/ExpoAppDelegate.swift", "prepublishOnly": "expo-module prepublishOnly", "expo-module": "expo-module" }, @@ -47,11 +48,13 @@ "dependencies": { "arg": "^5.0.2", "chalk": "^4.1.2", + "diff": "^5.2.0", "expo-build-properties": "~1.0.8", "ora": "^5.4.1", "prompts": "^2.4.2" }, "devDependencies": { + "@types/diff": "^5.2.0", "expo-module-scripts": "^5.0.7", "jest-expo": "~54.0.11" }, diff --git a/packages/expo-brownfield/plugin/build/common/filesystem.d.ts b/packages/expo-brownfield/plugin/build/common/filesystem.d.ts index 4b2a44bf766fa4..f96d6eb545032d 100644 --- a/packages/expo-brownfield/plugin/build/common/filesystem.d.ts +++ b/packages/expo-brownfield/plugin/build/common/filesystem.d.ts @@ -3,3 +3,9 @@ export declare const mkdir: (path: string, recursive?: boolean) => void; export declare const createFileFromTemplate: (template: string, at: string, platform?: PlatformString, variables?: Record) => void; export declare const createFileFromTemplateAs: (template: string, at: string, as: string, platform?: PlatformString, variables?: Record) => void; export declare const readFromTemplate: (template: string, platform?: PlatformString, variables?: Record) => string; +/** + * Applies a unified diff patch to a file. + * @param patchFile - The name of the patch file in the patches directory + * @param targetFilePath - The absolute path to the file to patch + */ +export declare const applyPatchToFile: (patchFile: string, targetFilePath: string) => void; diff --git a/packages/expo-brownfield/plugin/build/common/filesystem.js b/packages/expo-brownfield/plugin/build/common/filesystem.js index 7256c359dc2d09..9325b769c1e82a 100644 --- a/packages/expo-brownfield/plugin/build/common/filesystem.js +++ b/packages/expo-brownfield/plugin/build/common/filesystem.js @@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.readFromTemplate = exports.createFileFromTemplateAs = exports.createFileFromTemplate = exports.mkdir = void 0; +exports.applyPatchToFile = exports.readFromTemplate = exports.createFileFromTemplateAs = exports.createFileFromTemplate = exports.mkdir = void 0; +const diff_1 = require("diff"); const node_fs_1 = require("node:fs"); const node_path_1 = __importDefault(require("node:path")); const mkdir = (path, recursive = false) => { @@ -74,3 +75,25 @@ const readFromTemplate = (template, platform, variables) => { return templateContents; }; exports.readFromTemplate = readFromTemplate; +/** + * Applies a unified diff patch to a file. + * @param patchFile - The name of the patch file in the patches directory + * @param targetFilePath - The absolute path to the file to patch + */ +const applyPatchToFile = (patchFile, targetFilePath) => { + const patchPath = node_path_1.default.join(__filename, '../../..', 'templates', 'patches', patchFile); + if (!(0, node_fs_1.existsSync)(patchPath)) { + throw new Error(`Patch file ${patchFile} doesn't exist at ${patchPath}`); + } + if (!(0, node_fs_1.existsSync)(targetFilePath)) { + throw new Error(`Target file doesn't exist at ${targetFilePath}`); + } + const patchContent = (0, node_fs_1.readFileSync)(patchPath, 'utf-8'); + const originalContent = (0, node_fs_1.readFileSync)(targetFilePath, 'utf-8'); + const patchedContent = (0, diff_1.applyPatch)(originalContent, patchContent); + if (patchedContent === false) { + throw new Error(`Failed to apply patch ${patchFile} to ${targetFilePath}`); + } + (0, node_fs_1.writeFileSync)(targetFilePath, patchedContent); +}; +exports.applyPatchToFile = applyPatchToFile; diff --git a/packages/expo-brownfield/plugin/build/ios/plugins/withXcodeProjectPlugin.js b/packages/expo-brownfield/plugin/build/ios/plugins/withXcodeProjectPlugin.js index f2ba90d0a4a604..6e442ab9fccfac 100644 --- a/packages/expo-brownfield/plugin/build/ios/plugins/withXcodeProjectPlugin.js +++ b/packages/expo-brownfield/plugin/build/ios/plugins/withXcodeProjectPlugin.js @@ -16,27 +16,26 @@ const withXcodeProjectPlugin = (config, pluginConfig) => { // Create a directory for the framework files const groupPath = node_path_1.default.join(projectRoot, 'ios', pluginConfig.targetName); (0, utils_1.mkdir)(groupPath); - // Create the React Native host manager based on the template - (0, utils_1.createFileFromTemplate)('ReactNativeHostManager.swift', groupPath); - // Create the messaging proxy based on the template - (0, utils_1.createFileFromTemplate)('Messaging.swift', groupPath); - // Create the SwiftUI brownfield entrypoint based on the template - (0, utils_1.createFileFromTemplate)('ReactNativeView.swift', groupPath); - // Create the UIKit brownfield view controller based on the template - (0, utils_1.createFileFromTemplate)('ReactNativeViewController.swift', groupPath); - // Create the BrownfieldAppDelegate based on the template - (0, utils_1.createFileFromTemplate)('BrownfieldAppDelegate.swift', groupPath); - // Create the ReactNativeDelegate based on the template - (0, utils_1.createFileFromTemplate)('ReactNativeDelegate.swift', groupPath); - // Create and properly add a new group for the framework - (0, utils_1.createGroup)(xcodeProject, pluginConfig.targetName, groupPath, [ + const templateFiles = [ + // React Native host manager 'ReactNativeHostManager.swift', + // Messaging proxy 'Messaging.swift', + //SwiftUI brownfield entrypoint 'ReactNativeView.swift', + // UIKit brownfield view controller 'ReactNativeViewController.swift', - 'BrownfieldAppDelegate.swift', + // ExpoAppDelegate symlinked and reexported from the main Expo package + 'ExpoAppDelegate.swift', + // ReactNativeDelegate 'ReactNativeDelegate.swift', - ]); + ]; + // Create files from templates + templateFiles.forEach((templateFile) => (0, utils_1.createFileFromTemplate)(templateFile, groupPath)); + // Apply patch to ExpoAppDelegate.swift to make it compatible with the brownfield framework + (0, utils_1.applyPatchToFile)('ExpoAppDelegate.patch', node_path_1.default.join(groupPath, 'ExpoAppDelegate.swift')); + // Create and properly add a new group for the framework + (0, utils_1.createGroup)(xcodeProject, pluginConfig.targetName, groupPath, templateFiles); // Create 'Info.plist' and '.entitlements' based on the templates (0, utils_1.createFileFromTemplate)('Info.plist', groupPath, { bundleIdentifier: pluginConfig.bundleIdentifier, @@ -46,18 +45,8 @@ const withXcodeProjectPlugin = (config, pluginConfig) => { // Configure build phases: // - Reference Expo app target's RN bundle script // - Add custom script for patching ExpoModulesProvider - // - Add 'ReactNativeHostManager.swift', 'ReactNativeView.swift', - // 'Messaging.swift', 'ReactNativeViewController.swift' and - // 'BrownfieldAppDelegate.swift' - // to the compile sources phase - (0, utils_1.configureBuildPhases)(xcodeProject, target, pluginConfig.targetName, projectName, [ - `${pluginConfig.targetName}/ReactNativeHostManager.swift`, - `${pluginConfig.targetName}/Messaging.swift`, - `${pluginConfig.targetName}/ReactNativeView.swift`, - `${pluginConfig.targetName}/ReactNativeViewController.swift`, - `${pluginConfig.targetName}/BrownfieldAppDelegate.swift`, - `${pluginConfig.targetName}/ReactNativeDelegate.swift`, - ]); + // - Add template files to the compile sources phase + (0, utils_1.configureBuildPhases)(xcodeProject, target, pluginConfig.targetName, projectName, templateFiles.map((file) => `${pluginConfig.targetName}/${file}`)); // Add the required build settings (0, utils_1.configureBuildSettings)(xcodeProject, pluginConfig.targetName, config.ios?.buildNumber || '1', pluginConfig.bundleIdentifier); return config; diff --git a/packages/expo-brownfield/plugin/build/ios/utils/filesystem.d.ts b/packages/expo-brownfield/plugin/build/ios/utils/filesystem.d.ts index a7424836be562e..2640b3c9e5b13b 100644 --- a/packages/expo-brownfield/plugin/build/ios/utils/filesystem.d.ts +++ b/packages/expo-brownfield/plugin/build/ios/utils/filesystem.d.ts @@ -1,3 +1,5 @@ +import { applyPatchToFile as applyPatchToFileCommon } from '../../common/filesystem'; +export { applyPatchToFileCommon as applyPatchToFile }; export declare const mkdir: (path: string, recursive?: boolean) => void; export declare const createFileFromTemplate: (template: string, at: string, variables?: Record) => void; export declare const createFileFromTemplateAs: (template: string, at: string, as: string, variables?: Record) => void; diff --git a/packages/expo-brownfield/plugin/build/ios/utils/filesystem.js b/packages/expo-brownfield/plugin/build/ios/utils/filesystem.js index 7bd7abb8805c80..ca45440d071cb1 100644 --- a/packages/expo-brownfield/plugin/build/ios/utils/filesystem.js +++ b/packages/expo-brownfield/plugin/build/ios/utils/filesystem.js @@ -3,9 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.readFromTemplate = exports.createFileFromTemplateAs = exports.createFileFromTemplate = exports.mkdir = void 0; +exports.readFromTemplate = exports.createFileFromTemplateAs = exports.createFileFromTemplate = exports.mkdir = exports.applyPatchToFile = void 0; const node_fs_1 = __importDefault(require("node:fs")); const filesystem_1 = require("../../common/filesystem"); +Object.defineProperty(exports, "applyPatchToFile", { enumerable: true, get: function () { return filesystem_1.applyPatchToFile; } }); const mkdir = (path, recursive = false) => { node_fs_1.default.mkdirSync(path, { recursive, diff --git a/packages/expo-brownfield/plugin/src/common/filesystem.ts b/packages/expo-brownfield/plugin/src/common/filesystem.ts index 1f067fbcd1bc2b..dfbcceaeb05b2d 100644 --- a/packages/expo-brownfield/plugin/src/common/filesystem.ts +++ b/packages/expo-brownfield/plugin/src/common/filesystem.ts @@ -1,3 +1,4 @@ +import { applyPatch } from 'diff'; import { accessSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import path from 'node:path'; @@ -104,3 +105,31 @@ export const readFromTemplate = ( return templateContents; }; + +/** + * Applies a unified diff patch to a file. + * @param patchFile - The name of the patch file in the patches directory + * @param targetFilePath - The absolute path to the file to patch + */ +export const applyPatchToFile = (patchFile: string, targetFilePath: string) => { + const patchPath = path.join(__filename, '../../..', 'templates', 'patches', patchFile); + + if (!existsSync(patchPath)) { + throw new Error(`Patch file ${patchFile} doesn't exist at ${patchPath}`); + } + + if (!existsSync(targetFilePath)) { + throw new Error(`Target file doesn't exist at ${targetFilePath}`); + } + + const patchContent = readFileSync(patchPath, 'utf-8'); + const originalContent = readFileSync(targetFilePath, 'utf-8'); + + const patchedContent = applyPatch(originalContent, patchContent); + + if (patchedContent === false) { + throw new Error(`Failed to apply patch ${patchFile} to ${targetFilePath}`); + } + + writeFileSync(targetFilePath, patchedContent); +}; diff --git a/packages/expo-brownfield/plugin/src/ios/plugins/withXcodeProjectPlugin.ts b/packages/expo-brownfield/plugin/src/ios/plugins/withXcodeProjectPlugin.ts index a6994279a4dcd4..89794b3ee81664 100644 --- a/packages/expo-brownfield/plugin/src/ios/plugins/withXcodeProjectPlugin.ts +++ b/packages/expo-brownfield/plugin/src/ios/plugins/withXcodeProjectPlugin.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import type { PluginConfig } from '../types'; import { + applyPatchToFile, configureBuildPhases, configureBuildSettings, createFileFromTemplate, @@ -30,28 +31,30 @@ const withXcodeProjectPlugin: ConfigPlugin = (config, pluginConfig // Create a directory for the framework files const groupPath = path.join(projectRoot, 'ios', pluginConfig.targetName); mkdir(groupPath); - // Create the React Native host manager based on the template - createFileFromTemplate('ReactNativeHostManager.swift', groupPath); - // Create the messaging proxy based on the template - createFileFromTemplate('Messaging.swift', groupPath); - // Create the SwiftUI brownfield entrypoint based on the template - createFileFromTemplate('ReactNativeView.swift', groupPath); - // Create the UIKit brownfield view controller based on the template - createFileFromTemplate('ReactNativeViewController.swift', groupPath); - // Create the BrownfieldAppDelegate based on the template - createFileFromTemplate('BrownfieldAppDelegate.swift', groupPath); - // Create the ReactNativeDelegate based on the template - createFileFromTemplate('ReactNativeDelegate.swift', groupPath); - // Create and properly add a new group for the framework - createGroup(xcodeProject, pluginConfig.targetName, groupPath, [ + const templateFiles = [ + // React Native host manager 'ReactNativeHostManager.swift', + // Messaging proxy 'Messaging.swift', + //SwiftUI brownfield entrypoint 'ReactNativeView.swift', + // UIKit brownfield view controller 'ReactNativeViewController.swift', - 'BrownfieldAppDelegate.swift', + // ExpoAppDelegate symlinked and reexported from the main Expo package + 'ExpoAppDelegate.swift', + // ReactNativeDelegate 'ReactNativeDelegate.swift', - ]); + ]; + + // Create files from templates + templateFiles.forEach((templateFile) => createFileFromTemplate(templateFile, groupPath)); + + // Apply patch to ExpoAppDelegate.swift to make it compatible with the brownfield framework + applyPatchToFile('ExpoAppDelegate.patch', path.join(groupPath, 'ExpoAppDelegate.swift')); + + // Create and properly add a new group for the framework + createGroup(xcodeProject, pluginConfig.targetName, groupPath, templateFiles); // Create 'Info.plist' and '.entitlements' based on the templates createFileFromTemplate('Info.plist', groupPath, { @@ -67,18 +70,14 @@ const withXcodeProjectPlugin: ConfigPlugin = (config, pluginConfig // Configure build phases: // - Reference Expo app target's RN bundle script // - Add custom script for patching ExpoModulesProvider - // - Add 'ReactNativeHostManager.swift', 'ReactNativeView.swift', - // 'Messaging.swift', 'ReactNativeViewController.swift' and - // 'BrownfieldAppDelegate.swift' - // to the compile sources phase - configureBuildPhases(xcodeProject, target, pluginConfig.targetName, projectName, [ - `${pluginConfig.targetName}/ReactNativeHostManager.swift`, - `${pluginConfig.targetName}/Messaging.swift`, - `${pluginConfig.targetName}/ReactNativeView.swift`, - `${pluginConfig.targetName}/ReactNativeViewController.swift`, - `${pluginConfig.targetName}/BrownfieldAppDelegate.swift`, - `${pluginConfig.targetName}/ReactNativeDelegate.swift`, - ]); + // - Add template files to the compile sources phase + configureBuildPhases( + xcodeProject, + target, + pluginConfig.targetName, + projectName, + templateFiles.map((file) => `${pluginConfig.targetName}/${file}`) + ); // Add the required build settings configureBuildSettings( xcodeProject, diff --git a/packages/expo-brownfield/plugin/src/ios/utils/filesystem.ts b/packages/expo-brownfield/plugin/src/ios/utils/filesystem.ts index 56119e8322c27f..0aeb7cb5b882c2 100644 --- a/packages/expo-brownfield/plugin/src/ios/utils/filesystem.ts +++ b/packages/expo-brownfield/plugin/src/ios/utils/filesystem.ts @@ -1,11 +1,14 @@ import fs from 'node:fs'; import { + applyPatchToFile as applyPatchToFileCommon, createFileFromTemplate as createFileFromTemplateCommon, createFileFromTemplateAs as createFileFromTemplateAsCommon, readFromTemplate as readFromTemplateCommon, } from '../../common/filesystem'; +export { applyPatchToFileCommon as applyPatchToFile }; + export const mkdir = (path: string, recursive: boolean = false) => { fs.mkdirSync(path, { recursive, diff --git a/packages/expo-brownfield/plugin/templates/ios/BrownfieldAppDelegate.swift b/packages/expo-brownfield/plugin/templates/ios/BrownfieldAppDelegate.swift deleted file mode 100644 index e3e8c9bbe5b6b5..00000000000000 --- a/packages/expo-brownfield/plugin/templates/ios/BrownfieldAppDelegate.swift +++ /dev/null @@ -1,171 +0,0 @@ -internal import ExpoModulesCore -import UIKit - -@objc -open class BrownfieldAppDelegate: UIResponder, UIApplicationDelegate { - // TODO(pmleczek): Add shared instance to enable using single methods (?) - - // SECTION: Initializing the app - open func application( - _ application: UIApplication, - willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil - ) -> Bool { - ExpoAppDelegateSubscriberManager.application( - application, willFinishLaunchingWithOptions: launchOptions) - } - - open func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil - ) -> Bool { - ExpoAppDelegateSubscriberManager.application( - application, didFinishLaunchingWithOptions: launchOptions) - } - // END SECTION: Initializing the app - - // SECTION: Responding to App Life-Cycle Events - open func applicationDidBecomeActive(_ application: UIApplication) { - ExpoAppDelegateSubscriberManager.applicationDidBecomeActive(application) - } - - open func applicationWillResignActive(_ application: UIApplication) { - ExpoAppDelegateSubscriberManager.applicationWillResignActive(application) - } - - open func applicationDidEnterBackground(_ application: UIApplication) { - ExpoAppDelegateSubscriberManager.applicationDidEnterBackground(application) - } - - open func applicationWillEnterForeground(_ application: UIApplication) { - ExpoAppDelegateSubscriberManager.applicationWillEnterForeground(application) - } - - open func applicationWillTerminate(_ application: UIApplication) { - ExpoAppDelegateSubscriberManager.applicationWillTerminate(application) - } - // END SECTION: Responding to App Life-Cycle Events - - // SECTION: Responding to Environment Changes - open func applicationDidReceiveMemoryWarning(_ application: UIApplication) { - ExpoAppDelegateSubscriberManager.applicationDidReceiveMemoryWarning(application) - } - // END SECTION: Responding to Environment Changes - - // SECTION: Downloading Data in the Background - open func application( - _ application: UIApplication, - handleEventsForBackgroundURLSession identifier: String, - completionHandler: @escaping () -> Void - ) { - ExpoAppDelegateSubscriberManager.application( - application, - handleEventsForBackgroundURLSession: identifier, - completionHandler: completionHandler - ) - } - // END SECTION: Downloading Data in the Background - - // SECTION: Handling Remote Notification Registration - open func application( - _ application: UIApplication, - didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data - ) { - ExpoAppDelegateSubscriberManager.application( - application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) - } - - open func application( - _ application: UIApplication, - didFailToRegisterForRemoteNotificationsWithError error: Error - ) { - ExpoAppDelegateSubscriberManager.application( - application, didFailToRegisterForRemoteNotificationsWithError: error) - } - - open func application( - _ application: UIApplication, - didReceiveRemoteNotification userInfo: [AnyHashable: Any], - fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void - ) { - ExpoAppDelegateSubscriberManager.application( - application, - didReceiveRemoteNotification: userInfo, - fetchCompletionHandler: completionHandler - ) - } - // END SECTION: Handling Remote Notification Registration - - // SECTION: Continuing User Activity and Handling Quick Actions - open func application( - _ application: UIApplication, - willContinueUserActivityWithType userActivityType: String - ) -> Bool { - ExpoAppDelegateSubscriberManager.application( - application, willContinueUserActivityWithType: userActivityType) - } - - open func application( - _ application: UIApplication, - continue userActivity: NSUserActivity, - restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void - ) -> Bool { - ExpoAppDelegateSubscriberManager.application( - application, continue: userActivity, restorationHandler: restorationHandler) - } - - open func application( - _ application: UIApplication, - didUpdate userActivity: NSUserActivity - ) { - ExpoAppDelegateSubscriberManager.application(application, didUpdate: userActivity) - } - - open func application( - _ application: UIApplication, - didFailToContinueUserActivityWithType userActivityType: String, - error: Error - ) { - ExpoAppDelegateSubscriberManager.application( - application, didFailToContinueUserActivityWithType: userActivityType, error: error) - } - - open func application( - _ application: UIApplication, - performActionFor shortcutItem: UIApplicationShortcutItem, - completionHandler: @escaping (Bool) -> Void - ) { - ExpoAppDelegateSubscriberManager.application( - application, performActionFor: shortcutItem, completionHandler: completionHandler) - } - // END SECTION: Continuing User Activity and Handling Quick Actions - - // SECTION: Background Fetch - open func application( - _ application: UIApplication, - performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void - ) { - ExpoAppDelegateSubscriberManager.application( - application, performFetchWithCompletionHandler: completionHandler) - } - // END SECTION: Background Fetch - - // SECTION: Opening a URL-Specified Resource - open func application( - _ app: UIApplication, - open url: URL, - options: [UIApplication.OpenURLOptionsKey: Any] = [:] - ) -> Bool { - ExpoAppDelegateSubscriberManager.application(app, open: url, options: options) - } - // END SECTION: Opening a URL-Specified Resource - - // SECTION: Managing Interface Geometry - open func application( - _ application: UIApplication, - supportedInterfaceOrientationsFor window: UIWindow? - ) -> UIInterfaceOrientationMask { - ExpoAppDelegateSubscriberManager.application( - application, supportedInterfaceOrientationsFor: window) - } - // END SECTION: Managing Interface Geometry -} diff --git a/packages/expo-brownfield/plugin/templates/ios/ExpoAppDelegate.swift b/packages/expo-brownfield/plugin/templates/ios/ExpoAppDelegate.swift new file mode 120000 index 00000000000000..ec8bccd5cb2af5 --- /dev/null +++ b/packages/expo-brownfield/plugin/templates/ios/ExpoAppDelegate.swift @@ -0,0 +1 @@ +/Users/gabriel/Workspace/expo/expo/packages/expo/ios/AppDelegates/ExpoAppDelegate.swift \ No newline at end of file diff --git a/packages/expo-brownfield/plugin/templates/patches/ExpoAppDelegate.patch b/packages/expo-brownfield/plugin/templates/patches/ExpoAppDelegate.patch new file mode 100644 index 00000000000000..7ec819ce5fb64e --- /dev/null +++ b/packages/expo-brownfield/plugin/templates/patches/ExpoAppDelegate.patch @@ -0,0 +1,23 @@ +diff --git a/plugin/templates/ios/ExpoAppDelegate.swift b/plugin/templates/ios/ExpoAppDelegate.swift +index 09766c71fd8..f1e5a19d7cd 100644 +--- a/plugin/templates/ios/ExpoAppDelegate.swift ++++ b/plugin/templates/ios/ExpoAppDelegate.swift +@@ -1,5 +1,5 @@ +-import Foundation +-import ExpoModulesCore ++import UIKit ++internal import ExpoModulesCore + + /** + Allows classes extending `ExpoAppDelegateSubscriber` to hook into project's app delegate +@@ -7,8 +7,8 @@ import ExpoModulesCore + + Keep functions and markers in sync with https://developer.apple.com/documentation/uikit/uiapplicationdelegate + */ +-@objc(EXExpoAppDelegate) +-open class ExpoAppDelegate: UIResponder, UIApplicationDelegate { ++@objc ++open class ExpoBrownfieldAppDelegate: UIResponder, UIApplicationDelegate { + override public init() { + // The subscribers are initializing and registering before the main code starts executing. + // Here we're letting them know when the `AppDelegate` is being created, diff --git a/packages/expo-modules-autolinking/scripts/ios/autolinking_manager.rb b/packages/expo-modules-autolinking/scripts/ios/autolinking_manager.rb index 690e1a9c58998a..cf2123dd9cd9c1 100644 --- a/packages/expo-modules-autolinking/scripts/ios/autolinking_manager.rb +++ b/packages/expo-modules-autolinking/scripts/ios/autolinking_manager.rb @@ -172,9 +172,7 @@ class AutolinkingManager return @options.fetch(:appRoot, @options.fetch(:projectRoot, nil)) end - # privates - - private def resolve + public def resolve json = [] IO.popen(resolve_command_args) do |data| diff --git a/packages/expo-modules-core/android/src/main/java/expo/modules/kotlin/AppContext.kt b/packages/expo-modules-core/android/src/main/java/expo/modules/kotlin/AppContext.kt index 18cb18a1fb5834..28165ce11c1a7f 100644 --- a/packages/expo-modules-core/android/src/main/java/expo/modules/kotlin/AppContext.kt +++ b/packages/expo-modules-core/android/src/main/java/expo/modules/kotlin/AppContext.kt @@ -129,6 +129,8 @@ class AppContext( registry.register(modulesProvider) + registerInlineModulesList() + logger.info("✅ AppContext was initialized") } } @@ -137,6 +139,14 @@ class AppContext( registry.postOnCreate() } + private fun registerInlineModulesList() { + try { + val inlineModulesList = Class.forName("inline.modules.ExpoInlineModulesList").getConstructor() + .newInstance() as ModulesProvider + registry.register(inlineModulesList) + } catch (_: ClassNotFoundException) {} + } + /** * Initializes a JSI part of the module registry. * It will be a NOOP if the remote debugging was activated. diff --git a/packages/expo-router/CHANGELOG.md b/packages/expo-router/CHANGELOG.md index 8f1d4c579ba6ce..39b715c391e29f 100644 --- a/packages/expo-router/CHANGELOG.md +++ b/packages/expo-router/CHANGELOG.md @@ -69,6 +69,7 @@ - add replace action handling to headless tabs ([#41815](https://github.com/expo/expo/pull/41815) by [@Ubax](https://github.com/Ubax)) - [ios] fix build error 'Logger' is ambiguous ([#42229](https://github.com/expo/expo/pull/42229) by [@Ubax](https://github.com/Ubax)) - [ios] fix shadow color in native tabs ([#42125](https://github.com/expo/expo/pull/42125) by [@Ubax](https://github.com/Ubax)) +- Preserve search params for loader data fetches ([#42227](https://github.com/expo/expo/pull/42227) by [@hassankhan](https://github.com/hassankhan)) ### 💡 Others @@ -110,6 +111,7 @@ - [iOS] reduce number of times UIBarButtonItem is recreated ([#41900](https://github.com/expo/expo/pull/41900) by [@Ubax](https://github.com/Ubax)) - [ios] add comment to ENV['RNS_GAMMA_ENABLED'] set by config plugin ([#42231](https://github.com/expo/expo/pull/42231) by [@Ubax](https://github.com/Ubax)) - add `unstable_navigationEvents` to `globalThis.expo` ([#42238](https://github.com/expo/expo/pull/42238) by [@Ubax](https://github.com/Ubax)) +- Upgrade react-native-screens to 4.20.0 ([#42282](https://github.com/expo/expo/pull/42282) by [@Ubax](https://github.com/Ubax)) ## 6.0.17 - 2025-12-05 diff --git a/packages/expo-router/build/loaders/utils.d.ts b/packages/expo-router/build/loaders/utils.d.ts index ab48989a5b342d..ddf75a55abf671 100644 --- a/packages/expo-router/build/loaders/utils.d.ts +++ b/packages/expo-router/build/loaders/utils.d.ts @@ -6,13 +6,16 @@ * getLoaderModulePath(`/about`) // `/_expo/loaders/about` * getLoaderModulePath(`/posts/1`) // `/_expo/loaders/posts/1` */ -export declare function getLoaderModulePath(pathname: string): string; +export declare function getLoaderModulePath(routePath: string): string; /** * Fetches and parses a loader module from the given route path. * This works in all environments including: - * 1. Development with Metro dev server (see `LoaderModuleMiddleware`) + * 1. Development with Metro dev server * 2. Production with static files (SSG) * 3. SSR environments + * + * @see import('packages/@expo/cli/src/start/server/metro/createServerRouteMiddleware.ts').createRouteHandlerMiddleware + * @see import('packages/expo-server/src/vendor/environment/common.ts').createEnvironment */ export declare function fetchLoaderModule(routePath: string): Promise; //# sourceMappingURL=utils.d.ts.map \ No newline at end of file diff --git a/packages/expo-router/build/loaders/utils.d.ts.map b/packages/expo-router/build/loaders/utils.d.ts.map index f94aaf04fd53bb..e5106e84bbe043 100644 --- a/packages/expo-router/build/loaders/utils.d.ts.map +++ b/packages/expo-router/build/loaders/utils.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/loaders/utils.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAM5D;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAiBvE"} \ No newline at end of file +{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/loaders/utils.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAM7D;AAED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAiBvE"} \ No newline at end of file diff --git a/packages/expo-router/build/loaders/utils.js b/packages/expo-router/build/loaders/utils.js index 3d617234e1fb1c..04b70aa3464c1e 100644 --- a/packages/expo-router/build/loaders/utils.js +++ b/packages/expo-router/build/loaders/utils.js @@ -11,18 +11,21 @@ const url_1 = require("../utils/url"); * getLoaderModulePath(`/about`) // `/_expo/loaders/about` * getLoaderModulePath(`/posts/1`) // `/_expo/loaders/posts/1` */ -function getLoaderModulePath(pathname) { - const urlPath = (0, url_1.parseUrlUsingCustomBase)(pathname).pathname; - const normalizedPath = urlPath === '/' ? '/' : urlPath.replace(/\/$/, ''); +function getLoaderModulePath(routePath) { + const { pathname, search } = (0, url_1.parseUrlUsingCustomBase)(routePath); + const normalizedPath = pathname === '/' ? '/' : pathname.replace(/\/$/, ''); const pathSegment = normalizedPath === '/' ? '/index' : normalizedPath; - return `/_expo/loaders${pathSegment}`; + return `/_expo/loaders${pathSegment}${search}`; } /** * Fetches and parses a loader module from the given route path. * This works in all environments including: - * 1. Development with Metro dev server (see `LoaderModuleMiddleware`) + * 1. Development with Metro dev server * 2. Production with static files (SSG) * 3. SSR environments + * + * @see import('packages/@expo/cli/src/start/server/metro/createServerRouteMiddleware.ts').createRouteHandlerMiddleware + * @see import('packages/expo-server/src/vendor/environment/common.ts').createEnvironment */ async function fetchLoaderModule(routePath) { const loaderPath = getLoaderModulePath(routePath); diff --git a/packages/expo-router/build/loaders/utils.js.map b/packages/expo-router/build/loaders/utils.js.map index 117e7d7fd90561..72cd349c25c863 100644 --- a/packages/expo-router/build/loaders/utils.js.map +++ b/packages/expo-router/build/loaders/utils.js.map @@ -1 +1 @@ -{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/loaders/utils.ts"],"names":[],"mappings":";;AAUA,kDAMC;AASD,8CAiBC;AA1CD,sCAAuD;AAEvD;;;;;;;GAOG;AACH,SAAgB,mBAAmB,CAAC,QAAgB;IAClD,MAAM,OAAO,GAAG,IAAA,6BAAuB,EAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;IAC3D,MAAM,cAAc,GAAG,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC1E,MAAM,WAAW,GAAG,cAAc,KAAK,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC;IAEvE,OAAO,iBAAiB,WAAW,EAAE,CAAC;AACxC,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,iBAAiB,CAAC,SAAiB;IACvD,MAAM,UAAU,GAAG,mBAAmB,CAAC,SAAS,CAAC,CAAC;IAElD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,UAAU,EAAE;QACvC,OAAO,EAAE;YACP,MAAM,EAAE,kBAAkB;SAC3B;KACF,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,gCAAgC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACrE,CAAC;IAED,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,gCAAgC,KAAK,EAAE,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC","sourcesContent":["import { parseUrlUsingCustomBase } from '../utils/url';\n\n/**\n * Convert a route's pathname to a loader module path.\n *\n * @example\n * getLoaderModulePath(`/`); // `/_expo/loaders/index`\n * getLoaderModulePath(`/about`) // `/_expo/loaders/about`\n * getLoaderModulePath(`/posts/1`) // `/_expo/loaders/posts/1`\n */\nexport function getLoaderModulePath(pathname: string): string {\n const urlPath = parseUrlUsingCustomBase(pathname).pathname;\n const normalizedPath = urlPath === '/' ? '/' : urlPath.replace(/\\/$/, '');\n const pathSegment = normalizedPath === '/' ? '/index' : normalizedPath;\n\n return `/_expo/loaders${pathSegment}`;\n}\n\n/**\n * Fetches and parses a loader module from the given route path.\n * This works in all environments including:\n * 1. Development with Metro dev server (see `LoaderModuleMiddleware`)\n * 2. Production with static files (SSG)\n * 3. SSR environments\n */\nexport async function fetchLoaderModule(routePath: string): Promise {\n const loaderPath = getLoaderModulePath(routePath);\n\n const response = await fetch(loaderPath, {\n headers: {\n Accept: 'application/json',\n },\n });\n if (!response.ok) {\n throw new Error(`Failed to fetch loader data: ${response.status}`);\n }\n\n try {\n return await response.json();\n } catch (error) {\n throw new Error(`Failed to parse loader data: ${error}`);\n }\n}\n"]} \ No newline at end of file +{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/loaders/utils.ts"],"names":[],"mappings":";;AAUA,kDAMC;AAYD,8CAiBC;AA7CD,sCAAuD;AAEvD;;;;;;;GAOG;AACH,SAAgB,mBAAmB,CAAC,SAAiB;IACnD,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAA,6BAAuB,EAAC,SAAS,CAAC,CAAC;IAChE,MAAM,cAAc,GAAG,QAAQ,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC5E,MAAM,WAAW,GAAG,cAAc,KAAK,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC;IAEvE,OAAO,iBAAiB,WAAW,GAAG,MAAM,EAAE,CAAC;AACjD,CAAC;AAED;;;;;;;;;GASG;AACI,KAAK,UAAU,iBAAiB,CAAC,SAAiB;IACvD,MAAM,UAAU,GAAG,mBAAmB,CAAC,SAAS,CAAC,CAAC;IAElD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,UAAU,EAAE;QACvC,OAAO,EAAE;YACP,MAAM,EAAE,kBAAkB;SAC3B;KACF,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,gCAAgC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACrE,CAAC;IAED,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,gCAAgC,KAAK,EAAE,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC","sourcesContent":["import { parseUrlUsingCustomBase } from '../utils/url';\n\n/**\n * Convert a route's pathname to a loader module path.\n *\n * @example\n * getLoaderModulePath(`/`); // `/_expo/loaders/index`\n * getLoaderModulePath(`/about`) // `/_expo/loaders/about`\n * getLoaderModulePath(`/posts/1`) // `/_expo/loaders/posts/1`\n */\nexport function getLoaderModulePath(routePath: string): string {\n const { pathname, search } = parseUrlUsingCustomBase(routePath);\n const normalizedPath = pathname === '/' ? '/' : pathname.replace(/\\/$/, '');\n const pathSegment = normalizedPath === '/' ? '/index' : normalizedPath;\n\n return `/_expo/loaders${pathSegment}${search}`;\n}\n\n/**\n * Fetches and parses a loader module from the given route path.\n * This works in all environments including:\n * 1. Development with Metro dev server\n * 2. Production with static files (SSG)\n * 3. SSR environments\n *\n * @see import('packages/@expo/cli/src/start/server/metro/createServerRouteMiddleware.ts').createRouteHandlerMiddleware\n * @see import('packages/expo-server/src/vendor/environment/common.ts').createEnvironment\n */\nexport async function fetchLoaderModule(routePath: string): Promise {\n const loaderPath = getLoaderModulePath(routePath);\n\n const response = await fetch(loaderPath, {\n headers: {\n Accept: 'application/json',\n },\n });\n if (!response.ok) {\n throw new Error(`Failed to fetch loader data: ${response.status}`);\n }\n\n try {\n return await response.json();\n } catch (error) {\n throw new Error(`Failed to parse loader data: ${error}`);\n }\n}\n"]} \ No newline at end of file diff --git a/packages/expo-router/build/native-tabs/NativeTabsView.d.ts.map b/packages/expo-router/build/native-tabs/NativeTabsView.d.ts.map index 674c484b2fb2a5..d5aa175225f02c 100644 --- a/packages/expo-router/build/native-tabs/NativeTabsView.d.ts.map +++ b/packages/expo-router/build/native-tabs/NativeTabsView.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"NativeTabsView.d.ts","sourceRoot":"","sources":["../../src/native-tabs/NativeTabsView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAoC,MAAM,OAAO,CAAC;AAgBzD,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,SAAS,CAAC;AASjB,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,qBAyHxD"} \ No newline at end of file +{"version":3,"file":"NativeTabsView.d.ts","sourceRoot":"","sources":["../../src/native-tabs/NativeTabsView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAoC,MAAM,OAAO,CAAC;AAWzD,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,SAAS,CAAC;AASjB,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,qBAyHxD"} \ No newline at end of file diff --git a/packages/expo-router/build/native-tabs/NativeTabsView.js b/packages/expo-router/build/native-tabs/NativeTabsView.js index 4fa56cf5ac8342..188c867f13d21a 100644 --- a/packages/expo-router/build/native-tabs/NativeTabsView.js +++ b/packages/expo-router/build/native-tabs/NativeTabsView.js @@ -82,7 +82,7 @@ function NativeTabsView(props) { indicatorColor: color_1.Color.android.dynamic.secondaryContainer, } : undefined; - return ( {children} - ); + ); } function Screen(props) { const { routeKey, name, options, isFocused, standardAppearance, scrollEdgeAppearance, badgeTextColor, contentRenderer, } = props; @@ -125,13 +125,13 @@ function Screen(props) { collapsable={false} style={{ flex: 1 }} edges={{ bottom: true }}> {content} ) : (content); - return ( + return ( {wrappedContent} - ); + ); } const supportedTabBarMinimizeBehaviorsSet = new Set(types_1.SUPPORTED_TAB_BAR_MINIMIZE_BEHAVIORS); const supportedTabBarItemLabelVisibilityModesSet = new Set(types_1.SUPPORTED_TAB_BAR_ITEM_LABEL_VISIBILITY_MODES); -function BottomTabsWrapper(props) { +function TabsHostWrapper(props) { let { tabBarMinimizeBehavior, tabBarItemLabelVisibilityMode, ...rest } = props; if (tabBarMinimizeBehavior && !supportedTabBarMinimizeBehaviorsSet.has(tabBarMinimizeBehavior)) { console.warn(`Unsupported minimizeBehavior: ${tabBarMinimizeBehavior}. Supported values are: ${types_1.SUPPORTED_TAB_BAR_MINIMIZE_BEHAVIORS.map((behavior) => `"${behavior}"`).join(', ')}`); @@ -142,6 +142,6 @@ function BottomTabsWrapper(props) { console.warn(`Unsupported labelVisibilityMode: ${tabBarItemLabelVisibilityMode}. Supported values are: ${types_1.SUPPORTED_TAB_BAR_ITEM_LABEL_VISIBILITY_MODES.map((mode) => `"${mode}"`).join(', ')}`); tabBarItemLabelVisibilityMode = undefined; } - return (); + return (); } //# sourceMappingURL=NativeTabsView.js.map \ No newline at end of file diff --git a/packages/expo-router/build/native-tabs/NativeTabsView.js.map b/packages/expo-router/build/native-tabs/NativeTabsView.js.map index b2f85b979488c6..32e5ae8c409d7c 100644 --- a/packages/expo-router/build/native-tabs/NativeTabsView.js.map +++ b/packages/expo-router/build/native-tabs/NativeTabsView.js.map @@ -1 +1 @@ -{"version":3,"file":"NativeTabsView.js","sourceRoot":"","sources":["../../src/native-tabs/NativeTabsView.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useTheme } from '@react-navigation/native';\nimport React, { useDeferredValue, useMemo } from 'react';\nimport { View, type ColorValue } from 'react-native';\nimport {\n BottomTabs,\n BottomTabsScreen,\n type BottomTabsProps,\n type BottomTabsScreenAppearance,\n} from 'react-native-screens';\nimport { SafeAreaView } from 'react-native-screens/experimental';\n\nimport {\n createScrollEdgeAppearanceFromOptions,\n createStandardAppearanceFromOptions,\n} from './appearance';\nimport { Color } from '../color';\nimport { NativeTabsBottomAccessory } from './common/elements';\nimport {\n SUPPORTED_TAB_BAR_ITEM_LABEL_VISIBILITY_MODES,\n SUPPORTED_TAB_BAR_MINIMIZE_BEHAVIORS,\n type NativeTabOptions,\n type NativeTabsViewProps,\n} from './types';\nimport {\n convertOptionsIconToRNScreensPropsIcon,\n convertOptionsIconToIOSPropsIcon,\n useAwaitedScreensIcon,\n} from './utils/icon';\nimport { getFirstChildOfType } from '../utils/children';\nimport { useBottomAccessoryFunctionFromBottomAccessories } from './utils/bottomAccessory';\n\nexport function NativeTabsView(props: NativeTabsViewProps) {\n const {\n minimizeBehavior,\n disableIndicator,\n focusedIndex,\n tabs,\n sidebarAdaptable,\n nonTriggerChildren,\n } = props;\n\n const deferredFocusedIndex = useDeferredValue(focusedIndex);\n // We need to check if the deferred index is not out of bounds\n // This can happen when the focused index is the last tab, and user removes that tab\n // In that case the deferred index will still point to the last tab, but after re-render\n // it will be out of bounds\n const inBoundsDeferredFocusedIndex =\n deferredFocusedIndex < tabs.length ? deferredFocusedIndex : focusedIndex;\n\n const appearances = tabs.map((tab) => ({\n standardAppearance: createStandardAppearanceFromOptions(tab.options),\n scrollEdgeAppearance: createScrollEdgeAppearanceFromOptions(tab.options),\n }));\n\n const options = tabs.map((tab) => tab.options);\n\n const bottomAccessory = useMemo(\n () => getFirstChildOfType(nonTriggerChildren, NativeTabsBottomAccessory),\n [nonTriggerChildren]\n );\n\n const bottomAccessoryFn = useBottomAccessoryFunctionFromBottomAccessories(bottomAccessory);\n\n const children = tabs.map((tab, index) => {\n const isFocused = index === inBoundsDeferredFocusedIndex;\n\n return (\n \n );\n });\n\n const currentTabAppearance = appearances[inBoundsDeferredFocusedIndex]?.standardAppearance;\n const tabBarControllerMode: BottomTabsProps['tabBarControllerMode'] = sidebarAdaptable\n ? 'tabSidebar'\n : sidebarAdaptable === false\n ? 'tabBar'\n : 'automatic';\n\n // Material Design 3 dynamic color defaults for Android\n const androidMaterialDefaults =\n process.env.EXPO_OS === 'android'\n ? {\n inactiveColor: Color.android.dynamic.onSurfaceVariant,\n activeIconColor: Color.android.dynamic.onSecondaryContainer,\n activeLabelColor: Color.android.dynamic.onSurface,\n backgroundColor: Color.android.dynamic.surfaceContainer,\n rippleColor: Color.android.dynamic.primary,\n indicatorColor: Color.android.dynamic.secondaryContainer,\n }\n : undefined;\n\n return (\n {\n props.onTabChange(tabKey);\n }}>\n {children}\n \n );\n}\n\nfunction Screen(props: {\n routeKey: string;\n name: string;\n isFocused: boolean;\n options: NativeTabOptions;\n standardAppearance: BottomTabsScreenAppearance;\n scrollEdgeAppearance: BottomTabsScreenAppearance;\n badgeTextColor: ColorValue | undefined;\n contentRenderer: () => React.ReactNode;\n}) {\n const {\n routeKey,\n name,\n options,\n isFocused,\n standardAppearance,\n scrollEdgeAppearance,\n badgeTextColor,\n contentRenderer,\n } = props;\n const title = options.title ?? name;\n\n // We need to await the icon, as VectorIcon will load asynchronously\n const icon = useAwaitedScreensIcon(options.icon);\n const selectedIcon = useAwaitedScreensIcon(options.selectedIcon);\n const { colors } = useTheme();\n\n const content = (\n \n {contentRenderer()}\n \n );\n const wrappedContent =\n process.env.EXPO_OS === 'android' && !options.disableAutomaticContentInsets ? (\n \n {content}\n \n ) : (\n content\n );\n\n return (\n \n {wrappedContent}\n \n );\n}\n\nconst supportedTabBarMinimizeBehaviorsSet = new Set(SUPPORTED_TAB_BAR_MINIMIZE_BEHAVIORS);\nconst supportedTabBarItemLabelVisibilityModesSet = new Set(\n SUPPORTED_TAB_BAR_ITEM_LABEL_VISIBILITY_MODES\n);\n\nfunction BottomTabsWrapper(props: BottomTabsProps) {\n let { tabBarMinimizeBehavior, tabBarItemLabelVisibilityMode, ...rest } = props;\n if (tabBarMinimizeBehavior && !supportedTabBarMinimizeBehaviorsSet.has(tabBarMinimizeBehavior)) {\n console.warn(\n `Unsupported minimizeBehavior: ${tabBarMinimizeBehavior}. Supported values are: ${SUPPORTED_TAB_BAR_MINIMIZE_BEHAVIORS.map((behavior) => `\"${behavior}\"`).join(', ')}`\n );\n tabBarMinimizeBehavior = undefined;\n }\n if (\n tabBarItemLabelVisibilityMode &&\n !supportedTabBarItemLabelVisibilityModesSet.has(tabBarItemLabelVisibilityMode)\n ) {\n console.warn(\n `Unsupported labelVisibilityMode: ${tabBarItemLabelVisibilityMode}. Supported values are: ${SUPPORTED_TAB_BAR_ITEM_LABEL_VISIBILITY_MODES.map((mode) => `\"${mode}\"`).join(', ')}`\n );\n tabBarItemLabelVisibilityMode = undefined;\n }\n\n return (\n \n );\n}\n"]} \ No newline at end of file +{"version":3,"file":"NativeTabsView.js","sourceRoot":"","sources":["../../src/native-tabs/NativeTabsView.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useTheme } from '@react-navigation/native';\nimport React, { useDeferredValue, useMemo } from 'react';\nimport { View, type ColorValue } from 'react-native';\nimport { Tabs, type TabsHostProps, type TabsScreenAppearance } from 'react-native-screens';\nimport { SafeAreaView } from 'react-native-screens/experimental';\n\nimport {\n createScrollEdgeAppearanceFromOptions,\n createStandardAppearanceFromOptions,\n} from './appearance';\nimport { Color } from '../color';\nimport { NativeTabsBottomAccessory } from './common/elements';\nimport {\n SUPPORTED_TAB_BAR_ITEM_LABEL_VISIBILITY_MODES,\n SUPPORTED_TAB_BAR_MINIMIZE_BEHAVIORS,\n type NativeTabOptions,\n type NativeTabsViewProps,\n} from './types';\nimport {\n convertOptionsIconToRNScreensPropsIcon,\n convertOptionsIconToIOSPropsIcon,\n useAwaitedScreensIcon,\n} from './utils/icon';\nimport { getFirstChildOfType } from '../utils/children';\nimport { useBottomAccessoryFunctionFromBottomAccessories } from './utils/bottomAccessory';\n\nexport function NativeTabsView(props: NativeTabsViewProps) {\n const {\n minimizeBehavior,\n disableIndicator,\n focusedIndex,\n tabs,\n sidebarAdaptable,\n nonTriggerChildren,\n } = props;\n\n const deferredFocusedIndex = useDeferredValue(focusedIndex);\n // We need to check if the deferred index is not out of bounds\n // This can happen when the focused index is the last tab, and user removes that tab\n // In that case the deferred index will still point to the last tab, but after re-render\n // it will be out of bounds\n const inBoundsDeferredFocusedIndex =\n deferredFocusedIndex < tabs.length ? deferredFocusedIndex : focusedIndex;\n\n const appearances = tabs.map((tab) => ({\n standardAppearance: createStandardAppearanceFromOptions(tab.options),\n scrollEdgeAppearance: createScrollEdgeAppearanceFromOptions(tab.options),\n }));\n\n const options = tabs.map((tab) => tab.options);\n\n const bottomAccessory = useMemo(\n () => getFirstChildOfType(nonTriggerChildren, NativeTabsBottomAccessory),\n [nonTriggerChildren]\n );\n\n const bottomAccessoryFn = useBottomAccessoryFunctionFromBottomAccessories(bottomAccessory);\n\n const children = tabs.map((tab, index) => {\n const isFocused = index === inBoundsDeferredFocusedIndex;\n\n return (\n \n );\n });\n\n const currentTabAppearance = appearances[inBoundsDeferredFocusedIndex]?.standardAppearance;\n const tabBarControllerMode: TabsHostProps['tabBarControllerMode'] = sidebarAdaptable\n ? 'tabSidebar'\n : sidebarAdaptable === false\n ? 'tabBar'\n : 'automatic';\n\n // Material Design 3 dynamic color defaults for Android\n const androidMaterialDefaults =\n process.env.EXPO_OS === 'android'\n ? {\n inactiveColor: Color.android.dynamic.onSurfaceVariant,\n activeIconColor: Color.android.dynamic.onSecondaryContainer,\n activeLabelColor: Color.android.dynamic.onSurface,\n backgroundColor: Color.android.dynamic.surfaceContainer,\n rippleColor: Color.android.dynamic.primary,\n indicatorColor: Color.android.dynamic.secondaryContainer,\n }\n : undefined;\n\n return (\n {\n props.onTabChange(tabKey);\n }}>\n {children}\n \n );\n}\n\nfunction Screen(props: {\n routeKey: string;\n name: string;\n isFocused: boolean;\n options: NativeTabOptions;\n standardAppearance: TabsScreenAppearance;\n scrollEdgeAppearance: TabsScreenAppearance;\n badgeTextColor: ColorValue | undefined;\n contentRenderer: () => React.ReactNode;\n}) {\n const {\n routeKey,\n name,\n options,\n isFocused,\n standardAppearance,\n scrollEdgeAppearance,\n badgeTextColor,\n contentRenderer,\n } = props;\n const title = options.title ?? name;\n\n // We need to await the icon, as VectorIcon will load asynchronously\n const icon = useAwaitedScreensIcon(options.icon);\n const selectedIcon = useAwaitedScreensIcon(options.selectedIcon);\n const { colors } = useTheme();\n\n const content = (\n \n {contentRenderer()}\n \n );\n const wrappedContent =\n process.env.EXPO_OS === 'android' && !options.disableAutomaticContentInsets ? (\n \n {content}\n \n ) : (\n content\n );\n\n return (\n \n {wrappedContent}\n \n );\n}\n\nconst supportedTabBarMinimizeBehaviorsSet = new Set(SUPPORTED_TAB_BAR_MINIMIZE_BEHAVIORS);\nconst supportedTabBarItemLabelVisibilityModesSet = new Set(\n SUPPORTED_TAB_BAR_ITEM_LABEL_VISIBILITY_MODES\n);\n\nfunction TabsHostWrapper(props: TabsHostProps) {\n let { tabBarMinimizeBehavior, tabBarItemLabelVisibilityMode, ...rest } = props;\n if (tabBarMinimizeBehavior && !supportedTabBarMinimizeBehaviorsSet.has(tabBarMinimizeBehavior)) {\n console.warn(\n `Unsupported minimizeBehavior: ${tabBarMinimizeBehavior}. Supported values are: ${SUPPORTED_TAB_BAR_MINIMIZE_BEHAVIORS.map((behavior) => `\"${behavior}\"`).join(', ')}`\n );\n tabBarMinimizeBehavior = undefined;\n }\n if (\n tabBarItemLabelVisibilityMode &&\n !supportedTabBarItemLabelVisibilityModesSet.has(tabBarItemLabelVisibilityMode)\n ) {\n console.warn(\n `Unsupported labelVisibilityMode: ${tabBarItemLabelVisibilityMode}. Supported values are: ${SUPPORTED_TAB_BAR_ITEM_LABEL_VISIBILITY_MODES.map((mode) => `\"${mode}\"`).join(', ')}`\n );\n tabBarItemLabelVisibilityMode = undefined;\n }\n\n return (\n \n );\n}\n"]} \ No newline at end of file diff --git a/packages/expo-router/build/native-tabs/appearance.d.ts b/packages/expo-router/build/native-tabs/appearance.d.ts index 8c05405e740136..6cf4b0f76dd47d 100644 --- a/packages/expo-router/build/native-tabs/appearance.d.ts +++ b/packages/expo-router/build/native-tabs/appearance.d.ts @@ -1,8 +1,8 @@ import type { ColorValue } from 'react-native'; -import type { BottomTabsScreenAppearance, BottomTabsScreenItemStateAppearance } from 'react-native-screens'; +import type { TabsScreenAppearance, TabsScreenItemStateAppearance } from 'react-native-screens'; import { type NativeTabOptions, type NativeTabsBlurEffect, type NativeTabsLabelStyle } from './types'; -export declare function createStandardAppearanceFromOptions(options: NativeTabOptions): BottomTabsScreenAppearance; -export declare function createScrollEdgeAppearanceFromOptions(options: NativeTabOptions): BottomTabsScreenAppearance; +export declare function createStandardAppearanceFromOptions(options: NativeTabOptions): TabsScreenAppearance; +export declare function createScrollEdgeAppearanceFromOptions(options: NativeTabOptions): TabsScreenAppearance; export interface AppearanceStyle extends NativeTabsLabelStyle { iconColor?: ColorValue; backgroundColor?: ColorValue | null; @@ -14,8 +14,8 @@ export interface AppearanceStyle extends NativeTabsLabelStyle { vertical?: number; }; } -export declare function appendSelectedStyleToAppearance(selectedStyle: AppearanceStyle, appearance: BottomTabsScreenAppearance): BottomTabsScreenAppearance; -export declare function appendStyleToAppearance(style: AppearanceStyle, appearance: BottomTabsScreenAppearance, states: ('selected' | 'focused' | 'disabled' | 'normal')[]): BottomTabsScreenAppearance; -export declare function convertStyleToAppearance(style: AppearanceStyle | undefined): BottomTabsScreenAppearance; -export declare function convertStyleToItemStateAppearance(style: AppearanceStyle | undefined): BottomTabsScreenItemStateAppearance; +export declare function appendSelectedStyleToAppearance(selectedStyle: AppearanceStyle, appearance: TabsScreenAppearance): TabsScreenAppearance; +export declare function appendStyleToAppearance(style: AppearanceStyle, appearance: TabsScreenAppearance, states: ('selected' | 'focused' | 'disabled' | 'normal')[]): TabsScreenAppearance; +export declare function convertStyleToAppearance(style: AppearanceStyle | undefined): TabsScreenAppearance; +export declare function convertStyleToItemStateAppearance(style: AppearanceStyle | undefined): TabsScreenItemStateAppearance; //# sourceMappingURL=appearance.d.ts.map \ No newline at end of file diff --git a/packages/expo-router/build/native-tabs/appearance.d.ts.map b/packages/expo-router/build/native-tabs/appearance.d.ts.map index c137576c37e784..74b224e727d4c3 100644 --- a/packages/expo-router/build/native-tabs/appearance.d.ts.map +++ b/packages/expo-router/build/native-tabs/appearance.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"appearance.d.ts","sourceRoot":"","sources":["../../src/native-tabs/appearance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EACV,0BAA0B,EAE1B,mCAAmC,EACpC,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EAC1B,MAAM,SAAS,CAAC;AAKjB,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,gBAAgB,GACxB,0BAA0B,CAgC5B;AAED,wBAAgB,qCAAqC,CACnD,OAAO,EAAE,gBAAgB,GACxB,0BAA0B,CAgC5B;AAED,MAAM,WAAW,eAAgB,SAAQ,oBAAoB;IAC3D,SAAS,CAAC,EAAE,UAAU,CAAC;IACvB,eAAe,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACpC,UAAU,CAAC,EAAE,oBAAoB,CAAC;IAClC,oBAAoB,CAAC,EAAE,UAAU,CAAC;IAClC,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,uBAAuB,CAAC,EAAE;QACxB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,wBAAgB,+BAA+B,CAC7C,aAAa,EAAE,eAAe,EAC9B,UAAU,EAAE,0BAA0B,GACrC,0BAA0B,CAE5B;AASD,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,eAAe,EACtB,UAAU,EAAE,0BAA0B,EACtC,MAAM,EAAE,CAAC,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC,EAAE,GACzD,0BAA0B,CA8B5B;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,eAAe,GAAG,SAAS,GACjC,0BAA0B,CAmB5B;AAED,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,eAAe,GAAG,SAAS,GACjC,mCAAmC,CAsBrC"} \ No newline at end of file +{"version":3,"file":"appearance.d.ts","sourceRoot":"","sources":["../../src/native-tabs/appearance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EACV,oBAAoB,EAEpB,6BAA6B,EAC9B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EAC1B,MAAM,SAAS,CAAC;AAKjB,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,gBAAgB,GACxB,oBAAoB,CAgCtB;AAED,wBAAgB,qCAAqC,CACnD,OAAO,EAAE,gBAAgB,GACxB,oBAAoB,CAgCtB;AAED,MAAM,WAAW,eAAgB,SAAQ,oBAAoB;IAC3D,SAAS,CAAC,EAAE,UAAU,CAAC;IACvB,eAAe,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACpC,UAAU,CAAC,EAAE,oBAAoB,CAAC;IAClC,oBAAoB,CAAC,EAAE,UAAU,CAAC;IAClC,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,uBAAuB,CAAC,EAAE;QACxB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,wBAAgB,+BAA+B,CAC7C,aAAa,EAAE,eAAe,EAC9B,UAAU,EAAE,oBAAoB,GAC/B,oBAAoB,CAEtB;AASD,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,eAAe,EACtB,UAAU,EAAE,oBAAoB,EAChC,MAAM,EAAE,CAAC,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC,EAAE,GACzD,oBAAoB,CA8BtB;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,eAAe,GAAG,SAAS,GAAG,oBAAoB,CAmBjG;AAED,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,eAAe,GAAG,SAAS,GACjC,6BAA6B,CAsB/B"} \ No newline at end of file diff --git a/packages/expo-router/build/native-tabs/appearance.js.map b/packages/expo-router/build/native-tabs/appearance.js.map index 9e5c9bf4fc4f8c..b08a481add7285 100644 --- a/packages/expo-router/build/native-tabs/appearance.js.map +++ b/packages/expo-router/build/native-tabs/appearance.js.map @@ -1 +1 @@ -{"version":3,"file":"appearance.js","sourceRoot":"","sources":["../../src/native-tabs/appearance.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ColorValue } from 'react-native';\nimport type {\n BottomTabsScreenAppearance,\n BottomTabsScreenItemAppearance,\n BottomTabsScreenItemStateAppearance,\n} from 'react-native-screens';\n\nimport {\n SUPPORTED_BLUR_EFFECTS,\n type NativeTabOptions,\n type NativeTabsBlurEffect,\n type NativeTabsLabelStyle,\n} from './types';\nimport { convertFontWeightToStringFontWeight } from '../utils/style';\n\nconst supportedBlurEffectsSet = new Set(SUPPORTED_BLUR_EFFECTS);\n\nexport function createStandardAppearanceFromOptions(\n options: NativeTabOptions\n): BottomTabsScreenAppearance {\n let blurEffect = options.blurEffect;\n if (blurEffect && !supportedBlurEffectsSet.has(blurEffect)) {\n console.warn(\n `Unsupported blurEffect: ${blurEffect}. Supported values are: ${SUPPORTED_BLUR_EFFECTS.map(\n (effect) => `\"${effect}\"`\n ).join(', ')}`\n );\n blurEffect = undefined;\n }\n const appearance = appendStyleToAppearance(\n {\n ...options.labelStyle,\n iconColor: options.iconColor,\n backgroundColor: options.backgroundColor,\n blurEffect,\n badgeBackgroundColor: options.badgeBackgroundColor,\n titlePositionAdjustment: options.titlePositionAdjustment,\n shadowColor: options.shadowColor,\n },\n {},\n ['normal', 'focused', 'selected']\n );\n return appendSelectedStyleToAppearance(\n {\n ...(options.selectedLabelStyle ?? {}),\n iconColor: options.selectedIconColor,\n badgeBackgroundColor: options.selectedBadgeBackgroundColor,\n titlePositionAdjustment: options.selectedTitlePositionAdjustment,\n },\n appearance\n );\n}\n\nexport function createScrollEdgeAppearanceFromOptions(\n options: NativeTabOptions\n): BottomTabsScreenAppearance {\n let blurEffect = options.disableTransparentOnScrollEdge ? options.blurEffect : 'none';\n if (blurEffect && !supportedBlurEffectsSet.has(blurEffect)) {\n console.warn(\n `Unsupported blurEffect: ${blurEffect}. Supported values are: ${SUPPORTED_BLUR_EFFECTS.map(\n (effect) => `\"${effect}\"`\n ).join(', ')}`\n );\n blurEffect = undefined;\n }\n const appearance = appendStyleToAppearance(\n {\n ...options.labelStyle,\n iconColor: options.iconColor,\n blurEffect,\n backgroundColor: options.disableTransparentOnScrollEdge ? options.backgroundColor : null,\n shadowColor: options.disableTransparentOnScrollEdge ? options.shadowColor : 'transparent',\n badgeBackgroundColor: options.badgeBackgroundColor,\n titlePositionAdjustment: options.titlePositionAdjustment,\n },\n {},\n ['normal', 'focused', 'selected']\n );\n return appendSelectedStyleToAppearance(\n {\n ...(options.selectedLabelStyle ?? {}),\n iconColor: options.selectedIconColor,\n badgeBackgroundColor: options.selectedBadgeBackgroundColor,\n titlePositionAdjustment: options.selectedTitlePositionAdjustment,\n },\n appearance\n );\n}\n\nexport interface AppearanceStyle extends NativeTabsLabelStyle {\n iconColor?: ColorValue;\n backgroundColor?: ColorValue | null;\n blurEffect?: NativeTabsBlurEffect;\n badgeBackgroundColor?: ColorValue;\n shadowColor?: ColorValue;\n titlePositionAdjustment?: {\n horizontal?: number;\n vertical?: number;\n };\n}\n\nexport function appendSelectedStyleToAppearance(\n selectedStyle: AppearanceStyle,\n appearance: BottomTabsScreenAppearance\n): BottomTabsScreenAppearance {\n return appendStyleToAppearance(selectedStyle, appearance, ['selected', 'focused']);\n}\n\nconst EMPTY_APPEARANCE_ITEM: BottomTabsScreenItemAppearance = {\n normal: {},\n selected: {},\n focused: {},\n disabled: {},\n};\n\nexport function appendStyleToAppearance(\n style: AppearanceStyle,\n appearance: BottomTabsScreenAppearance,\n states: ('selected' | 'focused' | 'disabled' | 'normal')[]\n): BottomTabsScreenAppearance {\n const baseItemAppearance =\n appearance.stacked || appearance.inline || appearance.compactInline || {};\n\n const styleAppearance = convertStyleToAppearance(style);\n const newAppearances = states.map((state) => ({\n key: state,\n appearance: {\n ...baseItemAppearance.normal,\n ...baseItemAppearance[state],\n ...styleAppearance.stacked?.normal,\n },\n }));\n\n const itemAppearance: BottomTabsScreenItemAppearance = {\n ...EMPTY_APPEARANCE_ITEM,\n ...baseItemAppearance,\n ...Object.fromEntries(newAppearances.map(({ key, appearance }) => [key, appearance])),\n };\n return {\n stacked: itemAppearance,\n inline: itemAppearance,\n compactInline: itemAppearance,\n tabBarBackgroundColor:\n style.backgroundColor === null\n ? undefined\n : (style.backgroundColor ?? appearance.tabBarBackgroundColor),\n tabBarBlurEffect: styleAppearance.tabBarBlurEffect ?? appearance.tabBarBlurEffect,\n tabBarShadowColor: styleAppearance.tabBarShadowColor ?? appearance.tabBarShadowColor,\n };\n}\n\nexport function convertStyleToAppearance(\n style: AppearanceStyle | undefined\n): BottomTabsScreenAppearance {\n if (!style) {\n return {};\n }\n const stateAppearance = convertStyleToItemStateAppearance(style);\n const itemAppearance: BottomTabsScreenItemAppearance = {\n normal: stateAppearance,\n selected: stateAppearance,\n focused: stateAppearance,\n disabled: {},\n };\n return {\n inline: itemAppearance,\n stacked: itemAppearance,\n compactInline: itemAppearance,\n tabBarBackgroundColor: style?.backgroundColor ?? undefined,\n tabBarBlurEffect: style?.blurEffect,\n tabBarShadowColor: style?.shadowColor,\n };\n}\n\nexport function convertStyleToItemStateAppearance(\n style: AppearanceStyle | undefined\n): BottomTabsScreenItemStateAppearance {\n if (!style) {\n return {};\n }\n const stateAppearance: BottomTabsScreenItemStateAppearance = {\n tabBarItemBadgeBackgroundColor: style.badgeBackgroundColor,\n tabBarItemTitlePositionAdjustment: style.titlePositionAdjustment,\n tabBarItemIconColor: style.iconColor,\n tabBarItemTitleFontFamily: style.fontFamily,\n tabBarItemTitleFontSize: style.fontSize,\n tabBarItemTitleFontWeight: convertFontWeightToStringFontWeight(style.fontWeight),\n tabBarItemTitleFontStyle: style.fontStyle,\n tabBarItemTitleFontColor: style.color,\n };\n\n (Object.keys(stateAppearance) as (keyof BottomTabsScreenItemStateAppearance)[]).forEach((key) => {\n if (stateAppearance[key] === undefined) {\n delete stateAppearance[key];\n }\n });\n\n return stateAppearance;\n}\n"]} \ No newline at end of file +{"version":3,"file":"appearance.js","sourceRoot":"","sources":["../../src/native-tabs/appearance.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ColorValue } from 'react-native';\nimport type {\n TabsScreenAppearance,\n TabsScreenItemAppearance,\n TabsScreenItemStateAppearance,\n} from 'react-native-screens';\n\nimport {\n SUPPORTED_BLUR_EFFECTS,\n type NativeTabOptions,\n type NativeTabsBlurEffect,\n type NativeTabsLabelStyle,\n} from './types';\nimport { convertFontWeightToStringFontWeight } from '../utils/style';\n\nconst supportedBlurEffectsSet = new Set(SUPPORTED_BLUR_EFFECTS);\n\nexport function createStandardAppearanceFromOptions(\n options: NativeTabOptions\n): TabsScreenAppearance {\n let blurEffect = options.blurEffect;\n if (blurEffect && !supportedBlurEffectsSet.has(blurEffect)) {\n console.warn(\n `Unsupported blurEffect: ${blurEffect}. Supported values are: ${SUPPORTED_BLUR_EFFECTS.map(\n (effect) => `\"${effect}\"`\n ).join(', ')}`\n );\n blurEffect = undefined;\n }\n const appearance = appendStyleToAppearance(\n {\n ...options.labelStyle,\n iconColor: options.iconColor,\n backgroundColor: options.backgroundColor,\n blurEffect,\n badgeBackgroundColor: options.badgeBackgroundColor,\n titlePositionAdjustment: options.titlePositionAdjustment,\n shadowColor: options.shadowColor,\n },\n {},\n ['normal', 'focused', 'selected']\n );\n return appendSelectedStyleToAppearance(\n {\n ...(options.selectedLabelStyle ?? {}),\n iconColor: options.selectedIconColor,\n badgeBackgroundColor: options.selectedBadgeBackgroundColor,\n titlePositionAdjustment: options.selectedTitlePositionAdjustment,\n },\n appearance\n );\n}\n\nexport function createScrollEdgeAppearanceFromOptions(\n options: NativeTabOptions\n): TabsScreenAppearance {\n let blurEffect = options.disableTransparentOnScrollEdge ? options.blurEffect : 'none';\n if (blurEffect && !supportedBlurEffectsSet.has(blurEffect)) {\n console.warn(\n `Unsupported blurEffect: ${blurEffect}. Supported values are: ${SUPPORTED_BLUR_EFFECTS.map(\n (effect) => `\"${effect}\"`\n ).join(', ')}`\n );\n blurEffect = undefined;\n }\n const appearance = appendStyleToAppearance(\n {\n ...options.labelStyle,\n iconColor: options.iconColor,\n blurEffect,\n backgroundColor: options.disableTransparentOnScrollEdge ? options.backgroundColor : null,\n shadowColor: options.disableTransparentOnScrollEdge ? options.shadowColor : 'transparent',\n badgeBackgroundColor: options.badgeBackgroundColor,\n titlePositionAdjustment: options.titlePositionAdjustment,\n },\n {},\n ['normal', 'focused', 'selected']\n );\n return appendSelectedStyleToAppearance(\n {\n ...(options.selectedLabelStyle ?? {}),\n iconColor: options.selectedIconColor,\n badgeBackgroundColor: options.selectedBadgeBackgroundColor,\n titlePositionAdjustment: options.selectedTitlePositionAdjustment,\n },\n appearance\n );\n}\n\nexport interface AppearanceStyle extends NativeTabsLabelStyle {\n iconColor?: ColorValue;\n backgroundColor?: ColorValue | null;\n blurEffect?: NativeTabsBlurEffect;\n badgeBackgroundColor?: ColorValue;\n shadowColor?: ColorValue;\n titlePositionAdjustment?: {\n horizontal?: number;\n vertical?: number;\n };\n}\n\nexport function appendSelectedStyleToAppearance(\n selectedStyle: AppearanceStyle,\n appearance: TabsScreenAppearance\n): TabsScreenAppearance {\n return appendStyleToAppearance(selectedStyle, appearance, ['selected', 'focused']);\n}\n\nconst EMPTY_APPEARANCE_ITEM: TabsScreenItemAppearance = {\n normal: {},\n selected: {},\n focused: {},\n disabled: {},\n};\n\nexport function appendStyleToAppearance(\n style: AppearanceStyle,\n appearance: TabsScreenAppearance,\n states: ('selected' | 'focused' | 'disabled' | 'normal')[]\n): TabsScreenAppearance {\n const baseItemAppearance =\n appearance.stacked || appearance.inline || appearance.compactInline || {};\n\n const styleAppearance = convertStyleToAppearance(style);\n const newAppearances = states.map((state) => ({\n key: state,\n appearance: {\n ...baseItemAppearance.normal,\n ...baseItemAppearance[state],\n ...styleAppearance.stacked?.normal,\n },\n }));\n\n const itemAppearance: TabsScreenItemAppearance = {\n ...EMPTY_APPEARANCE_ITEM,\n ...baseItemAppearance,\n ...Object.fromEntries(newAppearances.map(({ key, appearance }) => [key, appearance])),\n };\n return {\n stacked: itemAppearance,\n inline: itemAppearance,\n compactInline: itemAppearance,\n tabBarBackgroundColor:\n style.backgroundColor === null\n ? undefined\n : (style.backgroundColor ?? appearance.tabBarBackgroundColor),\n tabBarBlurEffect: styleAppearance.tabBarBlurEffect ?? appearance.tabBarBlurEffect,\n tabBarShadowColor: styleAppearance.tabBarShadowColor ?? appearance.tabBarShadowColor,\n };\n}\n\nexport function convertStyleToAppearance(style: AppearanceStyle | undefined): TabsScreenAppearance {\n if (!style) {\n return {};\n }\n const stateAppearance = convertStyleToItemStateAppearance(style);\n const itemAppearance: TabsScreenItemAppearance = {\n normal: stateAppearance,\n selected: stateAppearance,\n focused: stateAppearance,\n disabled: {},\n };\n return {\n inline: itemAppearance,\n stacked: itemAppearance,\n compactInline: itemAppearance,\n tabBarBackgroundColor: style?.backgroundColor ?? undefined,\n tabBarBlurEffect: style?.blurEffect,\n tabBarShadowColor: style?.shadowColor,\n };\n}\n\nexport function convertStyleToItemStateAppearance(\n style: AppearanceStyle | undefined\n): TabsScreenItemStateAppearance {\n if (!style) {\n return {};\n }\n const stateAppearance: TabsScreenItemStateAppearance = {\n tabBarItemBadgeBackgroundColor: style.badgeBackgroundColor,\n tabBarItemTitlePositionAdjustment: style.titlePositionAdjustment,\n tabBarItemIconColor: style.iconColor,\n tabBarItemTitleFontFamily: style.fontFamily,\n tabBarItemTitleFontSize: style.fontSize,\n tabBarItemTitleFontWeight: convertFontWeightToStringFontWeight(style.fontWeight),\n tabBarItemTitleFontStyle: style.fontStyle,\n tabBarItemTitleFontColor: style.color,\n };\n\n (Object.keys(stateAppearance) as (keyof TabsScreenItemStateAppearance)[]).forEach((key) => {\n if (stateAppearance[key] === undefined) {\n delete stateAppearance[key];\n }\n });\n\n return stateAppearance;\n}\n"]} \ No newline at end of file diff --git a/packages/expo-router/build/native-tabs/types.d.ts b/packages/expo-router/build/native-tabs/types.d.ts index 90172c2fe6f0ca..5d364b9836dce8 100644 --- a/packages/expo-router/build/native-tabs/types.d.ts +++ b/packages/expo-router/build/native-tabs/types.d.ts @@ -1,9 +1,9 @@ import type { DefaultRouterOptions } from '@react-navigation/native'; import type { PropsWithChildren } from 'react'; import type { ColorValue, ImageSourcePropType, StyleProp, TextStyle, ViewStyle } from 'react-native'; -import type { BottomTabsScreenProps } from 'react-native-screens'; +import type { TabsScreenProps } from 'react-native-screens'; import type { SFSymbol } from 'sf-symbols-typescript'; -export type NativeScreenProps = Partial>; +export type NativeScreenProps = Partial>; export interface NativeTabOptions extends DefaultRouterOptions { icon?: SymbolOrImageSource; selectedIcon?: SymbolOrImageSource; @@ -31,7 +31,7 @@ export interface NativeTabOptions extends DefaultRouterOptions { }; indicatorColor?: ColorValue; hidden?: boolean; - specialEffects?: BottomTabsScreenProps['specialEffects']; + specialEffects?: TabsScreenProps['specialEffects']; nativeProps?: NativeScreenProps; disableAutomaticContentInsets?: boolean; contentStyle?: Pick; diff --git a/packages/expo-router/build/native-tabs/types.d.ts.map b/packages/expo-router/build/native-tabs/types.d.ts.map index f61a7a04301267..d99ccef039f50e 100644 --- a/packages/expo-router/build/native-tabs/types.d.ts.map +++ b/packages/expo-router/build/native-tabs/types.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/native-tabs/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/native-tabs/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/expo-router/build/native-tabs/types.js.map b/packages/expo-router/build/native-tabs/types.js.map index 8f7b7b37200e76..f8922244179359 100644 --- a/packages/expo-router/build/native-tabs/types.js.map +++ b/packages/expo-router/build/native-tabs/types.js.map @@ -1 +1 @@ -{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/native-tabs/types.ts"],"names":[],"mappings":";;;AAsGa,QAAA,sBAAsB,GAAG;IACpC,MAAM;IACN,eAAe;IACf,YAAY;IACZ,OAAO;IACP,MAAM;IACN,SAAS;IACT,WAAW;IACX,yBAAyB;IACzB,oBAAoB;IACpB,gBAAgB;IAChB,qBAAqB;IACrB,sBAAsB;IACtB,8BAA8B;IAC9B,yBAAyB;IACzB,qBAAqB;IACrB,0BAA0B;IAC1B,2BAA2B;IAC3B,6BAA6B;IAC7B,wBAAwB;IACxB,oBAAoB;IACpB,yBAAyB;IACzB,0BAA0B;CAClB,CAAC;AA2KE,QAAA,6CAA6C,GAAG;IAC3D,MAAM;IACN,UAAU;IACV,SAAS;IACT,WAAW;CACH,CAAC;AAUE,QAAA,oCAAoC,GAAG;IAClD,WAAW;IACX,OAAO;IACP,cAAc;IACd,YAAY;CACJ,CAAC;AA8FX,MAAM,4BAA4B,GAAG;IACnC,WAAW;IACX,UAAU;IACV,WAAW;IACX,WAAW;IACX,UAAU;IACV,SAAS;IACT,MAAM;IACN,YAAY;IACZ,YAAY;IACZ,SAAS;IACT,QAAQ;IACR,UAAU;CACF,CAAC","sourcesContent":["import type { DefaultRouterOptions } from '@react-navigation/native';\nimport type { PropsWithChildren } from 'react';\nimport type {\n ColorValue,\n ImageSourcePropType,\n StyleProp,\n TextStyle,\n ViewStyle,\n} from 'react-native';\nimport type { BottomTabsScreenProps } from 'react-native-screens';\nimport type { SFSymbol } from 'sf-symbols-typescript';\n\nexport type NativeScreenProps = Partial>;\n\nexport interface NativeTabOptions extends DefaultRouterOptions {\n icon?: SymbolOrImageSource;\n selectedIcon?: SymbolOrImageSource;\n title?: string;\n badgeValue?: string;\n selectedLabelStyle?: NativeTabsLabelStyle;\n labelStyle?: NativeTabsLabelStyle;\n role?: NativeTabsTabBarItemRole;\n selectedIconColor?: ColorValue;\n selectedBadgeBackgroundColor?: ColorValue;\n badgeBackgroundColor?: ColorValue;\n badgeTextColor?: ColorValue;\n backgroundColor?: ColorValue;\n blurEffect?: NativeTabsBlurEffect;\n shadowColor?: ColorValue;\n iconColor?: ColorValue;\n disableTransparentOnScrollEdge?: boolean;\n titlePositionAdjustment?: {\n horizontal?: number;\n vertical?: number;\n };\n selectedTitlePositionAdjustment?: {\n horizontal?: number;\n vertical?: number;\n };\n indicatorColor?: ColorValue;\n hidden?: boolean;\n specialEffects?: BottomTabsScreenProps['specialEffects'];\n nativeProps?: NativeScreenProps;\n disableAutomaticContentInsets?: boolean;\n contentStyle?: Pick<\n ViewStyle,\n | 'backgroundColor'\n | 'experimental_backgroundImage'\n | 'padding'\n | 'paddingTop'\n | 'paddingBottom'\n | 'paddingLeft'\n | 'paddingRight'\n | 'paddingBlock'\n | 'paddingBlockEnd'\n | 'paddingBlockStart'\n | 'paddingInline'\n | 'paddingInlineEnd'\n | 'paddingInlineStart'\n | 'paddingEnd'\n | 'paddingHorizontal'\n | 'paddingVertical'\n | 'paddingStart'\n | 'alignContent'\n | 'alignItems'\n | 'justifyContent'\n | 'flexDirection'\n | 'gap'\n >;\n}\n\nexport type SymbolOrImageSource =\n | {\n /**\n * The name of the SF Symbol to use as an icon.\n * @platform iOS\n */\n sf?: SFSymbol;\n /**\n * The name of the drawable resource to use as an icon.\n * @platform android\n */\n drawable?: string;\n }\n | {\n /**\n * The image source to use as an icon.\n */\n src?: ImageSourcePropType | Promise;\n /**\n * Controls how the icon is rendered on iOS.\n * @platform ios\n * @default 'template'\n */\n renderingMode?: 'template' | 'original';\n };\n\nexport type NativeTabsLabelStyle = Pick<\n TextStyle,\n 'fontFamily' | 'fontSize' | 'fontStyle' | 'fontWeight' | 'color'\n>;\n\nexport const SUPPORTED_BLUR_EFFECTS = [\n 'none',\n 'systemDefault',\n 'extraLight',\n 'light',\n 'dark',\n 'regular',\n 'prominent',\n 'systemUltraThinMaterial',\n 'systemThinMaterial',\n 'systemMaterial',\n 'systemThickMaterial',\n 'systemChromeMaterial',\n 'systemUltraThinMaterialLight',\n 'systemThinMaterialLight',\n 'systemMaterialLight',\n 'systemThickMaterialLight',\n 'systemChromeMaterialLight',\n 'systemUltraThinMaterialDark',\n 'systemThinMaterialDark',\n 'systemMaterialDark',\n 'systemThickMaterialDark',\n 'systemChromeMaterialDark',\n] as const;\n\n/**\n * @see [Apple documentation](https://developer.apple.com/documentation/uikit/uiblureffect/style)\n */\nexport type NativeTabsBlurEffect = (typeof SUPPORTED_BLUR_EFFECTS)[number];\n\nexport interface NativeTabsProps extends PropsWithChildren {\n // #region common props\n /**\n * The style of the every tab label in the tab bar.\n */\n labelStyle?:\n | StyleProp\n | {\n default?: StyleProp;\n selected?: StyleProp;\n };\n /**\n * The color of every tab icon in the tab bar.\n */\n iconColor?: ColorValue | { default?: ColorValue; selected?: ColorValue };\n /**\n * The tint color of the tab icon.\n *\n * Can be overridden by icon color and label color for each tab individually.\n */\n tintColor?: ColorValue;\n /**\n * The background color of the tab bar.\n */\n backgroundColor?: ColorValue;\n /**\n * The background color of every badge in the tab bar.\n */\n badgeBackgroundColor?: ColorValue;\n /**\n * When set to `true`, hides the tab bar.\n *\n * @default false\n */\n hidden?: boolean;\n // #endregion common props\n // #region iOS props\n /**\n * Specifies the minimize behavior for the tab bar.\n *\n * Available starting from iOS 26.\n *\n * The following values are currently supported:\n *\n * - `automatic` - resolves to the system default minimize behavior\n * - `never` - the tab bar does not minimize\n * - `onScrollDown` - the tab bar minimizes when scrolling down and\n * expands when scrolling back up\n * - `onScrollUp` - the tab bar minimizes when scrolling up and expands\n * when scrolling back down\n *\n * @see The supported values correspond to the official [Apple documentation](https://developer.apple.com/documentation/uikit/uitabbarcontroller/minimizebehavior).\n *\n * @default automatic\n *\n * @platform iOS 26+\n */\n minimizeBehavior?: NativeTabsTabBarMinimizeBehavior;\n /**\n * The blur effect applied to the tab bar.\n *\n * @platform iOS\n */\n blurEffect?: NativeTabsBlurEffect;\n /**\n * The color of the shadow.\n *\n * @see [Apple documentation](https://developer.apple.com/documentation/uikit/uibarappearance/shadowcolor)\n *\n * @platform iOS\n */\n shadowColor?: ColorValue;\n /**\n * @see [Apple documentation](https://developer.apple.com/documentation/uikit/uitabbaritem/titlepositionadjustment)\n *\n * @platform iOS\n */\n titlePositionAdjustment?: {\n horizontal?: number;\n vertical?: number;\n };\n /**\n * When set to `true`, the tab bar will not become transparent when scrolled to the edge.\n *\n * @platform iOS\n */\n disableTransparentOnScrollEdge?: boolean;\n /**\n * When set to `true`, enables the sidebarAdaptable tab bar style on iPadOS and macOS. This prop has no effect on iPhone.\n *\n * @platform iOS 18+\n */\n sidebarAdaptable?: boolean;\n // #endregion iOS props\n // #region android props\n /**\n * Disables the active indicator for the tab bar.\n *\n * @platform android\n */\n disableIndicator?: boolean;\n /**\n * The behavior when navigating back with the back button.\n *\n * @platform android\n */\n backBehavior?: 'none' | 'initialRoute' | 'history';\n /**\n * The visibility mode of the tab item label.\n *\n * @see [Material Components documentation](https://github.com/material-components/material-components-android/blob/master/docs/components/BottomNavigation.md#making-navigation-bar-accessible)\n *\n * @platform android\n */\n labelVisibilityMode?: NativeTabsTabBarItemLabelVisibilityMode;\n /**\n * The color of the ripple effect when the tab is pressed.\n *\n * @platform android\n */\n rippleColor?: ColorValue;\n /**\n * The color of the tab indicator.\n *\n * @platform android\n * @platform web\n */\n indicatorColor?: ColorValue;\n /**\n * The color of the badge text.\n *\n * @platform android\n * @platform web\n */\n badgeTextColor?: ColorValue;\n // #endregion android props\n}\n\nexport interface InternalNativeTabsProps extends NativeTabsProps {\n nonTriggerChildren?: React.ReactNode;\n}\nexport interface NativeTabsViewProps\n extends Omit<\n InternalNativeTabsProps,\n | 'labelStyle'\n | 'iconColor'\n | 'backgroundColor'\n | 'badgeBackgroundColor'\n | 'blurEffect'\n | 'indicatorColor'\n | 'badgeTextColor'\n > {\n focusedIndex: number;\n tabs: NativeTabsViewTabItem[];\n onTabChange: (tabKey: string) => void;\n}\n\nexport interface NativeTabsViewTabItem {\n options: NativeTabOptions;\n routeKey: string;\n name: string;\n contentRenderer: () => React.ReactNode;\n}\n\nexport const SUPPORTED_TAB_BAR_ITEM_LABEL_VISIBILITY_MODES = [\n 'auto',\n 'selected',\n 'labeled',\n 'unlabeled',\n] as const;\n\n/**\n * @see [Material Components documentation](https://github.com/material-components/material-components-android/blob/master/docs/components/BottomNavigation.md#making-navigation-bar-accessible)\n *\n * @platform android\n */\nexport type NativeTabsTabBarItemLabelVisibilityMode =\n (typeof SUPPORTED_TAB_BAR_ITEM_LABEL_VISIBILITY_MODES)[number];\n\nexport const SUPPORTED_TAB_BAR_MINIMIZE_BEHAVIORS = [\n 'automatic',\n 'never',\n 'onScrollDown',\n 'onScrollUp',\n] as const;\n\n/**\n * @see [Apple documentation](https://developer.apple.com/documentation/uikit/uitabbarcontroller/minimizebehavior)\n *\n * @platform iOS 26\n */\nexport type NativeTabsTabBarMinimizeBehavior =\n (typeof SUPPORTED_TAB_BAR_MINIMIZE_BEHAVIORS)[number];\n\nexport interface NativeTabTriggerProps {\n /**\n * The name of the route.\n *\n * This is required when used inside a Layout component.\n *\n * When used in a route it has no effect.\n */\n name?: string;\n /**\n * If true, the tab will be hidden from the tab bar.\n *\n * > **Note**: Marking a tab as `hidden` means it cannot be navigated to in any way.\n */\n hidden?: boolean;\n /**\n * Props passed to the underlying native tab screen implementation.\n * Use this to configure props not directly exposed by Expo Router, but available in `react-native-screens`.\n *\n * > **Note**: This will override any other props set by Expo Router and may lead to unexpected behavior.\n *\n * > **Note**: This is an unstable API and may change or be removed in minor versions.\n *\n * @platform android\n * @platform iOS\n */\n unstable_nativeProps?: NativeScreenProps;\n /**\n * If true, the tab will not pop stack to the root when selected again.\n *\n * @default false\n * @platform iOS\n */\n disablePopToTop?: boolean;\n /**\n * If true, the tab will not scroll to the top when selected again.\n * @default false\n *\n * @platform iOS\n */\n disableScrollToTop?: boolean;\n /**\n * The children of the trigger.\n *\n * Use `Icon`, `Label`, and `Badge` components to customize the tab.\n */\n children?: React.ReactNode;\n /**\n * System-provided tab bar item with predefined icon and title\n *\n * Uses Apple's built-in tab bar items (e.g., bookmarks, contacts, downloads) with\n * standard iOS styling and localized titles. Custom `icon` or `selectedIcon`\n * properties will override the system icon, but the system-defined title cannot\n * be customized.\n *\n * @see The supported values correspond to the official [Apple documentation](https://developer.apple.com/documentation/uikit/uitabbaritem/systemitem).\n * @platform ios\n */\n role?: NativeTabsTabBarItemRole;\n /**\n * The default behavior differs between iOS and Android.\n *\n * On **Android**, the content of a native tabs screen is automatically wrapped in a `SafeAreaView`,\n * and the **bottom** inset is applied. Other insets must be handled manually.\n *\n * On **iOS**, the first scroll view nested inside a native tabs screen has\n * [automatic content inset adjustment](https://reactnative.dev/docs/scrollview#contentinsetadjustmentbehavior-ios) enabled\n *\n * When this property is set to `true`, automatic content inset adjustment is disabled for the screen\n * and must be managed manually. You can use `SafeAreaView` from `react-native-screens/experimental`\n * to handle safe area insets.\n *\n * @platform android\n * @platform ios\n */\n disableAutomaticContentInsets?: boolean;\n /**\n * The style applied to the content of the tab\n *\n * Note: Only certain style properties are supported.\n */\n contentStyle?: NativeTabOptions['contentStyle'];\n}\n\nconst SUPPORTED_TAB_BAR_ITEM_ROLES = [\n 'bookmarks',\n 'contacts',\n 'downloads',\n 'favorites',\n 'featured',\n 'history',\n 'more',\n 'mostRecent',\n 'mostViewed',\n 'recents',\n 'search',\n 'topRated',\n] as const;\n\nexport type NativeTabsTabBarItemRole = (typeof SUPPORTED_TAB_BAR_ITEM_ROLES)[number];\n"]} \ No newline at end of file +{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/native-tabs/types.ts"],"names":[],"mappings":";;;AAsGa,QAAA,sBAAsB,GAAG;IACpC,MAAM;IACN,eAAe;IACf,YAAY;IACZ,OAAO;IACP,MAAM;IACN,SAAS;IACT,WAAW;IACX,yBAAyB;IACzB,oBAAoB;IACpB,gBAAgB;IAChB,qBAAqB;IACrB,sBAAsB;IACtB,8BAA8B;IAC9B,yBAAyB;IACzB,qBAAqB;IACrB,0BAA0B;IAC1B,2BAA2B;IAC3B,6BAA6B;IAC7B,wBAAwB;IACxB,oBAAoB;IACpB,yBAAyB;IACzB,0BAA0B;CAClB,CAAC;AA2KE,QAAA,6CAA6C,GAAG;IAC3D,MAAM;IACN,UAAU;IACV,SAAS;IACT,WAAW;CACH,CAAC;AAUE,QAAA,oCAAoC,GAAG;IAClD,WAAW;IACX,OAAO;IACP,cAAc;IACd,YAAY;CACJ,CAAC;AA8FX,MAAM,4BAA4B,GAAG;IACnC,WAAW;IACX,UAAU;IACV,WAAW;IACX,WAAW;IACX,UAAU;IACV,SAAS;IACT,MAAM;IACN,YAAY;IACZ,YAAY;IACZ,SAAS;IACT,QAAQ;IACR,UAAU;CACF,CAAC","sourcesContent":["import type { DefaultRouterOptions } from '@react-navigation/native';\nimport type { PropsWithChildren } from 'react';\nimport type {\n ColorValue,\n ImageSourcePropType,\n StyleProp,\n TextStyle,\n ViewStyle,\n} from 'react-native';\nimport type { TabsScreenProps } from 'react-native-screens';\nimport type { SFSymbol } from 'sf-symbols-typescript';\n\nexport type NativeScreenProps = Partial>;\n\nexport interface NativeTabOptions extends DefaultRouterOptions {\n icon?: SymbolOrImageSource;\n selectedIcon?: SymbolOrImageSource;\n title?: string;\n badgeValue?: string;\n selectedLabelStyle?: NativeTabsLabelStyle;\n labelStyle?: NativeTabsLabelStyle;\n role?: NativeTabsTabBarItemRole;\n selectedIconColor?: ColorValue;\n selectedBadgeBackgroundColor?: ColorValue;\n badgeBackgroundColor?: ColorValue;\n badgeTextColor?: ColorValue;\n backgroundColor?: ColorValue;\n blurEffect?: NativeTabsBlurEffect;\n shadowColor?: ColorValue;\n iconColor?: ColorValue;\n disableTransparentOnScrollEdge?: boolean;\n titlePositionAdjustment?: {\n horizontal?: number;\n vertical?: number;\n };\n selectedTitlePositionAdjustment?: {\n horizontal?: number;\n vertical?: number;\n };\n indicatorColor?: ColorValue;\n hidden?: boolean;\n specialEffects?: TabsScreenProps['specialEffects'];\n nativeProps?: NativeScreenProps;\n disableAutomaticContentInsets?: boolean;\n contentStyle?: Pick<\n ViewStyle,\n | 'backgroundColor'\n | 'experimental_backgroundImage'\n | 'padding'\n | 'paddingTop'\n | 'paddingBottom'\n | 'paddingLeft'\n | 'paddingRight'\n | 'paddingBlock'\n | 'paddingBlockEnd'\n | 'paddingBlockStart'\n | 'paddingInline'\n | 'paddingInlineEnd'\n | 'paddingInlineStart'\n | 'paddingEnd'\n | 'paddingHorizontal'\n | 'paddingVertical'\n | 'paddingStart'\n | 'alignContent'\n | 'alignItems'\n | 'justifyContent'\n | 'flexDirection'\n | 'gap'\n >;\n}\n\nexport type SymbolOrImageSource =\n | {\n /**\n * The name of the SF Symbol to use as an icon.\n * @platform iOS\n */\n sf?: SFSymbol;\n /**\n * The name of the drawable resource to use as an icon.\n * @platform android\n */\n drawable?: string;\n }\n | {\n /**\n * The image source to use as an icon.\n */\n src?: ImageSourcePropType | Promise;\n /**\n * Controls how the icon is rendered on iOS.\n * @platform ios\n * @default 'template'\n */\n renderingMode?: 'template' | 'original';\n };\n\nexport type NativeTabsLabelStyle = Pick<\n TextStyle,\n 'fontFamily' | 'fontSize' | 'fontStyle' | 'fontWeight' | 'color'\n>;\n\nexport const SUPPORTED_BLUR_EFFECTS = [\n 'none',\n 'systemDefault',\n 'extraLight',\n 'light',\n 'dark',\n 'regular',\n 'prominent',\n 'systemUltraThinMaterial',\n 'systemThinMaterial',\n 'systemMaterial',\n 'systemThickMaterial',\n 'systemChromeMaterial',\n 'systemUltraThinMaterialLight',\n 'systemThinMaterialLight',\n 'systemMaterialLight',\n 'systemThickMaterialLight',\n 'systemChromeMaterialLight',\n 'systemUltraThinMaterialDark',\n 'systemThinMaterialDark',\n 'systemMaterialDark',\n 'systemThickMaterialDark',\n 'systemChromeMaterialDark',\n] as const;\n\n/**\n * @see [Apple documentation](https://developer.apple.com/documentation/uikit/uiblureffect/style)\n */\nexport type NativeTabsBlurEffect = (typeof SUPPORTED_BLUR_EFFECTS)[number];\n\nexport interface NativeTabsProps extends PropsWithChildren {\n // #region common props\n /**\n * The style of the every tab label in the tab bar.\n */\n labelStyle?:\n | StyleProp\n | {\n default?: StyleProp;\n selected?: StyleProp;\n };\n /**\n * The color of every tab icon in the tab bar.\n */\n iconColor?: ColorValue | { default?: ColorValue; selected?: ColorValue };\n /**\n * The tint color of the tab icon.\n *\n * Can be overridden by icon color and label color for each tab individually.\n */\n tintColor?: ColorValue;\n /**\n * The background color of the tab bar.\n */\n backgroundColor?: ColorValue;\n /**\n * The background color of every badge in the tab bar.\n */\n badgeBackgroundColor?: ColorValue;\n /**\n * When set to `true`, hides the tab bar.\n *\n * @default false\n */\n hidden?: boolean;\n // #endregion common props\n // #region iOS props\n /**\n * Specifies the minimize behavior for the tab bar.\n *\n * Available starting from iOS 26.\n *\n * The following values are currently supported:\n *\n * - `automatic` - resolves to the system default minimize behavior\n * - `never` - the tab bar does not minimize\n * - `onScrollDown` - the tab bar minimizes when scrolling down and\n * expands when scrolling back up\n * - `onScrollUp` - the tab bar minimizes when scrolling up and expands\n * when scrolling back down\n *\n * @see The supported values correspond to the official [Apple documentation](https://developer.apple.com/documentation/uikit/uitabbarcontroller/minimizebehavior).\n *\n * @default automatic\n *\n * @platform iOS 26+\n */\n minimizeBehavior?: NativeTabsTabBarMinimizeBehavior;\n /**\n * The blur effect applied to the tab bar.\n *\n * @platform iOS\n */\n blurEffect?: NativeTabsBlurEffect;\n /**\n * The color of the shadow.\n *\n * @see [Apple documentation](https://developer.apple.com/documentation/uikit/uibarappearance/shadowcolor)\n *\n * @platform iOS\n */\n shadowColor?: ColorValue;\n /**\n * @see [Apple documentation](https://developer.apple.com/documentation/uikit/uitabbaritem/titlepositionadjustment)\n *\n * @platform iOS\n */\n titlePositionAdjustment?: {\n horizontal?: number;\n vertical?: number;\n };\n /**\n * When set to `true`, the tab bar will not become transparent when scrolled to the edge.\n *\n * @platform iOS\n */\n disableTransparentOnScrollEdge?: boolean;\n /**\n * When set to `true`, enables the sidebarAdaptable tab bar style on iPadOS and macOS. This prop has no effect on iPhone.\n *\n * @platform iOS 18+\n */\n sidebarAdaptable?: boolean;\n // #endregion iOS props\n // #region android props\n /**\n * Disables the active indicator for the tab bar.\n *\n * @platform android\n */\n disableIndicator?: boolean;\n /**\n * The behavior when navigating back with the back button.\n *\n * @platform android\n */\n backBehavior?: 'none' | 'initialRoute' | 'history';\n /**\n * The visibility mode of the tab item label.\n *\n * @see [Material Components documentation](https://github.com/material-components/material-components-android/blob/master/docs/components/BottomNavigation.md#making-navigation-bar-accessible)\n *\n * @platform android\n */\n labelVisibilityMode?: NativeTabsTabBarItemLabelVisibilityMode;\n /**\n * The color of the ripple effect when the tab is pressed.\n *\n * @platform android\n */\n rippleColor?: ColorValue;\n /**\n * The color of the tab indicator.\n *\n * @platform android\n * @platform web\n */\n indicatorColor?: ColorValue;\n /**\n * The color of the badge text.\n *\n * @platform android\n * @platform web\n */\n badgeTextColor?: ColorValue;\n // #endregion android props\n}\n\nexport interface InternalNativeTabsProps extends NativeTabsProps {\n nonTriggerChildren?: React.ReactNode;\n}\nexport interface NativeTabsViewProps\n extends Omit<\n InternalNativeTabsProps,\n | 'labelStyle'\n | 'iconColor'\n | 'backgroundColor'\n | 'badgeBackgroundColor'\n | 'blurEffect'\n | 'indicatorColor'\n | 'badgeTextColor'\n > {\n focusedIndex: number;\n tabs: NativeTabsViewTabItem[];\n onTabChange: (tabKey: string) => void;\n}\n\nexport interface NativeTabsViewTabItem {\n options: NativeTabOptions;\n routeKey: string;\n name: string;\n contentRenderer: () => React.ReactNode;\n}\n\nexport const SUPPORTED_TAB_BAR_ITEM_LABEL_VISIBILITY_MODES = [\n 'auto',\n 'selected',\n 'labeled',\n 'unlabeled',\n] as const;\n\n/**\n * @see [Material Components documentation](https://github.com/material-components/material-components-android/blob/master/docs/components/BottomNavigation.md#making-navigation-bar-accessible)\n *\n * @platform android\n */\nexport type NativeTabsTabBarItemLabelVisibilityMode =\n (typeof SUPPORTED_TAB_BAR_ITEM_LABEL_VISIBILITY_MODES)[number];\n\nexport const SUPPORTED_TAB_BAR_MINIMIZE_BEHAVIORS = [\n 'automatic',\n 'never',\n 'onScrollDown',\n 'onScrollUp',\n] as const;\n\n/**\n * @see [Apple documentation](https://developer.apple.com/documentation/uikit/uitabbarcontroller/minimizebehavior)\n *\n * @platform iOS 26\n */\nexport type NativeTabsTabBarMinimizeBehavior =\n (typeof SUPPORTED_TAB_BAR_MINIMIZE_BEHAVIORS)[number];\n\nexport interface NativeTabTriggerProps {\n /**\n * The name of the route.\n *\n * This is required when used inside a Layout component.\n *\n * When used in a route it has no effect.\n */\n name?: string;\n /**\n * If true, the tab will be hidden from the tab bar.\n *\n * > **Note**: Marking a tab as `hidden` means it cannot be navigated to in any way.\n */\n hidden?: boolean;\n /**\n * Props passed to the underlying native tab screen implementation.\n * Use this to configure props not directly exposed by Expo Router, but available in `react-native-screens`.\n *\n * > **Note**: This will override any other props set by Expo Router and may lead to unexpected behavior.\n *\n * > **Note**: This is an unstable API and may change or be removed in minor versions.\n *\n * @platform android\n * @platform iOS\n */\n unstable_nativeProps?: NativeScreenProps;\n /**\n * If true, the tab will not pop stack to the root when selected again.\n *\n * @default false\n * @platform iOS\n */\n disablePopToTop?: boolean;\n /**\n * If true, the tab will not scroll to the top when selected again.\n * @default false\n *\n * @platform iOS\n */\n disableScrollToTop?: boolean;\n /**\n * The children of the trigger.\n *\n * Use `Icon`, `Label`, and `Badge` components to customize the tab.\n */\n children?: React.ReactNode;\n /**\n * System-provided tab bar item with predefined icon and title\n *\n * Uses Apple's built-in tab bar items (e.g., bookmarks, contacts, downloads) with\n * standard iOS styling and localized titles. Custom `icon` or `selectedIcon`\n * properties will override the system icon, but the system-defined title cannot\n * be customized.\n *\n * @see The supported values correspond to the official [Apple documentation](https://developer.apple.com/documentation/uikit/uitabbaritem/systemitem).\n * @platform ios\n */\n role?: NativeTabsTabBarItemRole;\n /**\n * The default behavior differs between iOS and Android.\n *\n * On **Android**, the content of a native tabs screen is automatically wrapped in a `SafeAreaView`,\n * and the **bottom** inset is applied. Other insets must be handled manually.\n *\n * On **iOS**, the first scroll view nested inside a native tabs screen has\n * [automatic content inset adjustment](https://reactnative.dev/docs/scrollview#contentinsetadjustmentbehavior-ios) enabled\n *\n * When this property is set to `true`, automatic content inset adjustment is disabled for the screen\n * and must be managed manually. You can use `SafeAreaView` from `react-native-screens/experimental`\n * to handle safe area insets.\n *\n * @platform android\n * @platform ios\n */\n disableAutomaticContentInsets?: boolean;\n /**\n * The style applied to the content of the tab\n *\n * Note: Only certain style properties are supported.\n */\n contentStyle?: NativeTabOptions['contentStyle'];\n}\n\nconst SUPPORTED_TAB_BAR_ITEM_ROLES = [\n 'bookmarks',\n 'contacts',\n 'downloads',\n 'favorites',\n 'featured',\n 'history',\n 'more',\n 'mostRecent',\n 'mostViewed',\n 'recents',\n 'search',\n 'topRated',\n] as const;\n\nexport type NativeTabsTabBarItemRole = (typeof SUPPORTED_TAB_BAR_ITEM_ROLES)[number];\n"]} \ No newline at end of file diff --git a/packages/expo-router/build/native-tabs/utils/bottomAccessory.d.ts b/packages/expo-router/build/native-tabs/utils/bottomAccessory.d.ts index d7586c71b42f4b..e36b6fcb6031dd 100644 --- a/packages/expo-router/build/native-tabs/utils/bottomAccessory.d.ts +++ b/packages/expo-router/build/native-tabs/utils/bottomAccessory.d.ts @@ -1,9 +1,9 @@ import { type ReactElement } from 'react'; -import type { BottomAccessoryFn } from 'react-native-screens'; +import type { TabAccessoryComponentFactory } from 'react-native-screens'; import type { NativeTabsBottomAccessoryProps } from '../common/elements'; /** * Converts `` component into a function, * which can be used by `react-native-screens` to render the accessory. */ -export declare function useBottomAccessoryFunctionFromBottomAccessories(bottomAccessory: ReactElement> | undefined): BottomAccessoryFn | undefined; +export declare function useBottomAccessoryFunctionFromBottomAccessories(bottomAccessory: ReactElement> | undefined): TabAccessoryComponentFactory | undefined; //# sourceMappingURL=bottomAccessory.d.ts.map \ No newline at end of file diff --git a/packages/expo-router/build/native-tabs/utils/bottomAccessory.d.ts.map b/packages/expo-router/build/native-tabs/utils/bottomAccessory.d.ts.map index 6e3bbb0dd93639..7eb8641080a66b 100644 --- a/packages/expo-router/build/native-tabs/utils/bottomAccessory.d.ts.map +++ b/packages/expo-router/build/native-tabs/utils/bottomAccessory.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"bottomAccessory.d.ts","sourceRoot":"","sources":["../../../src/native-tabs/utils/bottomAccessory.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AACnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AAGzE;;;GAGG;AACH,wBAAgB,+CAA+C,CAC7D,eAAe,EACX,YAAY,CAAC,8BAA8B,EAAE,MAAM,GAAG,KAAK,CAAC,qBAAqB,CAAC,GAAG,CAAC,CAAC,GACvF,SAAS,GACZ,iBAAiB,GAAG,SAAS,CAY/B"} \ No newline at end of file +{"version":3,"file":"bottomAccessory.d.ts","sourceRoot":"","sources":["../../../src/native-tabs/utils/bottomAccessory.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AACnD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC;AAEzE,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AAGzE;;;GAGG;AACH,wBAAgB,+CAA+C,CAC7D,eAAe,EACX,YAAY,CAAC,8BAA8B,EAAE,MAAM,GAAG,KAAK,CAAC,qBAAqB,CAAC,GAAG,CAAC,CAAC,GACvF,SAAS,GACZ,4BAA4B,GAAG,SAAS,CAY1C"} \ No newline at end of file diff --git a/packages/expo-router/build/native-tabs/utils/bottomAccessory.js.map b/packages/expo-router/build/native-tabs/utils/bottomAccessory.js.map index 37ddf4b4a705b0..1d21d1f42fc140 100644 --- a/packages/expo-router/build/native-tabs/utils/bottomAccessory.js.map +++ b/packages/expo-router/build/native-tabs/utils/bottomAccessory.js.map @@ -1 +1 @@ -{"version":3,"file":"bottomAccessory.js","sourceRoot":"","sources":["../../../src/native-tabs/utils/bottomAccessory.tsx"],"names":[],"mappings":";;AAUA,0GAgBC;AA1BD,iCAAmD;AAInD,oCAA2D;AAE3D;;;GAGG;AACH,SAAgB,+CAA+C,CAC7D,eAEa;IAEb,OAAO,IAAA,eAAO,EACZ,GAAG,EAAE,CACH,eAAe;QACb,CAAC,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CACf,CAAC,uCAA+B,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,CAClD;cAAA,CAAC,eAAe,CAAC,KAAK,CAAC,QAAQ,CACjC;YAAA,EAAE,uCAA+B,CAAC,CACnC;QACH,CAAC,CAAC,SAAS,EACf,CAAC,eAAe,CAAC,CAClB,CAAC;AACJ,CAAC","sourcesContent":["import { useMemo, type ReactElement } from 'react';\nimport type { BottomAccessoryFn } from 'react-native-screens';\n\nimport type { NativeTabsBottomAccessoryProps } from '../common/elements';\nimport { BottomAccessoryPlacementContext } from '../hooks';\n\n/**\n * Converts `` component into a function,\n * which can be used by `react-native-screens` to render the accessory.\n */\nexport function useBottomAccessoryFunctionFromBottomAccessories(\n bottomAccessory:\n | ReactElement>\n | undefined\n): BottomAccessoryFn | undefined {\n return useMemo(\n () =>\n bottomAccessory\n ? (environment) => (\n \n {bottomAccessory.props.children}\n \n )\n : undefined,\n [bottomAccessory]\n );\n}\n"]} \ No newline at end of file +{"version":3,"file":"bottomAccessory.js","sourceRoot":"","sources":["../../../src/native-tabs/utils/bottomAccessory.tsx"],"names":[],"mappings":";;AAUA,0GAgBC;AA1BD,iCAAmD;AAInD,oCAA2D;AAE3D;;;GAGG;AACH,SAAgB,+CAA+C,CAC7D,eAEa;IAEb,OAAO,IAAA,eAAO,EACZ,GAAG,EAAE,CACH,eAAe;QACb,CAAC,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CACf,CAAC,uCAA+B,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,CAClD;cAAA,CAAC,eAAe,CAAC,KAAK,CAAC,QAAQ,CACjC;YAAA,EAAE,uCAA+B,CAAC,CACnC;QACH,CAAC,CAAC,SAAS,EACf,CAAC,eAAe,CAAC,CAClB,CAAC;AACJ,CAAC","sourcesContent":["import { useMemo, type ReactElement } from 'react';\nimport type { TabAccessoryComponentFactory } from 'react-native-screens';\n\nimport type { NativeTabsBottomAccessoryProps } from '../common/elements';\nimport { BottomAccessoryPlacementContext } from '../hooks';\n\n/**\n * Converts `` component into a function,\n * which can be used by `react-native-screens` to render the accessory.\n */\nexport function useBottomAccessoryFunctionFromBottomAccessories(\n bottomAccessory:\n | ReactElement>\n | undefined\n): TabAccessoryComponentFactory | undefined {\n return useMemo(\n () =>\n bottomAccessory\n ? (environment) => (\n \n {bottomAccessory.props.children}\n \n )\n : undefined,\n [bottomAccessory]\n );\n}\n"]} \ No newline at end of file diff --git a/packages/expo-router/build/native-tabs/utils/icon.d.ts b/packages/expo-router/build/native-tabs/utils/icon.d.ts index 6348ff8ee88946..ed5a793d42cf06 100644 --- a/packages/expo-router/build/native-tabs/utils/icon.d.ts +++ b/packages/expo-router/build/native-tabs/utils/icon.d.ts @@ -1,5 +1,5 @@ import type { ColorValue, ImageSourcePropType } from 'react-native'; -import type { BottomTabsScreenProps, PlatformIconAndroid, PlatformIconIOS } from 'react-native-screens'; +import type { TabsScreenProps, PlatformIconAndroid, PlatformIconIOS } from 'react-native-screens'; import type { SFSymbol } from 'sf-symbols-typescript'; import type { NativeTabOptions, NativeTabsProps } from '../types'; export declare function convertIconColorPropToObject(iconColor: NativeTabsProps['iconColor']): { @@ -20,7 +20,7 @@ export declare function useAwaitedScreensIcon(icon: NativeTabOptions['icon']): { src?: ImageSourcePropType; renderingMode?: "template" | "original"; } | undefined; -export declare function convertOptionsIconToRNScreensPropsIcon(icon: AwaitedIcon | undefined): BottomTabsScreenProps['icon']; +export declare function convertOptionsIconToRNScreensPropsIcon(icon: AwaitedIcon | undefined): TabsScreenProps['icon']; export declare function convertOptionsIconToIOSPropsIcon(icon: AwaitedIcon | undefined): PlatformIconIOS | undefined; export declare function convertOptionsIconToAndroidPropsIcon(icon: AwaitedIcon): PlatformIconAndroid | undefined; export declare function convertComponentSrcToImageSource(src: React.ReactElement): { diff --git a/packages/expo-router/build/native-tabs/utils/icon.d.ts.map b/packages/expo-router/build/native-tabs/utils/icon.d.ts.map index 4fbe4e265f915f..90cee578313b78 100644 --- a/packages/expo-router/build/native-tabs/utils/icon.d.ts.map +++ b/packages/expo-router/build/native-tabs/utils/icon.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"icon.d.ts","sourceRoot":"","sources":["../../../src/native-tabs/utils/icon.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,KAAK,EACV,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,EAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAItD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAElE,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,eAAe,CAAC,WAAW,CAAC,GAAG;IACrF,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,QAAQ,CAAC,EAAE,UAAU,CAAC;CACvB,CAUA;AAED,KAAK,WAAW,GACZ;IACE,EAAE,CAAC,EAAE,QAAQ,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GACD;IACE,GAAG,CAAC,EAAE,mBAAmB,CAAC;IAC1B,aAAa,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;CACzC,CAAC;AAEN,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC;;;;UAJxD,mBAAmB;oBACT,UAAU,GAAG,UAAU;cAyB5C;AAMD,wBAAgB,sCAAsC,CACpD,IAAI,EAAE,WAAW,GAAG,SAAS,GAC5B,qBAAqB,CAAC,MAAM,CAAC,CAQ/B;AAED,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,WAAW,GAAG,SAAS,GAC5B,eAAe,GAAG,SAAS,CAc7B;AAED,wBAAgB,oCAAoC,CAClD,IAAI,EAAE,WAAW,GAChB,mBAAmB,GAAG,SAAS,CAWjC;AAED,wBAAgB,gCAAgC,CAAC,GAAG,EAAE,KAAK,CAAC,YAAY;;cAUvE"} \ No newline at end of file +{"version":3,"file":"icon.d.ts","sourceRoot":"","sources":["../../../src/native-tabs/utils/icon.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAClG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAItD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAElE,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,eAAe,CAAC,WAAW,CAAC,GAAG;IACrF,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,QAAQ,CAAC,EAAE,UAAU,CAAC;CACvB,CAUA;AAED,KAAK,WAAW,GACZ;IACE,EAAE,CAAC,EAAE,QAAQ,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GACD;IACE,GAAG,CAAC,EAAE,mBAAmB,CAAC;IAC1B,aAAa,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;CACzC,CAAC;AAEN,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC;;;;UAJxD,mBAAmB;oBACT,UAAU,GAAG,UAAU;cAyB5C;AAMD,wBAAgB,sCAAsC,CACpD,IAAI,EAAE,WAAW,GAAG,SAAS,GAC5B,eAAe,CAAC,MAAM,CAAC,CAQzB;AAED,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,WAAW,GAAG,SAAS,GAC5B,eAAe,GAAG,SAAS,CAc7B;AAED,wBAAgB,oCAAoC,CAClD,IAAI,EAAE,WAAW,GAChB,mBAAmB,GAAG,SAAS,CAWjC;AAED,wBAAgB,gCAAgC,CAAC,GAAG,EAAE,KAAK,CAAC,YAAY;;cAUvE"} \ No newline at end of file diff --git a/packages/expo-router/build/native-tabs/utils/icon.js.map b/packages/expo-router/build/native-tabs/utils/icon.js.map index af3d1606a96dae..5b0aebaceb7d54 100644 --- a/packages/expo-router/build/native-tabs/utils/icon.js.map +++ b/packages/expo-router/build/native-tabs/utils/icon.js.map @@ -1 +1 @@ -{"version":3,"file":"icon.js","sourceRoot":"","sources":["../../../src/native-tabs/utils/icon.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport type { ColorValue, ImageSourcePropType } from 'react-native';\nimport type {\n BottomTabsScreenProps,\n PlatformIconAndroid,\n PlatformIconIOS,\n} from 'react-native-screens';\nimport type { SFSymbol } from 'sf-symbols-typescript';\n\nimport { isChildOfType } from '../../utils/children';\nimport { NativeTabsTriggerPromiseIcon, NativeTabsTriggerVectorIcon } from '../common/elements';\nimport type { NativeTabOptions, NativeTabsProps } from '../types';\n\nexport function convertIconColorPropToObject(iconColor: NativeTabsProps['iconColor']): {\n default?: ColorValue;\n selected?: ColorValue;\n} {\n if (iconColor) {\n if (typeof iconColor === 'object' && ('default' in iconColor || 'selected' in iconColor)) {\n return iconColor;\n }\n return {\n default: iconColor as ColorValue,\n };\n }\n return {};\n}\n\ntype AwaitedIcon =\n | {\n sf?: SFSymbol;\n drawable?: string;\n }\n | {\n src?: ImageSourcePropType;\n renderingMode?: 'template' | 'original';\n };\n\nexport function useAwaitedScreensIcon(icon: NativeTabOptions['icon']) {\n const src = icon && typeof icon === 'object' && 'src' in icon ? icon.src : undefined;\n const [awaitedIcon, setAwaitedIcon] = useState(undefined);\n\n useEffect(() => {\n const loadIcon = async () => {\n if (src && src instanceof Promise) {\n const awaitedSrc = await src;\n if (awaitedSrc) {\n const currentAwaitedIcon = { src: awaitedSrc };\n setAwaitedIcon(currentAwaitedIcon);\n }\n }\n };\n loadIcon();\n // Checking `src` rather then icon here, to avoid unnecessary re-renders\n // The icon object can be recreated, while src should stay the same\n // In this case as we control `VectorIcon`, it will only change if `family` or `name` props change\n // So we should be safe with promise resolving\n }, [src]);\n\n return useMemo(() => (isAwaitedIcon(icon) ? icon : awaitedIcon), [awaitedIcon, icon]);\n}\n\nfunction isAwaitedIcon(icon: NativeTabOptions['icon']): icon is AwaitedIcon {\n return !icon || !('src' in icon && icon.src instanceof Promise);\n}\n\nexport function convertOptionsIconToRNScreensPropsIcon(\n icon: AwaitedIcon | undefined\n): BottomTabsScreenProps['icon'] {\n if (!icon) {\n return undefined;\n }\n return {\n ios: convertOptionsIconToIOSPropsIcon(icon),\n android: convertOptionsIconToAndroidPropsIcon(icon),\n };\n}\n\nexport function convertOptionsIconToIOSPropsIcon(\n icon: AwaitedIcon | undefined\n): PlatformIconIOS | undefined {\n if (icon && 'sf' in icon && icon.sf) {\n return {\n type: 'sfSymbol',\n name: icon.sf,\n };\n }\n if (icon && 'src' in icon && icon.src) {\n if (icon.renderingMode === 'original') {\n return { type: 'imageSource', imageSource: icon.src };\n }\n return { type: 'templateSource', templateSource: icon.src };\n }\n return undefined;\n}\n\nexport function convertOptionsIconToAndroidPropsIcon(\n icon: AwaitedIcon\n): PlatformIconAndroid | undefined {\n if (icon && 'drawable' in icon && icon.drawable) {\n return {\n type: 'drawableResource',\n name: icon.drawable,\n };\n }\n if (icon && 'src' in icon && icon.src) {\n return { type: 'imageSource', imageSource: icon.src };\n }\n return undefined;\n}\n\nexport function convertComponentSrcToImageSource(src: React.ReactElement) {\n if (isChildOfType(src, NativeTabsTriggerVectorIcon)) {\n const props = src.props;\n return { src: props.family.getImageSource(props.name, 24, 'white') };\n } else if (isChildOfType(src, NativeTabsTriggerPromiseIcon)) {\n return { src: src.props.loader() };\n } else {\n console.warn('Only VectorIcon is supported as a React element in Icon.src');\n }\n return undefined;\n}\n"]} \ No newline at end of file +{"version":3,"file":"icon.js","sourceRoot":"","sources":["../../../src/native-tabs/utils/icon.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport type { ColorValue, ImageSourcePropType } from 'react-native';\nimport type { TabsScreenProps, PlatformIconAndroid, PlatformIconIOS } from 'react-native-screens';\nimport type { SFSymbol } from 'sf-symbols-typescript';\n\nimport { isChildOfType } from '../../utils/children';\nimport { NativeTabsTriggerPromiseIcon, NativeTabsTriggerVectorIcon } from '../common/elements';\nimport type { NativeTabOptions, NativeTabsProps } from '../types';\n\nexport function convertIconColorPropToObject(iconColor: NativeTabsProps['iconColor']): {\n default?: ColorValue;\n selected?: ColorValue;\n} {\n if (iconColor) {\n if (typeof iconColor === 'object' && ('default' in iconColor || 'selected' in iconColor)) {\n return iconColor;\n }\n return {\n default: iconColor as ColorValue,\n };\n }\n return {};\n}\n\ntype AwaitedIcon =\n | {\n sf?: SFSymbol;\n drawable?: string;\n }\n | {\n src?: ImageSourcePropType;\n renderingMode?: 'template' | 'original';\n };\n\nexport function useAwaitedScreensIcon(icon: NativeTabOptions['icon']) {\n const src = icon && typeof icon === 'object' && 'src' in icon ? icon.src : undefined;\n const [awaitedIcon, setAwaitedIcon] = useState(undefined);\n\n useEffect(() => {\n const loadIcon = async () => {\n if (src && src instanceof Promise) {\n const awaitedSrc = await src;\n if (awaitedSrc) {\n const currentAwaitedIcon = { src: awaitedSrc };\n setAwaitedIcon(currentAwaitedIcon);\n }\n }\n };\n loadIcon();\n // Checking `src` rather then icon here, to avoid unnecessary re-renders\n // The icon object can be recreated, while src should stay the same\n // In this case as we control `VectorIcon`, it will only change if `family` or `name` props change\n // So we should be safe with promise resolving\n }, [src]);\n\n return useMemo(() => (isAwaitedIcon(icon) ? icon : awaitedIcon), [awaitedIcon, icon]);\n}\n\nfunction isAwaitedIcon(icon: NativeTabOptions['icon']): icon is AwaitedIcon {\n return !icon || !('src' in icon && icon.src instanceof Promise);\n}\n\nexport function convertOptionsIconToRNScreensPropsIcon(\n icon: AwaitedIcon | undefined\n): TabsScreenProps['icon'] {\n if (!icon) {\n return undefined;\n }\n return {\n ios: convertOptionsIconToIOSPropsIcon(icon),\n android: convertOptionsIconToAndroidPropsIcon(icon),\n };\n}\n\nexport function convertOptionsIconToIOSPropsIcon(\n icon: AwaitedIcon | undefined\n): PlatformIconIOS | undefined {\n if (icon && 'sf' in icon && icon.sf) {\n return {\n type: 'sfSymbol',\n name: icon.sf,\n };\n }\n if (icon && 'src' in icon && icon.src) {\n if (icon.renderingMode === 'original') {\n return { type: 'imageSource', imageSource: icon.src };\n }\n return { type: 'templateSource', templateSource: icon.src };\n }\n return undefined;\n}\n\nexport function convertOptionsIconToAndroidPropsIcon(\n icon: AwaitedIcon\n): PlatformIconAndroid | undefined {\n if (icon && 'drawable' in icon && icon.drawable) {\n return {\n type: 'drawableResource',\n name: icon.drawable,\n };\n }\n if (icon && 'src' in icon && icon.src) {\n return { type: 'imageSource', imageSource: icon.src };\n }\n return undefined;\n}\n\nexport function convertComponentSrcToImageSource(src: React.ReactElement) {\n if (isChildOfType(src, NativeTabsTriggerVectorIcon)) {\n const props = src.props;\n return { src: props.family.getImageSource(props.name, 24, 'white') };\n } else if (isChildOfType(src, NativeTabsTriggerPromiseIcon)) {\n return { src: src.props.loader() };\n } else {\n console.warn('Only VectorIcon is supported as a React element in Icon.src');\n }\n return undefined;\n}\n"]} \ No newline at end of file diff --git a/packages/expo-router/build/split-view/elements.js b/packages/expo-router/build/split-view/elements.js index 934818c2a66c30..9ee823789bf3c5 100644 --- a/packages/expo-router/build/split-view/elements.js +++ b/packages/expo-router/build/split-view/elements.js @@ -5,14 +5,14 @@ exports.SplitViewInspector = SplitViewInspector; const react_native_safe_area_context_1 = require("react-native-safe-area-context"); const experimental_1 = require("react-native-screens/experimental"); function SplitViewColumn(props) { - return ( + return ( {props.children} - ); + ); } /** * @platform iOS 26+ */ function SplitViewInspector(props) { - return {props.children}; + return {props.children}; } //# sourceMappingURL=elements.js.map \ No newline at end of file diff --git a/packages/expo-router/build/split-view/elements.js.map b/packages/expo-router/build/split-view/elements.js.map index ee7688327cf108..e99f7cf72dc62d 100644 --- a/packages/expo-router/build/split-view/elements.js.map +++ b/packages/expo-router/build/split-view/elements.js.map @@ -1 +1 @@ -{"version":3,"file":"elements.js","sourceRoot":"","sources":["../../src/split-view/elements.tsx"],"names":[],"mappings":";;AAOA,0CAMC;AAKD,gDAEC;AApBD,mFAAkE;AAClE,oEAAoE;AAMpE,SAAgB,eAAe,CAAC,KAA2B;IACzD,OAAO,CACL,CAAC,8BAAe,CAAC,MAAM,CACrB;MAAA,CAAC,iDAAgB,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,iDAAgB,CACtD;IAAA,EAAE,8BAAe,CAAC,MAAM,CAAC,CAC1B,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAgB,kBAAkB,CAAC,KAA2B;IAC5D,OAAO,CAAC,8BAAe,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,8BAAe,CAAC,SAAS,CAAC,CAAC;AACjF,CAAC","sourcesContent":["import { SafeAreaProvider } from 'react-native-safe-area-context';\nimport { SplitViewScreen } from 'react-native-screens/experimental';\n\nexport interface SplitViewColumnProps {\n children?: React.ReactNode;\n}\n\nexport function SplitViewColumn(props: SplitViewColumnProps) {\n return (\n \n {props.children}\n \n );\n}\n\n/**\n * @platform iOS 26+\n */\nexport function SplitViewInspector(props: SplitViewColumnProps) {\n return {props.children};\n}\n"]} \ No newline at end of file +{"version":3,"file":"elements.js","sourceRoot":"","sources":["../../src/split-view/elements.tsx"],"names":[],"mappings":";;AAOA,0CAMC;AAKD,gDAEC;AApBD,mFAAkE;AAClE,oEAA0D;AAM1D,SAAgB,eAAe,CAAC,KAA2B;IACzD,OAAO,CACL,CAAC,oBAAK,CAAC,MAAM,CACX;MAAA,CAAC,iDAAgB,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,iDAAgB,CACtD;IAAA,EAAE,oBAAK,CAAC,MAAM,CAAC,CAChB,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAgB,kBAAkB,CAAC,KAA2B;IAC5D,OAAO,CAAC,oBAAK,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,oBAAK,CAAC,SAAS,CAAC,CAAC;AAC7D,CAAC","sourcesContent":["import { SafeAreaProvider } from 'react-native-safe-area-context';\nimport { Split } from 'react-native-screens/experimental';\n\nexport interface SplitViewColumnProps {\n children?: React.ReactNode;\n}\n\nexport function SplitViewColumn(props: SplitViewColumnProps) {\n return (\n \n {props.children}\n \n );\n}\n\n/**\n * @platform iOS 26+\n */\nexport function SplitViewInspector(props: SplitViewColumnProps) {\n return {props.children};\n}\n"]} \ No newline at end of file diff --git a/packages/expo-router/build/split-view/index.d.ts b/packages/expo-router/build/split-view/index.d.ts index 2ed4bd70ea1ecd..906b476e28d30e 100644 --- a/packages/expo-router/build/split-view/index.d.ts +++ b/packages/expo-router/build/split-view/index.d.ts @@ -1,4 +1,4 @@ export { SplitView, SplitViewProps } from './split-view'; export * from './elements'; -export { type SplitViewHostProps } from 'react-native-screens/experimental'; +export { type SplitHostProps } from 'react-native-screens/experimental'; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/expo-router/build/split-view/index.d.ts.map b/packages/expo-router/build/split-view/index.d.ts.map index 103499d0baa173..a280c1261a4f2e 100644 --- a/packages/expo-router/build/split-view/index.d.ts.map +++ b/packages/expo-router/build/split-view/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/split-view/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACzD,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,mCAAmC,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/split-view/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACzD,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,mCAAmC,CAAC"} \ No newline at end of file diff --git a/packages/expo-router/build/split-view/index.js.map b/packages/expo-router/build/split-view/index.js.map index 5a6066227f9734..f293e85a0346ce 100644 --- a/packages/expo-router/build/split-view/index.js.map +++ b/packages/expo-router/build/split-view/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/split-view/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,2CAAyD;AAAhD,uGAAA,SAAS,OAAA;AAClB,6CAA2B","sourcesContent":["export { SplitView, SplitViewProps } from './split-view';\nexport * from './elements';\nexport { type SplitViewHostProps } from 'react-native-screens/experimental';\n"]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/split-view/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,2CAAyD;AAAhD,uGAAA,SAAS,OAAA;AAClB,6CAA2B","sourcesContent":["export { SplitView, SplitViewProps } from './split-view';\nexport * from './elements';\nexport { type SplitHostProps } from 'react-native-screens/experimental';\n"]} \ No newline at end of file diff --git a/packages/expo-router/build/split-view/split-view.d.ts b/packages/expo-router/build/split-view/split-view.d.ts index 966a88fffd2ecc..f5f1c0aa1b05c5 100644 --- a/packages/expo-router/build/split-view/split-view.d.ts +++ b/packages/expo-router/build/split-view/split-view.d.ts @@ -1,10 +1,10 @@ import React, { type ReactNode } from 'react'; -import { type SplitViewHostProps } from 'react-native-screens/experimental'; +import { type SplitHostProps } from 'react-native-screens/experimental'; import { SplitViewColumn, SplitViewInspector } from './elements'; /** - * For full list of supported props, see [`SplitViewHostProps`](https://github.com/software-mansion/react-native-screens/blob/main/src/components/gamma/split-view/SplitViewHost.types.ts#L124) + * For full list of supported props, see [`SplitHostProps`](http://github.com/software-mansion/react-native-screens/blob/main/src/components/gamma/split/SplitHost.types.ts#L117) */ -export interface SplitViewProps extends Omit { +export interface SplitViewProps extends Omit { children?: ReactNode; } declare function SplitViewNavigator({ children, ...splitViewHostProps }: SplitViewProps): React.JSX.Element; diff --git a/packages/expo-router/build/split-view/split-view.d.ts.map b/packages/expo-router/build/split-view/split-view.d.ts.map index 39afdaffdc264d..404f6ced1e4664 100644 --- a/packages/expo-router/build/split-view/split-view.d.ts.map +++ b/packages/expo-router/build/split-view/split-view.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"split-view.d.ts","sourceRoot":"","sources":["../../src/split-view/split-view.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAsC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAClF,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAMjE;;GAEG;AACH,MAAM,WAAW,cAAe,SAAQ,IAAI,CAAC,kBAAkB,EAAE,UAAU,CAAC;IAC1E,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,iBAAS,kBAAkB,CAAC,EAAE,QAAQ,EAAE,GAAG,kBAAkB,EAAE,EAAE,cAAc,qBA0D9E;AAED,eAAO,MAAM,SAAS;;;CAGpB,CAAC"} \ No newline at end of file +{"version":3,"file":"split-view.d.ts","sourceRoot":"","sources":["../../src/split-view/split-view.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAsC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAClF,OAAO,EAAS,KAAK,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAE/E,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAMjE;;GAEG;AACH,MAAM,WAAW,cAAe,SAAQ,IAAI,CAAC,cAAc,EAAE,UAAU,CAAC;IACtE,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,iBAAS,kBAAkB,CAAC,EAAE,QAAQ,EAAE,GAAG,kBAAkB,EAAE,EAAE,cAAc,qBA0D9E;AAED,eAAO,MAAM,SAAS;;;CAGpB,CAAC"} \ No newline at end of file diff --git a/packages/expo-router/build/split-view/split-view.js b/packages/expo-router/build/split-view/split-view.js index 224d7c03dc2426..5edd95af9c38db 100644 --- a/packages/expo-router/build/split-view/split-view.js +++ b/packages/expo-router/build/split-view/split-view.js @@ -71,13 +71,13 @@ function SplitViewNavigator({ children, ...splitViewHostProps }) { return ; } // The key is needed, because number of columns cannot be changed dynamically - return ( + return ( {columnChildren} - + - + {inspectorChildren} - ); + ); } exports.SplitView = Object.assign(SplitViewNavigator, { Column: elements_1.SplitViewColumn, diff --git a/packages/expo-router/build/split-view/split-view.js.map b/packages/expo-router/build/split-view/split-view.js.map index 3733011337147f..b8bbf2d9975c46 100644 --- a/packages/expo-router/build/split-view/split-view.js.map +++ b/packages/expo-router/build/split-view/split-view.js.map @@ -1 +1 @@ -{"version":3,"file":"split-view.js","sourceRoot":"","sources":["../../src/split-view/split-view.tsx"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,+CAAkF;AAClF,oEAI2C;AAE3C,yCAAiE;AACjE,4EAAyE;AACzE,kDAA0C;AAE1C,MAAM,wBAAwB,GAAG,IAAA,qBAAa,EAAC,KAAK,CAAC,CAAC;AAStD,SAAS,kBAAkB,CAAC,EAAE,QAAQ,EAAE,GAAG,kBAAkB,EAAkB;IAC7E,IAAI,IAAA,WAAG,EAAC,wBAAwB,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,8DAA8D,CAAC,CAAC;IAClF,CAAC;IAED,sFAAsF;IACtF,IAAI,IAAA,WAAG,EAAC,6CAAqB,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAC;IACzF,CAAC;IAED,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;QAClC,OAAO,CAAC,IAAI,CACV,yGAAyG,CAC1G,CAAC;QACF,OAAO,CAAC,gBAAI,CAAC,AAAD,EAAG,CAAC;IAClB,CAAC;IAED,MAAM,WAAW,GAAG,GAAG,EAAE,CAAC,CACxB,CAAC,6CAAqB,CAAC,KAAK,CAC1B;MAAA,CAAC,gBAAI,CAAC,AAAD,EACP;IAAA,EAAE,6CAAqB,CAAC,CACzB,CAAC;IAEF,MAAM,gBAAgB,GAAG,eAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1D,MAAM,cAAc,GAAG,gBAAgB,CAAC,MAAM,CAC5C,CAAC,KAAK,EAAE,EAAE,CAAC,IAAA,sBAAc,EAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,0BAAe,CACnE,CAAC;IACF,MAAM,iBAAiB,GAAG,gBAAgB,CAAC,MAAM,CAC/C,CAAC,KAAK,EAAE,EAAE,CAAC,IAAA,sBAAc,EAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,6BAAkB,CACtE,CAAC;IACF,MAAM,gBAAgB,GAAG,cAAc,CAAC,MAAM,CAAC;IAC/C,MAAM,kBAAkB,GAAG,iBAAiB,CAAC,MAAM,CAAC;IAEpD,IAAI,gBAAgB,CAAC,MAAM,KAAK,cAAc,CAAC,MAAM,GAAG,iBAAiB,CAAC,MAAM,EAAE,CAAC;QACjF,OAAO,CAAC,IAAI,CACV,uGAAuG,CACxG,CAAC;IACJ,CAAC;IAED,IAAI,gBAAgB,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAC;IAC9E,CAAC;IAED,IAAI,gBAAgB,GAAG,kBAAkB,KAAK,CAAC,EAAE,CAAC;QAChD,OAAO,CAAC,IAAI,CAAC,iEAAiE,CAAC,CAAC;QAChF,OAAO,CAAC,gBAAI,CAAC,AAAD,EAAG,CAAC;IAClB,CAAC;IAED,6EAA6E;IAC7E,OAAO,CACL,CAAC,4BAAa,CAAC,GAAG,CAAC,CAAC,gBAAgB,GAAG,kBAAkB,CAAC,CAAC,IAAI,kBAAkB,CAAC,CAChF;MAAA,CAAC,cAAc,CACf;MAAA,CAAC,8BAAe,CAAC,MAAM,CACrB;QAAA,CAAC,WAAW,CAAC,AAAD,EACd;MAAA,EAAE,8BAAe,CAAC,MAAM,CACxB;MAAA,CAAC,iBAAiB,CACpB;IAAA,EAAE,4BAAa,CAAC,CACjB,CAAC;AACJ,CAAC;AAEY,QAAA,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,kBAAkB,EAAE;IACzD,MAAM,EAAE,0BAAe;IACvB,SAAS,EAAE,6BAAkB;CAC9B,CAAC,CAAC","sourcesContent":["import React, { createContext, isValidElement, use, type ReactNode } from 'react';\nimport {\n SplitViewHost,\n SplitViewScreen,\n type SplitViewHostProps,\n} from 'react-native-screens/experimental';\n\nimport { SplitViewColumn, SplitViewInspector } from './elements';\nimport { IsWithinLayoutContext } from '../layouts/IsWithinLayoutContext';\nimport { Slot } from '../views/Navigator';\n\nconst IsWithinSplitViewContext = createContext(false);\n\n/**\n * For full list of supported props, see [`SplitViewHostProps`](https://github.com/software-mansion/react-native-screens/blob/main/src/components/gamma/split-view/SplitViewHost.types.ts#L124)\n */\nexport interface SplitViewProps extends Omit {\n children?: ReactNode;\n}\n\nfunction SplitViewNavigator({ children, ...splitViewHostProps }: SplitViewProps) {\n if (use(IsWithinSplitViewContext)) {\n throw new Error('There can only be one SplitView in the navigation hierarchy.');\n }\n\n // TODO: Add better way of detecting if SplitView is rendered inside Native navigator.\n if (use(IsWithinLayoutContext)) {\n throw new Error('SplitView cannot be used inside another navigator, except for Slot.');\n }\n\n if (process.env.EXPO_OS !== 'ios') {\n console.warn(\n 'SplitView is only supported on iOS. The SplitView will behave like a Slot navigator on other platforms.'\n );\n return ;\n }\n\n const WrappedSlot = () => (\n \n \n \n );\n\n const allChildrenArray = React.Children.toArray(children);\n const columnChildren = allChildrenArray.filter(\n (child) => isValidElement(child) && child.type === SplitViewColumn\n );\n const inspectorChildren = allChildrenArray.filter(\n (child) => isValidElement(child) && child.type === SplitViewInspector\n );\n const numberOfSidebars = columnChildren.length;\n const numberOfInspectors = inspectorChildren.length;\n\n if (allChildrenArray.length !== columnChildren.length + inspectorChildren.length) {\n console.warn(\n 'Only SplitView.Column and SplitView.Inspector components are allowed as direct children of SplitView.'\n );\n }\n\n if (numberOfSidebars > 2) {\n throw new Error('There can only be two SplitView.Column in the SplitView.');\n }\n\n if (numberOfSidebars + numberOfInspectors === 0) {\n console.warn('No SplitView.Column and SplitView.Inspector found in SplitView.');\n return ;\n }\n\n // The key is needed, because number of columns cannot be changed dynamically\n return (\n \n {columnChildren}\n \n \n \n {inspectorChildren}\n \n );\n}\n\nexport const SplitView = Object.assign(SplitViewNavigator, {\n Column: SplitViewColumn,\n Inspector: SplitViewInspector,\n});\n"]} \ No newline at end of file +{"version":3,"file":"split-view.js","sourceRoot":"","sources":["../../src/split-view/split-view.tsx"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,+CAAkF;AAClF,oEAA+E;AAE/E,yCAAiE;AACjE,4EAAyE;AACzE,kDAA0C;AAE1C,MAAM,wBAAwB,GAAG,IAAA,qBAAa,EAAC,KAAK,CAAC,CAAC;AAStD,SAAS,kBAAkB,CAAC,EAAE,QAAQ,EAAE,GAAG,kBAAkB,EAAkB;IAC7E,IAAI,IAAA,WAAG,EAAC,wBAAwB,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,8DAA8D,CAAC,CAAC;IAClF,CAAC;IAED,sFAAsF;IACtF,IAAI,IAAA,WAAG,EAAC,6CAAqB,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAC;IACzF,CAAC;IAED,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;QAClC,OAAO,CAAC,IAAI,CACV,yGAAyG,CAC1G,CAAC;QACF,OAAO,CAAC,gBAAI,CAAC,AAAD,EAAG,CAAC;IAClB,CAAC;IAED,MAAM,WAAW,GAAG,GAAG,EAAE,CAAC,CACxB,CAAC,6CAAqB,CAAC,KAAK,CAC1B;MAAA,CAAC,gBAAI,CAAC,AAAD,EACP;IAAA,EAAE,6CAAqB,CAAC,CACzB,CAAC;IAEF,MAAM,gBAAgB,GAAG,eAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1D,MAAM,cAAc,GAAG,gBAAgB,CAAC,MAAM,CAC5C,CAAC,KAAK,EAAE,EAAE,CAAC,IAAA,sBAAc,EAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,0BAAe,CACnE,CAAC;IACF,MAAM,iBAAiB,GAAG,gBAAgB,CAAC,MAAM,CAC/C,CAAC,KAAK,EAAE,EAAE,CAAC,IAAA,sBAAc,EAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,6BAAkB,CACtE,CAAC;IACF,MAAM,gBAAgB,GAAG,cAAc,CAAC,MAAM,CAAC;IAC/C,MAAM,kBAAkB,GAAG,iBAAiB,CAAC,MAAM,CAAC;IAEpD,IAAI,gBAAgB,CAAC,MAAM,KAAK,cAAc,CAAC,MAAM,GAAG,iBAAiB,CAAC,MAAM,EAAE,CAAC;QACjF,OAAO,CAAC,IAAI,CACV,uGAAuG,CACxG,CAAC;IACJ,CAAC;IAED,IAAI,gBAAgB,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAC;IAC9E,CAAC;IAED,IAAI,gBAAgB,GAAG,kBAAkB,KAAK,CAAC,EAAE,CAAC;QAChD,OAAO,CAAC,IAAI,CAAC,iEAAiE,CAAC,CAAC;QAChF,OAAO,CAAC,gBAAI,CAAC,AAAD,EAAG,CAAC;IAClB,CAAC;IAED,6EAA6E;IAC7E,OAAO,CACL,CAAC,oBAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,gBAAgB,GAAG,kBAAkB,CAAC,CAAC,IAAI,kBAAkB,CAAC,CAC7E;MAAA,CAAC,cAAc,CACf;MAAA,CAAC,oBAAK,CAAC,MAAM,CACX;QAAA,CAAC,WAAW,CAAC,AAAD,EACd;MAAA,EAAE,oBAAK,CAAC,MAAM,CACd;MAAA,CAAC,iBAAiB,CACpB;IAAA,EAAE,oBAAK,CAAC,IAAI,CAAC,CACd,CAAC;AACJ,CAAC;AAEY,QAAA,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,kBAAkB,EAAE;IACzD,MAAM,EAAE,0BAAe;IACvB,SAAS,EAAE,6BAAkB;CAC9B,CAAC,CAAC","sourcesContent":["import React, { createContext, isValidElement, use, type ReactNode } from 'react';\nimport { Split, type SplitHostProps } from 'react-native-screens/experimental';\n\nimport { SplitViewColumn, SplitViewInspector } from './elements';\nimport { IsWithinLayoutContext } from '../layouts/IsWithinLayoutContext';\nimport { Slot } from '../views/Navigator';\n\nconst IsWithinSplitViewContext = createContext(false);\n\n/**\n * For full list of supported props, see [`SplitHostProps`](http://github.com/software-mansion/react-native-screens/blob/main/src/components/gamma/split/SplitHost.types.ts#L117)\n */\nexport interface SplitViewProps extends Omit {\n children?: ReactNode;\n}\n\nfunction SplitViewNavigator({ children, ...splitViewHostProps }: SplitViewProps) {\n if (use(IsWithinSplitViewContext)) {\n throw new Error('There can only be one SplitView in the navigation hierarchy.');\n }\n\n // TODO: Add better way of detecting if SplitView is rendered inside Native navigator.\n if (use(IsWithinLayoutContext)) {\n throw new Error('SplitView cannot be used inside another navigator, except for Slot.');\n }\n\n if (process.env.EXPO_OS !== 'ios') {\n console.warn(\n 'SplitView is only supported on iOS. The SplitView will behave like a Slot navigator on other platforms.'\n );\n return ;\n }\n\n const WrappedSlot = () => (\n \n \n \n );\n\n const allChildrenArray = React.Children.toArray(children);\n const columnChildren = allChildrenArray.filter(\n (child) => isValidElement(child) && child.type === SplitViewColumn\n );\n const inspectorChildren = allChildrenArray.filter(\n (child) => isValidElement(child) && child.type === SplitViewInspector\n );\n const numberOfSidebars = columnChildren.length;\n const numberOfInspectors = inspectorChildren.length;\n\n if (allChildrenArray.length !== columnChildren.length + inspectorChildren.length) {\n console.warn(\n 'Only SplitView.Column and SplitView.Inspector components are allowed as direct children of SplitView.'\n );\n }\n\n if (numberOfSidebars > 2) {\n throw new Error('There can only be two SplitView.Column in the SplitView.');\n }\n\n if (numberOfSidebars + numberOfInspectors === 0) {\n console.warn('No SplitView.Column and SplitView.Inspector found in SplitView.');\n return ;\n }\n\n // The key is needed, because number of columns cannot be changed dynamically\n return (\n \n {columnChildren}\n \n \n \n {inspectorChildren}\n \n );\n}\n\nexport const SplitView = Object.assign(SplitViewNavigator, {\n Column: SplitViewColumn,\n Inspector: SplitViewInspector,\n});\n"]} \ No newline at end of file diff --git a/packages/expo-router/src/loaders/__tests__/utils.test.web.ts b/packages/expo-router/src/loaders/__tests__/utils.test.web.ts new file mode 100644 index 00000000000000..417e3e3f75226e --- /dev/null +++ b/packages/expo-router/src/loaders/__tests__/utils.test.web.ts @@ -0,0 +1,35 @@ +import { getLoaderModulePath } from '../utils'; + +describe(getLoaderModulePath, () => { + it('converts root path to /_expo/loaders/index', () => { + expect(getLoaderModulePath('/')).toBe('/_expo/loaders/index'); + }); + + it('converts paths without trailing slash', () => { + expect(getLoaderModulePath('/about')).toBe('/_expo/loaders/about'); + }); + + it('strips trailing slashes', () => { + expect(getLoaderModulePath('/about/')).toBe('/_expo/loaders/about'); + }); + + it('handles nested paths', () => { + expect(getLoaderModulePath('/posts/123')).toBe('/_expo/loaders/posts/123'); + }); + + it('preserves query parameters', () => { + expect(getLoaderModulePath('/request?foo=bar')).toBe('/_expo/loaders/request?foo=bar'); + }); + + it('preserves query parameters on root path', () => { + expect(getLoaderModulePath('/?foo=bar')).toBe('/_expo/loaders/index?foo=bar'); + }); + + it('preserves multiple query parameters', () => { + expect(getLoaderModulePath('/request?a=1&b=2')).toBe('/_expo/loaders/request?a=1&b=2'); + }); + + it('preserves query parameters with trailing slash', () => { + expect(getLoaderModulePath('/about/?foo=bar')).toBe('/_expo/loaders/about?foo=bar'); + }); +}); diff --git a/packages/expo-router/src/loaders/utils.ts b/packages/expo-router/src/loaders/utils.ts index 1b30e185f060d9..371b4151ad7aa9 100644 --- a/packages/expo-router/src/loaders/utils.ts +++ b/packages/expo-router/src/loaders/utils.ts @@ -8,20 +8,23 @@ import { parseUrlUsingCustomBase } from '../utils/url'; * getLoaderModulePath(`/about`) // `/_expo/loaders/about` * getLoaderModulePath(`/posts/1`) // `/_expo/loaders/posts/1` */ -export function getLoaderModulePath(pathname: string): string { - const urlPath = parseUrlUsingCustomBase(pathname).pathname; - const normalizedPath = urlPath === '/' ? '/' : urlPath.replace(/\/$/, ''); +export function getLoaderModulePath(routePath: string): string { + const { pathname, search } = parseUrlUsingCustomBase(routePath); + const normalizedPath = pathname === '/' ? '/' : pathname.replace(/\/$/, ''); const pathSegment = normalizedPath === '/' ? '/index' : normalizedPath; - return `/_expo/loaders${pathSegment}`; + return `/_expo/loaders${pathSegment}${search}`; } /** * Fetches and parses a loader module from the given route path. * This works in all environments including: - * 1. Development with Metro dev server (see `LoaderModuleMiddleware`) + * 1. Development with Metro dev server * 2. Production with static files (SSG) * 3. SSR environments + * + * @see import('packages/@expo/cli/src/start/server/metro/createServerRouteMiddleware.ts').createRouteHandlerMiddleware + * @see import('packages/expo-server/src/vendor/environment/common.ts').createEnvironment */ export async function fetchLoaderModule(routePath: string): Promise { const loaderPath = getLoaderModulePath(routePath); diff --git a/packages/expo-router/src/native-tabs/NativeTabsView.tsx b/packages/expo-router/src/native-tabs/NativeTabsView.tsx index 66d1de9d7b8104..daa80ea1d6b045 100644 --- a/packages/expo-router/src/native-tabs/NativeTabsView.tsx +++ b/packages/expo-router/src/native-tabs/NativeTabsView.tsx @@ -1,12 +1,7 @@ import { useTheme } from '@react-navigation/native'; import React, { useDeferredValue, useMemo } from 'react'; import { View, type ColorValue } from 'react-native'; -import { - BottomTabs, - BottomTabsScreen, - type BottomTabsProps, - type BottomTabsScreenAppearance, -} from 'react-native-screens'; +import { Tabs, type TabsHostProps, type TabsScreenAppearance } from 'react-native-screens'; import { SafeAreaView } from 'react-native-screens/experimental'; import { @@ -80,7 +75,7 @@ export function NativeTabsView(props: NativeTabsViewProps) { }); const currentTabAppearance = appearances[inBoundsDeferredFocusedIndex]?.standardAppearance; - const tabBarControllerMode: BottomTabsProps['tabBarControllerMode'] = sidebarAdaptable + const tabBarControllerMode: TabsHostProps['tabBarControllerMode'] = sidebarAdaptable ? 'tabSidebar' : sidebarAdaptable === false ? 'tabBar' @@ -100,7 +95,7 @@ export function NativeTabsView(props: NativeTabsViewProps) { : undefined; return ( - {children} - + ); } @@ -157,8 +152,8 @@ function Screen(props: { name: string; isFocused: boolean; options: NativeTabOptions; - standardAppearance: BottomTabsScreenAppearance; - scrollEdgeAppearance: BottomTabsScreenAppearance; + standardAppearance: TabsScreenAppearance; + scrollEdgeAppearance: TabsScreenAppearance; badgeTextColor: ColorValue | undefined; contentRenderer: () => React.ReactNode; }) { @@ -205,7 +200,7 @@ function Screen(props: { ); return ( - {wrappedContent} - + ); } @@ -232,7 +227,7 @@ const supportedTabBarItemLabelVisibilityModesSet = new Set( SUPPORTED_TAB_BAR_ITEM_LABEL_VISIBILITY_MODES ); -function BottomTabsWrapper(props: BottomTabsProps) { +function TabsHostWrapper(props: TabsHostProps) { let { tabBarMinimizeBehavior, tabBarItemLabelVisibilityMode, ...rest } = props; if (tabBarMinimizeBehavior && !supportedTabBarMinimizeBehaviorsSet.has(tabBarMinimizeBehavior)) { console.warn( @@ -251,7 +246,7 @@ function BottomTabsWrapper(props: BottomTabsProps) { } return ( - { const { View }: typeof import('react-native') = jest.requireActual('react-native'); + const actualModule = jest.requireActual( + 'react-native-screens' + ) as typeof import('react-native-screens'); return { - ...(jest.requireActual('react-native-screens') as typeof import('react-native-screens')), - BottomTabs: jest.fn(({ children }) => {children}), - BottomTabsScreen: jest.fn(({ children }) => {children}), + ...actualModule, + Tabs: { + ...actualModule.Tabs, + Host: jest.fn(({ children }) => {children}), + Screen: jest.fn(({ children }) => {children}), + }, }; }); -const BottomTabs = _BottomTabs as jest.MockedFunction; -const BottomTabsScreen = _BottomTabsScreen as jest.MockedFunction; +const TabsHost = Tabs.Host as jest.MockedFunction; +const TabsScreen = Tabs.Screen as jest.MockedFunction; it.each([ { value: undefined, expected: 'automatic' }, @@ -32,7 +30,7 @@ it.each([ { value: false, expected: 'tabBar' }, ] as { value: NativeTabsProps['sidebarAdaptable']; - expected: BottomTabsProps['tabBarControllerMode']; + expected: TabsHostProps['tabBarControllerMode']; }[])('when sidebarAdaptable is $value, then ', ({ value, expected }) => { renderRouter({ _layout: () => ( @@ -44,8 +42,8 @@ it.each([ }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabs).toHaveBeenCalledTimes(1); - expect(BottomTabs.mock.calls[0][0].tabBarControllerMode).toBe(expected); + expect(TabsHost).toHaveBeenCalledTimes(1); + expect(TabsHost.mock.calls[0][0].tabBarControllerMode).toBe(expected); }); it('uses shadowColor when it is passed to NativeTabs', () => { @@ -59,11 +57,9 @@ it('uses shadowColor when it is passed to NativeTabs', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].standardAppearance.tabBarShadowColor).toBe('red'); - expect(BottomTabsScreen.mock.calls[0][0].scrollEdgeAppearance.tabBarShadowColor).toBe( - 'transparent' - ); + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].standardAppearance.tabBarShadowColor).toBe('red'); + expect(TabsScreen.mock.calls[0][0].scrollEdgeAppearance.tabBarShadowColor).toBe('transparent'); }); it('uses shadowColor when it is passed to NativeTabs in both standardAppearance and scrollEdgeAppearance when disableTransparentOnScrollEdge is true', () => { @@ -77,7 +73,7 @@ it('uses shadowColor when it is passed to NativeTabs in both standardAppearance }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].standardAppearance.tabBarShadowColor).toBe('red'); - expect(BottomTabsScreen.mock.calls[0][0].scrollEdgeAppearance.tabBarShadowColor).toBe('red'); + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].standardAppearance.tabBarShadowColor).toBe('red'); + expect(TabsScreen.mock.calls[0][0].scrollEdgeAppearance.tabBarShadowColor).toBe('red'); }); diff --git a/packages/expo-router/src/native-tabs/__tests__/appearance.test.ios.tsx b/packages/expo-router/src/native-tabs/__tests__/appearance.test.ios.tsx index b9f0638ae745ea..feeee8c0d89faf 100644 --- a/packages/expo-router/src/native-tabs/__tests__/appearance.test.ios.tsx +++ b/packages/expo-router/src/native-tabs/__tests__/appearance.test.ios.tsx @@ -1,7 +1,7 @@ import type { - BottomTabsScreenAppearance, - BottomTabsScreenItemAppearance, - BottomTabsScreenItemStateAppearance, + TabsScreenAppearance, + TabsScreenItemAppearance, + TabsScreenItemStateAppearance, } from 'react-native-screens'; import { @@ -18,13 +18,13 @@ describe(createStandardAppearanceFromOptions, () => { it('empty options should create empty appearance', () => { const options: NativeTabOptions = {}; const result = createStandardAppearanceFromOptions(options); - const expectedItemAppearance: BottomTabsScreenItemAppearance = { + const expectedItemAppearance: TabsScreenItemAppearance = { normal: {}, selected: {}, disabled: {}, focused: {}, }; - const expectedAppearance: BottomTabsScreenAppearance = { + const expectedAppearance: TabsScreenAppearance = { stacked: expectedItemAppearance, inline: expectedItemAppearance, compactInline: expectedItemAppearance, @@ -53,7 +53,7 @@ describe(createStandardAppearanceFromOptions, () => { const result = createStandardAppearanceFromOptions(options); - const expectedItemAppearance: BottomTabsScreenItemAppearance = { + const expectedItemAppearance: TabsScreenItemAppearance = { normal: { tabBarItemBadgeBackgroundColor: 'green', tabBarItemIconColor: 'red', @@ -74,7 +74,7 @@ describe(createStandardAppearanceFromOptions, () => { }, disabled: {}, }; - const expectedAppearance: BottomTabsScreenAppearance = { + const expectedAppearance: TabsScreenAppearance = { stacked: expectedItemAppearance, inline: expectedItemAppearance, compactInline: expectedItemAppearance, @@ -92,13 +92,13 @@ describe(createScrollEdgeAppearanceFromOptions, () => { const options: NativeTabOptions = {}; const baseAppearance = {}; const result = createScrollEdgeAppearanceFromOptions(options, baseAppearance); - const expectedItemAppearance: BottomTabsScreenItemAppearance = { + const expectedItemAppearance: TabsScreenItemAppearance = { normal: {}, selected: {}, disabled: {}, focused: {}, }; - const expectedAppearance: BottomTabsScreenAppearance = { + const expectedAppearance: TabsScreenAppearance = { stacked: expectedItemAppearance, inline: expectedItemAppearance, compactInline: expectedItemAppearance, @@ -129,7 +129,7 @@ describe(createScrollEdgeAppearanceFromOptions, () => { }; const result = createScrollEdgeAppearanceFromOptions(options); - const expectedItemAppearance: BottomTabsScreenItemAppearance = { + const expectedItemAppearance: TabsScreenItemAppearance = { normal: { tabBarItemBadgeBackgroundColor: 'green', tabBarItemIconColor: 'red', @@ -150,7 +150,7 @@ describe(createScrollEdgeAppearanceFromOptions, () => { }, disabled: {}, }; - const expectedAppearance: BottomTabsScreenAppearance = { + const expectedAppearance: TabsScreenAppearance = { stacked: expectedItemAppearance, inline: expectedItemAppearance, compactInline: expectedItemAppearance, @@ -168,7 +168,7 @@ describe(appendStyleToAppearance, () => { [['normal']], [['normal', 'focused']], [['normal', 'focused', 'selected']], - ] as (keyof BottomTabsScreenItemAppearance)[][][])('for states %p', (states) => { + ] as (keyof TabsScreenItemAppearance)[][][])('for states %p', (states) => { it.each([ [ { @@ -214,15 +214,12 @@ describe(appendStyleToAppearance, () => { }, }, ], - ] as [BottomTabsScreenAppearance][])( - 'empty style should not change appearance %p', - (appearance) => { - const result = appendStyleToAppearance({}, appearance, states); - expect(result).toEqual(appearance); - } - ); + ] as [TabsScreenAppearance][])('empty style should not change appearance %p', (appearance) => { + const result = appendStyleToAppearance({}, appearance, states); + expect(result).toEqual(appearance); + }); it('should append style correctly', () => { - const item: BottomTabsScreenItemAppearance = { + const item: TabsScreenItemAppearance = { normal: { tabBarItemIconColor: '#f00', tabBarItemBadgeBackgroundColor: '#0f0', @@ -234,7 +231,7 @@ describe(appendStyleToAppearance, () => { focused: {}, disabled: {}, }; - const appearance: BottomTabsScreenAppearance = { + const appearance: TabsScreenAppearance = { stacked: item, inline: item, compactInline: item, @@ -273,7 +270,7 @@ describe(appendStyleToAppearance, () => { ...newStateAppearance, } : item.focused; - const expectedItem: BottomTabsScreenItemAppearance = { + const expectedItem: TabsScreenItemAppearance = { normal, selected, focused, @@ -304,7 +301,7 @@ describe(appendStyleToAppearance, () => { const normal = states.includes('normal') ? newStateAppearance : {}; const selected = states.includes('selected') ? newStateAppearance : {}; const focused = states.includes('focused') ? newStateAppearance : {}; - const expectedItem: BottomTabsScreenItemAppearance = { + const expectedItem: TabsScreenItemAppearance = { normal, selected, focused, @@ -327,7 +324,7 @@ describe(convertStyleToAppearance, () => { blurEffect: 'light', fontFamily: 'Arial', }; - const expected: BottomTabsScreenItemStateAppearance = { + const expected: TabsScreenItemStateAppearance = { tabBarItemTitleFontFamily: 'Arial', }; const result = convertStyleToAppearance(style); @@ -345,7 +342,7 @@ describe(convertStyleToAppearance, () => { tabBarBlurEffect: style.blurEffect, }); }); - const cases: [AppearanceStyle, BottomTabsScreenItemStateAppearance][] = [ + const cases: [AppearanceStyle, TabsScreenItemStateAppearance][] = [ [{}, {}], [{ fontFamily: 'xxx' }, { tabBarItemTitleFontFamily: 'xxx' }], [{ fontSize: 16 }, { tabBarItemTitleFontSize: 16 }], @@ -424,7 +421,7 @@ describe(convertStyleToAppearance, () => { }); }); describe(convertStyleToItemStateAppearance, () => { - const cases: [AppearanceStyle, BottomTabsScreenItemStateAppearance][] = [ + const cases: [AppearanceStyle, TabsScreenItemStateAppearance][] = [ [{}, {}], [{ backgroundColor: '#fff' }, {}], [{ blurEffect: 'none' }, {}], diff --git a/packages/expo-router/src/native-tabs/__tests__/events.test.ios.tsx b/packages/expo-router/src/native-tabs/__tests__/events.test.ios.tsx index 3272de0c6f0704..650a08d91d77fd 100644 --- a/packages/expo-router/src/native-tabs/__tests__/events.test.ios.tsx +++ b/packages/expo-router/src/native-tabs/__tests__/events.test.ios.tsx @@ -2,9 +2,8 @@ import { screen } from '@testing-library/react-native'; import React from 'react'; import { View, type NativeSyntheticEvent } from 'react-native'; import { - BottomTabsScreen as _BottomTabsScreen, - BottomTabs as _BottomTabs, - type BottomTabsProps, + Tabs, + type TabsHostProps, // @ts-expect-error: method is declared in mock below __triggerNativeFocusChange, } from 'react-native-screens'; @@ -17,25 +16,31 @@ import { NativeTabs } from '../NativeTabs'; jest.mock('react-native-screens', () => { const { View }: typeof import('react-native') = jest.requireActual('react-native'); - let triggerNativeFocusChange: BottomTabsProps['onNativeFocusChange'] = () => {}; + const actualModule = jest.requireActual( + 'react-native-screens' + ) as typeof import('react-native-screens'); + let triggerNativeFocusChange: TabsHostProps['onNativeFocusChange'] = () => {}; return { - ...(jest.requireActual('react-native-screens') as typeof import('react-native-screens')), - BottomTabs: jest.fn(({ children, onNativeFocusChange }) => { - triggerNativeFocusChange = onNativeFocusChange || (() => {}); - return {children}; - }), - BottomTabsScreen: jest.fn(({ children }) => {children}), - __triggerNativeFocusChange: (event: Parameters[0]) => + ...actualModule, + Tabs: { + ...actualModule.Tabs, + Host: jest.fn(({ children, onNativeFocusChange }) => { + triggerNativeFocusChange = onNativeFocusChange || (() => {}); + return {children}; + }), + Screen: jest.fn(({ children }) => {children}), + }, + __triggerNativeFocusChange: (event: Parameters[0]) => triggerNativeFocusChange(event), }; }); -const triggerNativeFocusChange: BottomTabsProps['onNativeFocusChange'] = (...args) => +const triggerNativeFocusChange: TabsHostProps['onNativeFocusChange'] = (...args) => act(() => { __triggerNativeFocusChange(...args); }); -const BottomTabsScreen = _BottomTabsScreen as jest.MockedFunction; +const TabsScreen = Tabs.Screen as jest.MockedFunction; const warn = jest.fn(); const error = jest.fn(); @@ -88,12 +93,12 @@ it('emits tabPress event onNativeFocusChange', () => { expect(screen.getByTestId('index')).toBeVisible(); expect(screen.getByTestId('second')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0].tabKey).toMatch(/index-[-\w]+/); - expect(BottomTabsScreen.mock.calls[1][0].tabKey).toMatch(/second-[-\w]+/); + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0].tabKey).toMatch(/index-[-\w]+/); + expect(TabsScreen.mock.calls[1][0].tabKey).toMatch(/second-[-\w]+/); - const indexTabKey = BottomTabsScreen.mock.calls[0][0].tabKey; - const secondTabKey = BottomTabsScreen.mock.calls[1][0].tabKey; + const indexTabKey = TabsScreen.mock.calls[0][0].tabKey; + const secondTabKey = TabsScreen.mock.calls[1][0].tabKey; expect(indexTabPressHandler).toHaveBeenCalledTimes(0); expect(secondTabPressHandler).toHaveBeenCalledTimes(0); @@ -101,8 +106,9 @@ it('emits tabPress event onNativeFocusChange', () => { triggerNativeFocusChange({ nativeEvent: { tabKey: indexTabKey, + repeatedSelectionHandledBySpecialEffect: false, }, - } as NativeSyntheticEvent<{ tabKey: string }>); + } as NativeSyntheticEvent<{ tabKey: string; repeatedSelectionHandledBySpecialEffect: boolean }>); act(() => jest.runAllTimers()); @@ -114,8 +120,9 @@ it('emits tabPress event onNativeFocusChange', () => { triggerNativeFocusChange({ nativeEvent: { tabKey: secondTabKey, + repeatedSelectionHandledBySpecialEffect: false, }, - } as NativeSyntheticEvent<{ tabKey: string }>); + } as NativeSyntheticEvent<{ tabKey: string; repeatedSelectionHandledBySpecialEffect: boolean }>); act(() => jest.runAllTimers()); @@ -127,8 +134,9 @@ it('emits tabPress event onNativeFocusChange', () => { triggerNativeFocusChange({ nativeEvent: { tabKey: secondTabKey, + repeatedSelectionHandledBySpecialEffect: false, }, - } as NativeSyntheticEvent<{ tabKey: string }>); + } as NativeSyntheticEvent<{ tabKey: string; repeatedSelectionHandledBySpecialEffect: boolean }>); act(() => jest.runAllTimers()); @@ -183,12 +191,12 @@ it('does not pop stack on repeated tab press', async () => { expect(screen.getByTestId('index')).toBeVisible(); expect(screen.getByTestId('a-index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0].tabKey).toMatch(/index-[-\w]+/); - expect(BottomTabsScreen.mock.calls[1][0].tabKey).toMatch(/a-[-\w]+/); + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0].tabKey).toMatch(/index-[-\w]+/); + expect(TabsScreen.mock.calls[1][0].tabKey).toMatch(/a-[-\w]+/); - const indexTabKey = BottomTabsScreen.mock.calls[0][0].tabKey; - const aTabKey = BottomTabsScreen.mock.calls[1][0].tabKey; + const indexTabKey = TabsScreen.mock.calls[0][0].tabKey; + const aTabKey = TabsScreen.mock.calls[1][0].tabKey; expect(indexTabPressHandler).toHaveBeenCalledTimes(0); expect(aIndexTabPressHandler).toHaveBeenCalledTimes(0); @@ -197,8 +205,9 @@ it('does not pop stack on repeated tab press', async () => { triggerNativeFocusChange({ nativeEvent: { tabKey: indexTabKey, + repeatedSelectionHandledBySpecialEffect: false, }, - } as NativeSyntheticEvent<{ tabKey: string }>); + } as NativeSyntheticEvent<{ tabKey: string; repeatedSelectionHandledBySpecialEffect: boolean }>); act(() => jest.runAllTimers()); @@ -210,8 +219,9 @@ it('does not pop stack on repeated tab press', async () => { triggerNativeFocusChange({ nativeEvent: { tabKey: aTabKey, + repeatedSelectionHandledBySpecialEffect: false, }, - } as NativeSyntheticEvent<{ tabKey: string }>); + } as NativeSyntheticEvent<{ tabKey: string; repeatedSelectionHandledBySpecialEffect: boolean }>); act(() => jest.runAllTimers()); @@ -231,8 +241,9 @@ it('does not pop stack on repeated tab press', async () => { triggerNativeFocusChange({ nativeEvent: { tabKey: aTabKey, + repeatedSelectionHandledBySpecialEffect: true, }, - } as NativeSyntheticEvent<{ tabKey: string }>); + } as NativeSyntheticEvent<{ tabKey: string; repeatedSelectionHandledBySpecialEffect: boolean }>); act(() => jest.runAllTimers()); diff --git a/packages/expo-router/src/native-tabs/__tests__/navigation.test.ios.tsx b/packages/expo-router/src/native-tabs/__tests__/navigation.test.ios.tsx index fd219d90b17c52..3b769f8c24ac1a 100644 --- a/packages/expo-router/src/native-tabs/__tests__/navigation.test.ios.tsx +++ b/packages/expo-router/src/native-tabs/__tests__/navigation.test.ios.tsx @@ -1,7 +1,7 @@ import { screen, fireEvent } from '@testing-library/react-native'; -import React, { act } from 'react'; +import { act } from 'react'; import { View } from 'react-native'; -import { BottomTabsScreen as _BottomTabsScreen } from 'react-native-screens'; +import { Tabs } from 'react-native-screens'; import { router } from '../../imperative-api'; import { Link } from '../../link/Link'; @@ -10,44 +10,46 @@ import { NativeTabs } from '../NativeTabs'; jest.mock('react-native-screens', () => { const { View }: typeof import('react-native') = jest.requireActual('react-native'); + const actualModule = jest.requireActual( + 'react-native-screens' + ) as typeof import('react-native-screens'); return { - ...(jest.requireActual('react-native-screens') as typeof import('react-native-screens')), - BottomTabs: jest.fn(({ children }) => {children}), - BottomTabsScreen: jest.fn(({ children }) => {children}), + ...actualModule, + Tabs: { + ...actualModule.Tabs, + Host: jest.fn(({ children }) => {children}), + Screen: jest.fn(({ children }) => {children}), + }, }; }); -const BottomTabsScreen = _BottomTabsScreen as jest.MockedFunction; +const TabsScreen = Tabs.Screen as jest.MockedFunction; describe('Native Bottom Tabs Navigation', () => { function expectNoRenders() { - expect(BottomTabsScreen).not.toHaveBeenCalled(); + expect(TabsScreen).not.toHaveBeenCalled(); } function expectOneRender() { - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen).toHaveBeenCalledTimes(2); } function expectTwoRenders() { - expect(BottomTabsScreen).toHaveBeenCalledTimes(4); + expect(TabsScreen).toHaveBeenCalledTimes(4); } function expectIndexTabFocused(renderNumber = 1) { - expect(BottomTabsScreen.mock.calls[(renderNumber - 1) * 2][0].tabKey).toMatch(/^index-[-\w]+/); - expect(BottomTabsScreen.mock.calls[(renderNumber - 1) * 2][0].isFocused).toBe(true); - expect(BottomTabsScreen.mock.calls[(renderNumber - 1) * 2 + 1][0].tabKey).toMatch( - /^second-[-\w]+/ - ); - expect(BottomTabsScreen.mock.calls[(renderNumber - 1) * 2 + 1][0].isFocused).toBe(false); + expect(TabsScreen.mock.calls[(renderNumber - 1) * 2][0].tabKey).toMatch(/^index-[-\w]+/); + expect(TabsScreen.mock.calls[(renderNumber - 1) * 2][0].isFocused).toBe(true); + expect(TabsScreen.mock.calls[(renderNumber - 1) * 2 + 1][0].tabKey).toMatch(/^second-[-\w]+/); + expect(TabsScreen.mock.calls[(renderNumber - 1) * 2 + 1][0].isFocused).toBe(false); } function expectSecondTabFocused(renderNumber = 1) { - expect(BottomTabsScreen.mock.calls[(renderNumber - 1) * 2][0].tabKey).toMatch(/^index-[-\w]+/); - expect(BottomTabsScreen.mock.calls[(renderNumber - 1) * 2][0].isFocused).toBe(false); - expect(BottomTabsScreen.mock.calls[(renderNumber - 1) * 2 + 1][0].tabKey).toMatch( - /^second-[-\w]+/ - ); - expect(BottomTabsScreen.mock.calls[(renderNumber - 1) * 2 + 1][0].isFocused).toBe(true); + expect(TabsScreen.mock.calls[(renderNumber - 1) * 2][0].tabKey).toMatch(/^index-[-\w]+/); + expect(TabsScreen.mock.calls[(renderNumber - 1) * 2][0].isFocused).toBe(false); + expect(TabsScreen.mock.calls[(renderNumber - 1) * 2 + 1][0].tabKey).toMatch(/^second-[-\w]+/); + expect(TabsScreen.mock.calls[(renderNumber - 1) * 2 + 1][0].isFocused).toBe(true); } beforeEach(() => { @@ -80,14 +82,14 @@ describe('Native Bottom Tabs Navigation', () => { }); expectOneRender(); expectIndexTabFocused(); - BottomTabsScreen.mockClear(); + TabsScreen.mockClear(); }); it('can navigate using router.push', () => { act(() => router.push('/second')); expectTwoRenders(); expectSecondTabFocused(2); - BottomTabsScreen.mockClear(); + TabsScreen.mockClear(); act(() => router.push('/')); expectTwoRenders(); expectIndexTabFocused(2); @@ -100,7 +102,7 @@ describe('Native Bottom Tabs Navigation', () => { // Second one is deferred index=1, index =1 expectTwoRenders(); expectSecondTabFocused(2); - BottomTabsScreen.mockClear(); + TabsScreen.mockClear(); act(() => fireEvent.press(screen.getByTestId('second-index-link'))); expectTwoRenders(); expectIndexTabFocused(2); @@ -117,11 +119,11 @@ describe('Native Bottom Tabs Navigation', () => { expectOneRender(); expectIndexTabFocused(); - BottomTabsScreen.mockClear(); + TabsScreen.mockClear(); act(() => router.push('/second')); expectSecondTabFocused(2); - BottomTabsScreen.mockClear(); + TabsScreen.mockClear(); act(() => fireEvent.press(screen.getByTestId('second-second-link'))); // link to same tab expectOneRender(); expectSecondTabFocused(); @@ -131,11 +133,11 @@ describe('Native Bottom Tabs Navigation', () => { act(() => fireEvent.press(screen.getByTestId('index-hidden-link'))); expectNoRenders(); - BottomTabsScreen.mockClear(); + TabsScreen.mockClear(); act(() => router.push('/second')); expectSecondTabFocused(2); - BottomTabsScreen.mockClear(); + TabsScreen.mockClear(); act(() => fireEvent.press(screen.getByTestId('second-hidden-link'))); expectNoRenders(); }); @@ -144,11 +146,11 @@ describe('Native Bottom Tabs Navigation', () => { act(() => fireEvent.press(screen.getByTestId('index-not-specified-link'))); expectNoRenders(); - BottomTabsScreen.mockClear(); + TabsScreen.mockClear(); act(() => router.push('/second')); expectSecondTabFocused(); - BottomTabsScreen.mockClear(); + TabsScreen.mockClear(); act(() => fireEvent.press(screen.getByTestId('second-hidden-link'))); expectNoRenders(); }); diff --git a/packages/expo-router/src/native-tabs/__tests__/options.e2e.test.android.tsx b/packages/expo-router/src/native-tabs/__tests__/options.e2e.test.android.tsx index f2872774149030..a441da5db2d91e 100644 --- a/packages/expo-router/src/native-tabs/__tests__/options.e2e.test.android.tsx +++ b/packages/expo-router/src/native-tabs/__tests__/options.e2e.test.android.tsx @@ -1,12 +1,7 @@ import { screen, act, fireEvent, waitFor } from '@testing-library/react-native'; import { Children, isValidElement, useState, type ComponentProps, type ReactNode } from 'react'; import { Button, Text, View } from 'react-native'; -import { - BottomTabs as _BottomTabs, - BottomTabsScreen as _BottomTabsScreen, - type BottomTabsProps, - type BottomTabsScreenProps, -} from 'react-native-screens'; +import { Tabs, type TabsHostProps, type TabsScreenProps } from 'react-native-screens'; import { SafeAreaView } from 'react-native-screens/experimental'; import type { ColorType } from '../../color'; @@ -38,10 +33,16 @@ jest.mock('../../color', (): typeof import('../../color') => ({ jest.mock('react-native-screens', () => { const { View }: typeof import('react-native') = jest.requireActual('react-native'); + const actualModule = jest.requireActual( + 'react-native-screens' + ) as typeof import('react-native-screens'); return { - ...(jest.requireActual('react-native-screens') as typeof import('react-native-screens')), - BottomTabs: jest.fn(({ children }) => {children}), - BottomTabsScreen: jest.fn(({ children }) => {children}), + ...actualModule, + Tabs: { + ...actualModule.Tabs, + Host: jest.fn(({ children }) => {children}), + Screen: jest.fn(({ children }) => {children}), + }, }; }); jest.mock('react-native-screens/experimental', () => { @@ -57,8 +58,8 @@ jest.mock('react-native-screens/experimental', () => { }; }); -const BottomTabs = _BottomTabs as jest.MockedFunction; -const BottomTabsScreen = _BottomTabsScreen as jest.MockedFunction; +const TabsHost = Tabs.Host as jest.MockedFunction; +const TabsScreen = Tabs.Screen as jest.MockedFunction; it('can pass props via unstable_nativeProps', () => { const indexOptions = { @@ -82,11 +83,11 @@ it('can pass props via unstable_nativeProps', () => { expect(screen.getByTestId('index')).toBeVisible(); expect(screen.getByTestId('second')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ ...indexOptions, }); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ ...secondOptions, }); }); @@ -104,15 +105,14 @@ it('can pass options via elements', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ icon: { android: { type: 'drawableResource', name: 'test' } }, selectedIcon: undefined, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('when no options are passed, default ones are used', () => { - const { BottomTabsScreen } = require('react-native-screens'); renderRouter({ _layout: () => ( @@ -123,8 +123,8 @@ it('when no options are passed, default ones are used', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ hidden: false, specialEffects: {}, tabKey: expect.stringMatching(/^index-[-\w]+/), @@ -133,7 +133,7 @@ it('when no options are passed, default ones are used', () => { icon: undefined, selectedIcon: undefined, freezeContents: false, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); describe('Icons', () => { @@ -157,10 +157,10 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ icon: { android: { type: 'drawableResource', name: 'homepod' } }, - } as BottomTabsScreenProps); + } as TabsScreenProps); expect(consoleWarnMock).not.toHaveBeenCalled(); }); @@ -177,9 +177,9 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].icon).toBeUndefined(); - expect(BottomTabsScreen.mock.calls[0][0].selectedIcon).toBeUndefined(); + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].icon).toBeUndefined(); + expect(TabsScreen.mock.calls[0][0].selectedIcon).toBeUndefined(); expect(consoleWarnMock).not.toHaveBeenCalled(); }); @@ -197,11 +197,11 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ icon: { android: { type: 'drawableResource', name: 'homepod' } }, selectedIcon: undefined, - } as BottomTabsScreenProps); + } as TabsScreenProps); expect(consoleWarnMock).not.toHaveBeenCalled(); }); @@ -215,8 +215,8 @@ describe('Icons', () => { index: () => , }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ standardAppearance: { stacked: { selected: { @@ -224,7 +224,7 @@ describe('Icons', () => { }, }, }, - } as Partial); + } as Partial); expect(consoleWarnMock).not.toHaveBeenCalled(); }); @@ -242,8 +242,8 @@ describe('Icons', () => { one: () => , }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ standardAppearance: { stacked: { selected: { @@ -251,8 +251,8 @@ describe('Icons', () => { }, }, }, - } as Partial); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as Partial); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ standardAppearance: { stacked: { selected: { @@ -260,7 +260,7 @@ describe('Icons', () => { }, }, }, - } as Partial); + } as Partial); expect(consoleWarnMock).not.toHaveBeenCalled(); }); @@ -294,7 +294,7 @@ describe('Icons', () => { ] as (DrawableIcon & SrcIcon & SFSymbolIcon & { - expectedIcon: BottomTabsScreenProps['icon']['android']; + expectedIcon: TabsScreenProps['icon']['android']; })[])( 'For , icon is $expectedIcon', ({ sf, src, drawable, expectedIcon }) => { @@ -310,8 +310,8 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].icon?.android).toEqual(expectedIcon); + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].icon?.android).toEqual(expectedIcon); expect(consoleWarnMock).not.toHaveBeenCalled(); } ); @@ -344,9 +344,9 @@ describe('Icons', () => { await waitFor(() => { expect(screen.getByTestId('index')).toBeVisible(); }); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0].icon?.android).toBeUndefined(); - expect(BottomTabsScreen.mock.calls[1][0].icon?.android).toEqual({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0].icon?.android).toBeUndefined(); + expect(TabsScreen.mock.calls[1][0].icon?.android).toEqual({ // This is declared in packages/expo-font/mocks/ExpoFontUtils.ts imageSource: { height: 0, uri: '', width: 0, scale: 1 }, type: 'imageSource', @@ -371,7 +371,7 @@ describe('Icons', () => { }, ] as (MaterialIcon & DrawableIcon & { - expectedIcon: BottomTabsScreenProps['icon']['android']; + expectedIcon: TabsScreenProps['icon']['android']; numberOfRenders: number; })[])( 'For , icon will be $expectedIcon during first render and tabs will render $numberOfRenders time(s)', @@ -393,10 +393,10 @@ describe('Icons', () => { await waitFor(() => { expect(screen.getByTestId('index')).toBeVisible(); }); - expect(BottomTabsScreen).toHaveBeenCalledTimes(numberOfRenders); - expect(BottomTabsScreen.mock.calls[0][0].icon?.android).toEqual(expectedIcon); + expect(TabsScreen).toHaveBeenCalledTimes(numberOfRenders); + expect(TabsScreen.mock.calls[0][0].icon?.android).toEqual(expectedIcon); if (numberOfRenders > 1) { - expect(BottomTabsScreen.mock.calls[1][0].icon?.android).toEqual({ + expect(TabsScreen.mock.calls[1][0].icon?.android).toEqual({ // This is declared in packages/expo-font/mocks/ExpoFontUtils.ts imageSource: { height: 0, uri: '', width: 0, scale: 1 }, type: 'imageSource', @@ -421,10 +421,10 @@ describe('Badge', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ badgeValue: '5', - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('passes badge value as string', () => { @@ -440,10 +440,10 @@ describe('Badge', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ badgeValue: 'New', - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('does not pass badge when Badge is not used', () => { @@ -457,8 +457,8 @@ describe('Badge', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).not.toHaveProperty('badgeValue'); + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).not.toHaveProperty('badgeValue'); }); it('uses last Badge value when multiple are provided', () => { @@ -476,10 +476,10 @@ describe('Badge', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ badgeValue: '3', - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('when empty Badge is used, passes space to badgeValue', () => { @@ -495,8 +495,8 @@ describe('Badge', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].badgeValue).toBe(' '); // Space is used to show empty badge + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].badgeValue).toBe(' '); // Space is used to show empty badge }); it('when empty Badge is used with hidden, passes undefined to badgeValue', () => { @@ -512,8 +512,8 @@ describe('Badge', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].badgeValue).toBeUndefined(); + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].badgeValue).toBeUndefined(); }); }); @@ -531,10 +531,10 @@ describe('Label', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Custom Title', - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('when title is not set, uses the route name', () => { @@ -551,9 +551,9 @@ describe('Label', () => { expect(screen.getByTestId('index')).toBeVisible(); expect(screen.getByTestId('one')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0].title).toBe('index'); - expect(BottomTabsScreen.mock.calls[1][0].title).toBe('one'); + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0].title).toBe('index'); + expect(TabsScreen.mock.calls[1][0].title).toBe('one'); }); it('uses last Label value when multiple are provided', () => { @@ -571,10 +571,10 @@ describe('Label', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Last Title', - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('when empty Label is used, passes route name to title', () => { @@ -590,8 +590,8 @@ describe('Label', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].title).toBe('index'); // Route name is used as title when Label is empty + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].title).toBe('index'); // Route name is used as title when Label is empty }); it('when Label with hidden is used, passes empty string to title', () => { @@ -607,8 +607,8 @@ describe('Label', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].title).toBe(''); // Route name is used as title when Label is empty + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].title).toBe(''); // Route name is used as title when Label is empty }); it('when selectedLabelStyle is provided, it is passed to screen', () => { @@ -621,8 +621,8 @@ describe('Label', () => { index: () => , }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ standardAppearance: { stacked: { selected: { @@ -631,7 +631,7 @@ describe('Label', () => { }, }, }, - } as Partial); + } as Partial); }); it('when selectedLabelStyle is provided in container and tab, the tab should use the tab color', () => { @@ -648,8 +648,8 @@ describe('Label', () => { one: () => , }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ standardAppearance: { stacked: { selected: { @@ -658,8 +658,8 @@ describe('Label', () => { }, }, }, - } as Partial); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as Partial); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ standardAppearance: { stacked: { selected: { @@ -668,7 +668,7 @@ describe('Label', () => { }, }, }, - } as Partial); + } as Partial); }); }); @@ -687,14 +687,14 @@ describe('Tab options', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ specialEffects: { repeatedTabSelection: { popToRoot: false, }, }, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('When disablePopToTop is not set or false, popToRoot is true', () => { @@ -714,23 +714,23 @@ describe('Tab options', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Custom Title', specialEffects: { repeatedTabSelection: { popToRoot: true, }, }, - } as BottomTabsScreenProps); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as TabsScreenProps); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ title: 'One', specialEffects: { repeatedTabSelection: { popToRoot: true, }, }, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); }); @@ -748,14 +748,14 @@ describe('Tab options', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ specialEffects: { repeatedTabSelection: { scrollToTop: false, }, }, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('When disableScrollToTop is not set or false, scrollToTop is true', () => { @@ -775,23 +775,23 @@ describe('Tab options', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Custom Title', specialEffects: { repeatedTabSelection: { scrollToTop: true, }, }, - } as BottomTabsScreenProps); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as TabsScreenProps); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ title: 'One', specialEffects: { repeatedTabSelection: { scrollToTop: true, }, }, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); }); @@ -808,8 +808,8 @@ describe('Tab options', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - const children = BottomTabsScreen.mock.calls[0][0].children; + expect(TabsScreen).toHaveBeenCalledTimes(1); + const children = TabsScreen.mock.calls[0][0].children; expect(isValidElement(children)).toBe(true); // Type assertion to satisfy TypeScript if (!isValidElement(children)) throw new Error(); @@ -831,8 +831,8 @@ describe('Tab options', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - const children = BottomTabsScreen.mock.calls[0][0].children; + expect(TabsScreen).toHaveBeenCalledTimes(1); + const children = TabsScreen.mock.calls[0][0].children; expect(Children.count(children)).toBe(1); expect(isValidElement(children)).toBe(true); // Type assertion to satisfy TypeScript @@ -856,8 +856,8 @@ describe('Dynamic options', () => { ), }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Initial Title', hidden: false, specialEffects: {}, @@ -867,8 +867,8 @@ describe('Dynamic options', () => { freezeContents: false, icon: undefined, selectedIcon: undefined, - } as BottomTabsScreenProps); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as TabsScreenProps); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ title: 'Updated Title', hidden: false, specialEffects: {}, @@ -878,7 +878,7 @@ describe('Dynamic options', () => { freezeContents: false, icon: undefined, selectedIcon: undefined, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('unstable_nativeProps override dynamic options configuration', () => { @@ -899,8 +899,8 @@ describe('Dynamic options', () => { ), }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Initial Title', hidden: false, specialEffects: {}, @@ -909,8 +909,8 @@ describe('Dynamic options', () => { icon: undefined, selectedIcon: undefined, freezeContents: false, - } as BottomTabsScreenProps); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as TabsScreenProps); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ title: 'Initial Title', hidden: false, specialEffects: {}, @@ -925,7 +925,7 @@ describe('Dynamic options', () => { }, selectedIcon: undefined, freezeContents: false, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('can override component children from _layout with unstable_nativeProps', () => { @@ -955,8 +955,8 @@ describe('Dynamic options', () => { ), }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Initial Title', badgeValue: '3', icon: { @@ -969,8 +969,8 @@ describe('Dynamic options', () => { specialEffects: {}, tabKey: expect.stringMatching(/^index-[-\w]+/), isFocused: true, - } as BottomTabsScreenProps); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as TabsScreenProps); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ title: 'Updated Title', hidden: false, specialEffects: {}, @@ -983,7 +983,7 @@ describe('Dynamic options', () => { name: '1234', }, }, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('can override component children from _layout with dynamic children', () => { @@ -1008,8 +1008,8 @@ describe('Dynamic options', () => { ), }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Initial Title', badgeValue: '3', icon: { @@ -1022,8 +1022,8 @@ describe('Dynamic options', () => { specialEffects: {}, tabKey: expect.stringMatching(/^index-[-\w]+/), isFocused: true, - } as BottomTabsScreenProps); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as TabsScreenProps); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ title: 'Updated Title', hidden: false, specialEffects: {}, @@ -1036,7 +1036,7 @@ describe('Dynamic options', () => { name: '1234', }, }, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('can dynamically update options with state update', () => { @@ -1062,15 +1062,15 @@ describe('Dynamic options', () => { }, }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0].title).toBe('Initial Title'); - expect(BottomTabsScreen.mock.calls[1][0].title).toBe('Updated Title 0'); + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0].title).toBe('Initial Title'); + expect(TabsScreen.mock.calls[1][0].title).toBe('Updated Title 0'); act(() => fireEvent.press(screen.getByTestId('update-button'))); - expect(BottomTabsScreen).toHaveBeenCalledTimes(3); - expect(BottomTabsScreen.mock.calls[2][0].title).toBe('Updated Title 1'); + expect(TabsScreen).toHaveBeenCalledTimes(3); + expect(TabsScreen.mock.calls[2][0].title).toBe('Updated Title 1'); act(() => fireEvent.press(screen.getByTestId('update-button'))); - expect(BottomTabsScreen).toHaveBeenCalledTimes(4); - expect(BottomTabsScreen.mock.calls[3][0].title).toBe('Updated Title 2'); + expect(TabsScreen).toHaveBeenCalledTimes(4); + expect(TabsScreen.mock.calls[3][0].title).toBe('Updated Title 2'); }); it('can be used in preview', () => { @@ -1104,8 +1104,8 @@ describe('Dynamic options', () => { // Tab + preview expect(screen.getAllByTestId('second')).toHaveLength(2); expect(within(screen.getByTestId('index')).getByTestId('second')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Initial Title', hidden: false, specialEffects: {}, @@ -1114,8 +1114,8 @@ describe('Dynamic options', () => { icon: undefined, selectedIcon: undefined, freezeContents: false, - } as BottomTabsScreenProps); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as TabsScreenProps); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ title: 'Second', hidden: false, specialEffects: {}, @@ -1124,7 +1124,7 @@ describe('Dynamic options', () => { icon: undefined, selectedIcon: undefined, freezeContents: false, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); }); @@ -1140,8 +1140,8 @@ describe('Material Design 3 dynamic color defaults', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabs).toHaveBeenCalledTimes(1); - expect(BottomTabs.mock.calls[0][0]).toMatchObject({ + expect(TabsHost).toHaveBeenCalledTimes(1); + expect(TabsHost.mock.calls[0][0]).toMatchObject({ tabBarItemIconColor: 'mock-onSurfaceVariant', tabBarItemTitleFontColor: 'mock-onSurfaceVariant', tabBarItemIconColorActive: 'mock-onSecondaryContainer', @@ -1149,7 +1149,7 @@ describe('Material Design 3 dynamic color defaults', () => { tabBarBackgroundColor: 'mock-surfaceContainer', tabBarItemRippleColor: 'mock-primary', tabBarItemActiveIndicatorColor: 'mock-secondaryContainer', - } as Partial); + } as Partial); }); it('uses custom tintColor when provided, overriding Material defaults for active states', () => { @@ -1163,15 +1163,15 @@ describe('Material Design 3 dynamic color defaults', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabs).toHaveBeenCalledTimes(1); - expect(BottomTabs.mock.calls[0][0]).toMatchObject({ + expect(TabsHost).toHaveBeenCalledTimes(1); + expect(TabsHost.mock.calls[0][0]).toMatchObject({ tabBarItemIconColor: 'mock-onSurfaceVariant', tabBarItemTitleFontColor: 'mock-onSurfaceVariant', tabBarItemIconColorActive: 'red', tabBarItemTitleFontColorActive: 'red', tabBarBackgroundColor: 'mock-surfaceContainer', tabBarItemRippleColor: 'mock-primary', - } as Partial); + } as Partial); }); it('uses custom rippleColor when provided, overriding Material default', () => { @@ -1185,11 +1185,11 @@ describe('Material Design 3 dynamic color defaults', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabs).toHaveBeenCalledTimes(1); - expect(BottomTabs.mock.calls[0][0]).toMatchObject({ + expect(TabsHost).toHaveBeenCalledTimes(1); + expect(TabsHost.mock.calls[0][0]).toMatchObject({ tabBarItemRippleColor: 'blue', tabBarBackgroundColor: 'mock-surfaceContainer', - } as Partial); + } as Partial); }); it('uses appearance options when provided, overriding Material defaults', () => { @@ -1206,14 +1206,14 @@ describe('Material Design 3 dynamic color defaults', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabs).toHaveBeenCalledTimes(1); - expect(BottomTabs.mock.calls[0][0]).toMatchObject({ + expect(TabsHost).toHaveBeenCalledTimes(1); + expect(TabsHost.mock.calls[0][0]).toMatchObject({ tabBarItemIconColor: 'green', tabBarItemIconColorActive: 'yellow', tabBarItemTitleFontColor: 'purple', tabBarItemTitleFontColorActive: 'orange', tabBarBackgroundColor: 'pink', - } as Partial); + } as Partial); }); it('uses container indicatorColor when provided, overriding Material default', () => { @@ -1227,10 +1227,10 @@ describe('Material Design 3 dynamic color defaults', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabs).toHaveBeenCalledTimes(1); - expect(BottomTabs.mock.calls[0][0]).toMatchObject({ + expect(TabsHost).toHaveBeenCalledTimes(1); + expect(TabsHost.mock.calls[0][0]).toMatchObject({ tabBarItemActiveIndicatorColor: 'cyan', tabBarBackgroundColor: 'mock-surfaceContainer', - } as Partial); + } as Partial); }); }); diff --git a/packages/expo-router/src/native-tabs/__tests__/options.e2e.test.ios.tsx b/packages/expo-router/src/native-tabs/__tests__/options.e2e.test.ios.tsx index ce10612c809c7a..71feb320f95fad 100644 --- a/packages/expo-router/src/native-tabs/__tests__/options.e2e.test.ios.tsx +++ b/packages/expo-router/src/native-tabs/__tests__/options.e2e.test.ios.tsx @@ -1,10 +1,7 @@ import { screen, act, fireEvent } from '@testing-library/react-native'; import React from 'react'; import { Button, View } from 'react-native'; -import { - BottomTabsScreen as _BottomTabsScreen, - type BottomTabsScreenProps, -} from 'react-native-screens'; +import { Tabs, type TabsScreenProps } from 'react-native-screens'; import { HrefPreview } from '../../link/preview/HrefPreview'; import { renderRouter, within } from '../../testing-library'; @@ -21,14 +18,20 @@ import type { NativeTabOptions } from '../types'; jest.mock('react-native-screens', () => { const { View }: typeof import('react-native') = jest.requireActual('react-native'); + const actualModule = jest.requireActual( + 'react-native-screens' + ) as typeof import('react-native-screens'); return { - ...(jest.requireActual('react-native-screens') as typeof import('react-native-screens')), - BottomTabs: jest.fn(({ children }) => {children}), - BottomTabsScreen: jest.fn(({ children }) => {children}), + ...actualModule, + Tabs: { + ...actualModule.Tabs, + Host: jest.fn(({ children }) => {children}), + Screen: jest.fn(({ children }) => {children}), + }, }; }); -const BottomTabsScreen = _BottomTabsScreen as jest.MockedFunction; +const TabsScreen = Tabs.Screen as jest.MockedFunction; it('can pass props via unstable_nativeProps', () => { const indexOptions = { @@ -52,11 +55,11 @@ it('can pass props via unstable_nativeProps', () => { expect(screen.getByTestId('index')).toBeVisible(); expect(screen.getByTestId('second')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ ...indexOptions, }); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ ...secondOptions, }); }); @@ -74,15 +77,14 @@ it('can pass options via elements', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ icon: { ios: { type: 'sfSymbol', name: 'homepod.2.fill' } }, selectedIcon: undefined, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('when no options are passed, default ones are used', () => { - const { BottomTabsScreen } = require('react-native-screens'); renderRouter({ _layout: () => ( @@ -93,8 +95,8 @@ it('when no options are passed, default ones are used', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ hidden: false, specialEffects: {}, tabKey: expect.stringMatching(/^index-[-\w]+/), @@ -103,7 +105,7 @@ it('when no options are passed, default ones are used', () => { icon: undefined, selectedIcon: undefined, freezeContents: false, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); describe('Icons', () => { @@ -120,10 +122,10 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ icon: { ios: { type: 'sfSymbol', name: 'homepod.2.fill' } }, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('when using Icon with sf selected prop, it is passed as selected icon sfSymbolName', () => { @@ -139,10 +141,10 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ selectedIcon: { type: 'sfSymbol', name: 'homepod.2.fill' }, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('when using Icon with sf object, values are passed correctly', () => { @@ -158,11 +160,11 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ selectedIcon: { type: 'sfSymbol', name: 'star.bubble' }, icon: { ios: { type: 'sfSymbol', name: 'stairs' } }, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('when using Icon drawable on iOS, no value is passed', () => { @@ -178,9 +180,9 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].icon).toBeUndefined(); - expect(BottomTabsScreen.mock.calls[0][0].selectedIcon).toBeUndefined(); + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].icon).toBeUndefined(); + expect(TabsScreen.mock.calls[0][0].selectedIcon).toBeUndefined(); }); it('uses last Icon sf value when multiple are provided', () => { @@ -197,11 +199,11 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ icon: { ios: { type: 'sfSymbol', name: 'homepod.2.fill' } }, selectedIcon: undefined, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('uses last Icon sf selected when multiple are provided', () => { @@ -218,10 +220,10 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ selectedIcon: { type: 'sfSymbol', name: 'homepod.2.fill' }, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('uses last Icon sf for each type when multiple are provided', () => { @@ -241,11 +243,11 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ selectedIcon: undefined, icon: { ios: { type: 'sfSymbol', name: '0.circle.ar' } }, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('uses last Icon sf for each type when multiple are provided', () => { @@ -265,11 +267,11 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ selectedIcon: { type: 'sfSymbol', name: '0.circle.ar' }, icon: undefined, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('when selectedIconColor is provided, it is passed to screen', () => { @@ -282,8 +284,8 @@ describe('Icons', () => { index: () => , }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ standardAppearance: { stacked: { selected: { @@ -291,7 +293,7 @@ describe('Icons', () => { }, }, }, - } as Partial); + } as Partial); }); it('when selectedIconColor is provided in container and tab, the tab should use the tab color', () => { @@ -308,8 +310,8 @@ describe('Icons', () => { one: () => , }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ standardAppearance: { stacked: { selected: { @@ -317,8 +319,8 @@ describe('Icons', () => { }, }, }, - } as Partial); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as Partial); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ standardAppearance: { stacked: { selected: { @@ -326,7 +328,7 @@ describe('Icons', () => { }, }, }, - } as Partial); + } as Partial); }); it.each([ @@ -367,7 +369,7 @@ describe('Icons', () => { MaterialIcon & SrcIcon & SFSymbolIcon & { - expectedIcon: BottomTabsScreenProps['icon']['ios']; + expectedIcon: TabsScreenProps['icon']['ios']; })[])( 'For , icon is $expectedIcon', ({ sf, src, drawable, material, expectedIcon }) => { @@ -383,8 +385,8 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].icon?.ios).toEqual(expectedIcon); + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].icon?.ios).toEqual(expectedIcon); } ); }); @@ -403,10 +405,10 @@ describe('Badge', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ badgeValue: '5', - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('passes badge value as string', () => { @@ -422,10 +424,10 @@ describe('Badge', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ badgeValue: 'New', - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('does not pass badge when Badge is not used', () => { @@ -439,8 +441,8 @@ describe('Badge', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).not.toHaveProperty('badgeValue'); + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).not.toHaveProperty('badgeValue'); }); it('uses last Badge value when multiple are provided', () => { @@ -458,10 +460,10 @@ describe('Badge', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ badgeValue: '3', - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('when empty Badge is used, passes space to badgeValue', () => { @@ -477,8 +479,8 @@ describe('Badge', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].badgeValue).toBe(' '); // Space is used to show empty badge + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].badgeValue).toBe(' '); // Space is used to show empty badge }); it('when empty Badge is used with hidden, passes undefined to badgeValue', () => { @@ -494,8 +496,8 @@ describe('Badge', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].badgeValue).toBeUndefined(); + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].badgeValue).toBeUndefined(); }); }); @@ -513,10 +515,10 @@ describe('Label', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Custom Title', - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('when title is not set, uses the route name', () => { @@ -533,9 +535,9 @@ describe('Label', () => { expect(screen.getByTestId('index')).toBeVisible(); expect(screen.getByTestId('one')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0].title).toBe('index'); - expect(BottomTabsScreen.mock.calls[1][0].title).toBe('one'); + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0].title).toBe('index'); + expect(TabsScreen.mock.calls[1][0].title).toBe('one'); }); it('uses last Label value when multiple are provided', () => { @@ -553,10 +555,10 @@ describe('Label', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Last Title', - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('when empty Label is used, passes route name to title', () => { @@ -572,8 +574,8 @@ describe('Label', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].title).toBe('index'); // Route name is used as title when Label is empty + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].title).toBe('index'); // Route name is used as title when Label is empty }); it('when Label with hidden is used, passes empty string to title', () => { @@ -589,8 +591,8 @@ describe('Label', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].title).toBe(''); // Route name is used as title when Label is empty + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].title).toBe(''); // Route name is used as title when Label is empty }); it('when selectedLabelStyle is provided, it is passed to screen', () => { @@ -603,8 +605,8 @@ describe('Label', () => { index: () => , }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ standardAppearance: { stacked: { selected: { @@ -613,7 +615,7 @@ describe('Label', () => { }, }, }, - } as Partial); + } as Partial); }); it('when selectedLabelStyle is provided in container and tab, the tab should use the tab color', () => { @@ -630,8 +632,8 @@ describe('Label', () => { one: () => , }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ standardAppearance: { stacked: { selected: { @@ -640,8 +642,8 @@ describe('Label', () => { }, }, }, - } as Partial); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as Partial); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ standardAppearance: { stacked: { selected: { @@ -650,7 +652,7 @@ describe('Label', () => { }, }, }, - } as Partial); + } as Partial); }); }); @@ -669,14 +671,14 @@ describe('Tab options', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ specialEffects: { repeatedTabSelection: { popToRoot: false, }, }, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('When disablePopToTop is not set or false, popToRoot is true', () => { @@ -696,23 +698,23 @@ describe('Tab options', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Custom Title', specialEffects: { repeatedTabSelection: { popToRoot: true, }, }, - } as BottomTabsScreenProps); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as TabsScreenProps); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ title: 'One', specialEffects: { repeatedTabSelection: { popToRoot: true, }, }, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); }); @@ -730,14 +732,14 @@ describe('Tab options', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ specialEffects: { repeatedTabSelection: { scrollToTop: false, }, }, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('When disableScrollToTop is not set or false, scrollToTop is true', () => { @@ -757,23 +759,23 @@ describe('Tab options', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Custom Title', specialEffects: { repeatedTabSelection: { scrollToTop: true, }, }, - } as BottomTabsScreenProps); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as TabsScreenProps); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ title: 'One', specialEffects: { repeatedTabSelection: { scrollToTop: true, }, }, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it.each([true, false, undefined])( @@ -789,10 +791,10 @@ describe('Tab options', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ overrideScrollViewContentInsetAdjustmentBehavior: !value, - } as BottomTabsScreenProps); + } as TabsScreenProps); } ); }); @@ -813,8 +815,8 @@ describe('Dynamic options', () => { ), }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Initial Title', hidden: false, specialEffects: {}, @@ -824,8 +826,8 @@ describe('Dynamic options', () => { freezeContents: false, icon: undefined, selectedIcon: undefined, - } as BottomTabsScreenProps); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as TabsScreenProps); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ title: 'Updated Title', hidden: false, specialEffects: {}, @@ -835,7 +837,7 @@ describe('Dynamic options', () => { freezeContents: false, icon: undefined, selectedIcon: undefined, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('unstable_nativeProps override dynamic options configuration', () => { @@ -856,8 +858,8 @@ describe('Dynamic options', () => { ), }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Initial Title', hidden: false, specialEffects: {}, @@ -866,8 +868,8 @@ describe('Dynamic options', () => { icon: undefined, selectedIcon: undefined, freezeContents: false, - } as BottomTabsScreenProps); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as TabsScreenProps); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ title: 'Initial Title', hidden: false, specialEffects: {}, @@ -882,7 +884,7 @@ describe('Dynamic options', () => { }, selectedIcon: undefined, freezeContents: false, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('can override component children from _layout with unstable_nativeProps', () => { @@ -912,8 +914,8 @@ describe('Dynamic options', () => { ), }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Initial Title', badgeValue: '3', icon: { @@ -926,8 +928,8 @@ describe('Dynamic options', () => { specialEffects: {}, tabKey: expect.stringMatching(/^index-[-\w]+/), isFocused: true, - } as BottomTabsScreenProps); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as TabsScreenProps); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ title: 'Updated Title', hidden: false, specialEffects: {}, @@ -940,7 +942,7 @@ describe('Dynamic options', () => { name: 'homepod.2.fill', }, }, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('can override component children from _layout with dynamic children', () => { @@ -965,8 +967,8 @@ describe('Dynamic options', () => { ), }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Initial Title', badgeValue: '3', icon: { @@ -979,8 +981,8 @@ describe('Dynamic options', () => { specialEffects: {}, tabKey: expect.stringMatching(/^index-[-\w]+/), isFocused: true, - } as BottomTabsScreenProps); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as TabsScreenProps); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ title: 'Updated Title', hidden: false, specialEffects: {}, @@ -993,7 +995,7 @@ describe('Dynamic options', () => { name: 'homepod.2.fill', }, }, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); it('can dynamically update options with state update', () => { @@ -1019,15 +1021,15 @@ describe('Dynamic options', () => { }, }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0].title).toBe('Initial Title'); - expect(BottomTabsScreen.mock.calls[1][0].title).toBe('Updated Title 0'); + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0].title).toBe('Initial Title'); + expect(TabsScreen.mock.calls[1][0].title).toBe('Updated Title 0'); act(() => fireEvent.press(screen.getByTestId('update-button'))); - expect(BottomTabsScreen).toHaveBeenCalledTimes(3); - expect(BottomTabsScreen.mock.calls[2][0].title).toBe('Updated Title 1'); + expect(TabsScreen).toHaveBeenCalledTimes(3); + expect(TabsScreen.mock.calls[2][0].title).toBe('Updated Title 1'); act(() => fireEvent.press(screen.getByTestId('update-button'))); - expect(BottomTabsScreen).toHaveBeenCalledTimes(4); - expect(BottomTabsScreen.mock.calls[3][0].title).toBe('Updated Title 2'); + expect(TabsScreen).toHaveBeenCalledTimes(4); + expect(TabsScreen.mock.calls[3][0].title).toBe('Updated Title 2'); }); it('can be used in preview', () => { @@ -1061,8 +1063,8 @@ describe('Dynamic options', () => { // Tab + preview expect(screen.getAllByTestId('second')).toHaveLength(2); expect(within(screen.getByTestId('index')).getByTestId('second')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0]).toMatchObject({ + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0]).toMatchObject({ title: 'Initial Title', hidden: false, specialEffects: {}, @@ -1071,8 +1073,8 @@ describe('Dynamic options', () => { icon: undefined, selectedIcon: undefined, freezeContents: false, - } as BottomTabsScreenProps); - expect(BottomTabsScreen.mock.calls[1][0]).toMatchObject({ + } as TabsScreenProps); + expect(TabsScreen.mock.calls[1][0]).toMatchObject({ title: 'Second', hidden: false, specialEffects: {}, @@ -1081,7 +1083,7 @@ describe('Dynamic options', () => { icon: undefined, selectedIcon: undefined, freezeContents: false, - } as BottomTabsScreenProps); + } as TabsScreenProps); }); }); diff --git a/packages/expo-router/src/native-tabs/__tests__/options.test.android.tsx b/packages/expo-router/src/native-tabs/__tests__/options.test.android.tsx index 336fa9db80f4ed..f81d80e54e4d06 100644 --- a/packages/expo-router/src/native-tabs/__tests__/options.test.android.tsx +++ b/packages/expo-router/src/native-tabs/__tests__/options.test.android.tsx @@ -1,7 +1,7 @@ import { screen } from '@testing-library/react-native'; import React from 'react'; import { View } from 'react-native'; -import { BottomTabsScreen as _BottomTabsScreen } from 'react-native-screens'; +import { Tabs } from 'react-native-screens'; import { renderRouter } from '../../testing-library'; import { appendIconOptions } from '../NativeTabTrigger'; @@ -11,14 +11,20 @@ import type { NativeTabOptions } from '../types'; jest.mock('react-native-screens', () => { const { View }: typeof import('react-native') = jest.requireActual('react-native'); + const actualModule = jest.requireActual( + 'react-native-screens' + ) as typeof import('react-native-screens'); return { - ...(jest.requireActual('react-native-screens') as typeof import('react-native-screens')), - BottomTabs: jest.fn(({ children }) => {children}), - BottomTabsScreen: jest.fn(({ children }) => {children}), + ...actualModule, + Tabs: { + ...actualModule.Tabs, + Host: jest.fn(({ children }) => {children}), + Screen: jest.fn(({ children }) => {children}), + }, }; }); -const BottomTabsScreen = _BottomTabsScreen as jest.MockedFunction; +const TabsScreen = Tabs.Screen as jest.MockedFunction; describe('Icons', () => { it('passes iconResourceName when using Icon drawable on Android', () => { @@ -34,10 +40,10 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].icon.android.type).toBe('drawableResource'); - if (BottomTabsScreen.mock.calls[0][0].icon.android.type === 'drawableResource') { - expect(BottomTabsScreen.mock.calls[0][0].icon.android.name).toBe('stairs'); + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].icon.android.type).toBe('drawableResource'); + if (TabsScreen.mock.calls[0][0].icon.android.type === 'drawableResource') { + expect(TabsScreen.mock.calls[0][0].icon.android.name).toBe('stairs'); } }); @@ -56,10 +62,10 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].icon.android.type).toBe('drawableResource'); - if (BottomTabsScreen.mock.calls[0][0].icon.android.type === 'drawableResource') { - expect(BottomTabsScreen.mock.calls[0][0].icon.android.name).toBe('last'); + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].icon.android.type).toBe('drawableResource'); + if (TabsScreen.mock.calls[0][0].icon.android.type === 'drawableResource') { + expect(TabsScreen.mock.calls[0][0].icon.android.name).toBe('last'); } }); @@ -74,8 +80,8 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].icon).toBeUndefined(); + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].icon).toBeUndefined(); }); // Currently not needed. Screens does not forbid this, as Icon does not work on Android yet. @@ -108,11 +114,11 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].selectedIcon).toBeUndefined(); - expect(BottomTabsScreen.mock.calls[0][0].icon.android.type).toBe('drawableResource'); - if (BottomTabsScreen.mock.calls[0][0].icon.android.type === 'drawableResource') { - expect(BottomTabsScreen.mock.calls[0][0].icon.android.name).toBe('stairs'); + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].selectedIcon).toBeUndefined(); + expect(TabsScreen.mock.calls[0][0].icon.android.type).toBe('drawableResource'); + if (TabsScreen.mock.calls[0][0].icon.android.type === 'drawableResource') { + expect(TabsScreen.mock.calls[0][0].icon.android.name).toBe('stairs'); } }); @@ -132,11 +138,11 @@ describe('Icons', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].selectedIcon).toBeUndefined(); - expect(BottomTabsScreen.mock.calls[0][0].icon.android.type).toBe('drawableResource'); - if (BottomTabsScreen.mock.calls[0][0].icon.android.type === 'drawableResource') { - expect(BottomTabsScreen.mock.calls[0][0].icon.android.name).toBe('stairs'); + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].selectedIcon).toBeUndefined(); + expect(TabsScreen.mock.calls[0][0].icon.android.type).toBe('drawableResource'); + if (TabsScreen.mock.calls[0][0].icon.android.type === 'drawableResource') { + expect(TabsScreen.mock.calls[0][0].icon.android.name).toBe('stairs'); } }); }); diff --git a/packages/expo-router/src/native-tabs/__tests__/options.test.ios.tsx b/packages/expo-router/src/native-tabs/__tests__/options.test.ios.tsx index 61674fda2fe695..225888cafda514 100644 --- a/packages/expo-router/src/native-tabs/__tests__/options.test.ios.tsx +++ b/packages/expo-router/src/native-tabs/__tests__/options.test.ios.tsx @@ -19,10 +19,16 @@ import type { NativeTabOptions } from '../types'; jest.mock('react-native-screens', () => { const { View }: typeof import('react-native') = jest.requireActual('react-native'); + const actualModule = jest.requireActual( + 'react-native-screens' + ) as typeof import('react-native-screens'); return { - ...(jest.requireActual('react-native-screens') as typeof import('react-native-screens')), - BottomTabs: jest.fn(({ children }) => {children}), - BottomTabsScreen: jest.fn(({ children }) => {children}), + ...actualModule, + Tabs: { + ...actualModule.Tabs, + Host: jest.fn(({ children }) => {children}), + Screen: jest.fn(({ children }) => {children}), + }, }; }); diff --git a/packages/expo-router/src/native-tabs/__tests__/render.test.ios.tsx b/packages/expo-router/src/native-tabs/__tests__/render.test.ios.tsx index 01667cd201be30..78474cac6126f4 100644 --- a/packages/expo-router/src/native-tabs/__tests__/render.test.ios.tsx +++ b/packages/expo-router/src/native-tabs/__tests__/render.test.ios.tsx @@ -2,10 +2,7 @@ import { usePreventRemove } from '@react-navigation/core'; import { screen } from '@testing-library/react-native'; import React, { isValidElement } from 'react'; import { Button, View } from 'react-native'; -import { - BottomTabsScreen as _BottomTabsScreen, - BottomTabs as _BottomTabs, -} from 'react-native-screens'; +import { Tabs } from 'react-native-screens'; import { usePathname } from '../../hooks'; import { router } from '../../imperative-api'; @@ -23,10 +20,16 @@ import { jest.mock('react-native-screens', () => { const { View }: typeof import('react-native') = jest.requireActual('react-native'); + const actualModule = jest.requireActual( + 'react-native-screens' + ) as typeof import('react-native-screens'); return { - ...(jest.requireActual('react-native-screens') as typeof import('react-native-screens')), - BottomTabs: jest.fn(({ children }) => {children}), - BottomTabsScreen: jest.fn(({ children }) => {children}), + ...actualModule, + Tabs: { + ...actualModule.Tabs, + Host: jest.fn(({ children }) => {children}), + Screen: jest.fn(({ children }) => {children}), + }, }; }); @@ -38,8 +41,8 @@ jest.mock('../NativeTabsView', () => { }; }); -const BottomTabsScreen = _BottomTabsScreen as jest.MockedFunction; -const BottomTabs = _BottomTabs as jest.MockedFunction; +const TabsScreen = Tabs.Screen as jest.MockedFunction; +const TabsHost = Tabs.Host as jest.MockedFunction; const warn = jest.fn(); const error = jest.fn(); @@ -70,7 +73,7 @@ it('renders tabs correctly', () => { expect(screen.getByTestId('index')).toBeVisible(); expect(screen.getByTestId('second')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen).toHaveBeenCalledTimes(2); }); describe('Tabs visibility', () => { @@ -90,7 +93,7 @@ describe('Tabs visibility', () => { expect(screen.getByTestId('index')).toBeVisible(); expect(screen.getByTestId('second')).toBeVisible(); expect(screen.queryByTestId('third')).toBeNull(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen).toHaveBeenCalledTimes(2); }); it('does not render hidden tabs', () => { @@ -115,7 +118,7 @@ describe('Tabs visibility', () => { expect(screen.queryByTestId('third')).toBeNull(); expect(screen.queryByTestId('fourth')).toBeNull(); expect(screen.getByTestId('fifth')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(3); + expect(TabsScreen).toHaveBeenCalledTimes(3); }); it('does not render tabs, when route does not exist', () => { @@ -131,7 +134,7 @@ describe('Tabs visibility', () => { expect(screen.getByTestId('index')).toBeVisible(); expect(screen.queryByTestId('second')).toBeNull(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen).toHaveBeenCalledTimes(1); expect(warn).toHaveBeenCalledTimes(1); expect(warn).toHaveBeenCalledWith( '[Layout children]: Too many screens defined. Route "second" is extraneous.' @@ -154,11 +157,11 @@ describe('First focused tab', () => { expect(screen.getByTestId('index')).toBeVisible(); expect(screen.getByTestId('second')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0].isFocused).toBe(true); - expect(BottomTabsScreen.mock.calls[0][0].tabKey).toMatch(/^index-[-\w]+/); - expect(BottomTabsScreen.mock.calls[1][0].isFocused).toBe(false); - expect(BottomTabsScreen.mock.calls[1][0].tabKey).toMatch(/^second-[-\w]+/); + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0].isFocused).toBe(true); + expect(TabsScreen.mock.calls[0][0].tabKey).toMatch(/^index-[-\w]+/); + expect(TabsScreen.mock.calls[1][0].isFocused).toBe(false); + expect(TabsScreen.mock.calls[1][0].tabKey).toMatch(/^second-[-\w]+/); }); it('index tab is focused when it is second tab', () => { @@ -175,11 +178,11 @@ describe('First focused tab', () => { expect(screen.getByTestId('index')).toBeVisible(); expect(screen.getByTestId('second')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0].isFocused).toBe(false); - expect(BottomTabsScreen.mock.calls[0][0].tabKey).toMatch(/^second-[-\w]+/); - expect(BottomTabsScreen.mock.calls[1][0].isFocused).toBe(true); - expect(BottomTabsScreen.mock.calls[1][0].tabKey).toMatch(/^index-[-\w]+/); + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0].isFocused).toBe(false); + expect(TabsScreen.mock.calls[0][0].tabKey).toMatch(/^second-[-\w]+/); + expect(TabsScreen.mock.calls[1][0].isFocused).toBe(true); + expect(TabsScreen.mock.calls[1][0].tabKey).toMatch(/^index-[-\w]+/); }); describe('First tab is used, when index is hidden', () => { @@ -235,7 +238,7 @@ describe('First focused tab', () => { expect(screen.getByTestId('expo-router-unmatched')).toBeVisible(); expect(screen.queryByTestId('first')).toBeNull(); expect(screen.queryByTestId('second')).toBeNull(); - expect(BottomTabsScreen).not.toHaveBeenCalled(); + expect(TabsScreen).not.toHaveBeenCalled(); }); it('Correct tab is shown, when index is hidden and redirect is set in layout', () => { @@ -260,11 +263,11 @@ describe('First focused tab', () => { expect(screen.getByTestId('first')).toBeVisible(); expect(screen.getByTestId('second')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0].isFocused).toBe(false); - expect(BottomTabsScreen.mock.calls[0][0].tabKey).toMatch(/^first-[-\w]+/); - expect(BottomTabsScreen.mock.calls[1][0].isFocused).toBe(true); - expect(BottomTabsScreen.mock.calls[1][0].tabKey).toMatch(/^second-[-\w]+/); + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0].isFocused).toBe(false); + expect(TabsScreen.mock.calls[0][0].tabKey).toMatch(/^first-[-\w]+/); + expect(TabsScreen.mock.calls[1][0].isFocused).toBe(true); + expect(TabsScreen.mock.calls[1][0].tabKey).toMatch(/^second-[-\w]+/); }); it('Correct tab is shown, when index does not exist, redirect is set in layout and +not-found is specified', () => { @@ -289,11 +292,11 @@ describe('First focused tab', () => { expect(screen.getByTestId('first')).toBeVisible(); expect(screen.getByTestId('second')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0].isFocused).toBe(false); - expect(BottomTabsScreen.mock.calls[0][0].tabKey).toMatch(/^first-[-\w]+/); - expect(BottomTabsScreen.mock.calls[1][0].isFocused).toBe(true); - expect(BottomTabsScreen.mock.calls[1][0].tabKey).toMatch(/^second-[-\w]+/); + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0].isFocused).toBe(false); + expect(TabsScreen.mock.calls[0][0].tabKey).toMatch(/^first-[-\w]+/); + expect(TabsScreen.mock.calls[1][0].isFocused).toBe(true); + expect(TabsScreen.mock.calls[1][0].tabKey).toMatch(/^second-[-\w]+/); }); it('404 is shown, when index does not exist, redirect is set in layout and no +not-found is specified', () => { @@ -318,7 +321,7 @@ describe('First focused tab', () => { expect(screen.getByTestId('expo-router-unmatched')).toBeVisible(); expect(screen.queryByTestId('first')).toBeNull(); expect(screen.queryByTestId('second')).toBeNull(); - expect(BottomTabsScreen).not.toHaveBeenCalled(); + expect(TabsScreen).not.toHaveBeenCalled(); }); it('Can remove the last tab, when it is focused', async () => { @@ -348,37 +351,37 @@ describe('First focused tab', () => { expect(screen.getByTestId('index')).toBeVisible(); expect(screen.getByTestId('second')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(2); - expect(BottomTabsScreen.mock.calls[0][0].isFocused).toBe(true); - expect(BottomTabsScreen.mock.calls[0][0].tabKey).toMatch(/^index-[-\w]+/); - expect(BottomTabsScreen.mock.calls[1][0].isFocused).toBe(false); - expect(BottomTabsScreen.mock.calls[1][0].tabKey).toMatch(/^second-[-\w]+/); + expect(TabsScreen).toHaveBeenCalledTimes(2); + expect(TabsScreen.mock.calls[0][0].isFocused).toBe(true); + expect(TabsScreen.mock.calls[0][0].tabKey).toMatch(/^index-[-\w]+/); + expect(TabsScreen.mock.calls[1][0].isFocused).toBe(false); + expect(TabsScreen.mock.calls[1][0].tabKey).toMatch(/^second-[-\w]+/); - BottomTabsScreen.mockClear(); + TabsScreen.mockClear(); act(() => router.navigate('/second')); expect(screen.getByTestId('index')).toBeVisible(); expect(screen.getByTestId('second')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(4); - expect(BottomTabsScreen.mock.calls[2][0].isFocused).toBe(false); - expect(BottomTabsScreen.mock.calls[2][0].tabKey).toMatch(/^index-[-\w]+/); - expect(BottomTabsScreen.mock.calls[3][0].isFocused).toBe(true); - expect(BottomTabsScreen.mock.calls[3][0].tabKey).toMatch(/^second-[-\w]+/); + expect(TabsScreen).toHaveBeenCalledTimes(4); + expect(TabsScreen.mock.calls[2][0].isFocused).toBe(false); + expect(TabsScreen.mock.calls[2][0].tabKey).toMatch(/^index-[-\w]+/); + expect(TabsScreen.mock.calls[3][0].isFocused).toBe(true); + expect(TabsScreen.mock.calls[3][0].tabKey).toMatch(/^second-[-\w]+/); - BottomTabsScreen.mockClear(); + TabsScreen.mockClear(); act(() => { fireEvent.press(screen.getByTestId('remove')); }); expect(screen.queryByTestId('second')).toBeNull(); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(3); - expect(BottomTabsScreen.mock.calls[0][0].isFocused).toBe(true); - expect(BottomTabsScreen.mock.calls[0][0].tabKey).toMatch(/^index-[-\w]+/); - expect(BottomTabsScreen.mock.calls[1][0].isFocused).toBe(true); - expect(BottomTabsScreen.mock.calls[1][0].tabKey).toMatch(/^index-[-\w]+/); - expect(BottomTabsScreen.mock.calls[2][0].isFocused).toBe(true); - expect(BottomTabsScreen.mock.calls[2][0].tabKey).toMatch(/^index-[-\w]+/); + expect(TabsScreen).toHaveBeenCalledTimes(3); + expect(TabsScreen.mock.calls[0][0].isFocused).toBe(true); + expect(TabsScreen.mock.calls[0][0].tabKey).toMatch(/^index-[-\w]+/); + expect(TabsScreen.mock.calls[1][0].isFocused).toBe(true); + expect(TabsScreen.mock.calls[1][0].tabKey).toMatch(/^index-[-\w]+/); + expect(TabsScreen.mock.calls[2][0].isFocused).toBe(true); + expect(TabsScreen.mock.calls[2][0].tabKey).toMatch(/^index-[-\w]+/); }); }); @@ -420,9 +423,9 @@ describe('Native props validation', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].standardAppearance.tabBarBlurEffect).toBe(blurEffect); - expect(BottomTabsScreen.mock.calls[0][0].scrollEdgeAppearance.tabBarBlurEffect).toBe('none'); + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].standardAppearance.tabBarBlurEffect).toBe(blurEffect); + expect(TabsScreen.mock.calls[0][0].scrollEdgeAppearance.tabBarBlurEffect).toBe('none'); }); it.each(['test', 'wrongValue', ...SUPPORTED_BLUR_EFFECTS.map((x) => x.toUpperCase())])( 'warns when unsupported %s blur effect is used', @@ -440,9 +443,9 @@ describe('Native props validation', () => { expect(warn).toHaveBeenCalledWith( `Unsupported blurEffect: ${blurEffect}. Supported values are: ${SUPPORTED_BLUR_EFFECTS.map((effect) => `"${effect}"`).join(', ')}` ); - expect(BottomTabsScreen).toHaveBeenCalledTimes(1); - expect(BottomTabsScreen.mock.calls[0][0].standardAppearance.tabBarBlurEffect).toBe(undefined); - expect(BottomTabsScreen.mock.calls[0][0].scrollEdgeAppearance.tabBarBlurEffect).toBe('none'); + expect(TabsScreen).toHaveBeenCalledTimes(1); + expect(TabsScreen.mock.calls[0][0].standardAppearance.tabBarBlurEffect).toBe(undefined); + expect(TabsScreen.mock.calls[0][0].scrollEdgeAppearance.tabBarBlurEffect).toBe('none'); } ); it.each(SUPPORTED_TAB_BAR_ITEM_LABEL_VISIBILITY_MODES)( @@ -458,8 +461,8 @@ describe('Native props validation', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabs).toHaveBeenCalledTimes(1); - expect(BottomTabs.mock.calls[0][0].tabBarItemLabelVisibilityMode).toBe(labelVisibilityMode); + expect(TabsHost).toHaveBeenCalledTimes(1); + expect(TabsHost.mock.calls[0][0].tabBarItemLabelVisibilityMode).toBe(labelVisibilityMode); } ); it.each([ @@ -480,8 +483,8 @@ describe('Native props validation', () => { expect(warn).toHaveBeenCalledWith( `Unsupported labelVisibilityMode: ${labelVisibilityMode}. Supported values are: ${SUPPORTED_TAB_BAR_ITEM_LABEL_VISIBILITY_MODES.map((effect) => `"${effect}"`).join(', ')}` ); - expect(BottomTabs).toHaveBeenCalledTimes(1); - expect(BottomTabs.mock.calls[0][0].tabBarItemLabelVisibilityMode).toBe(undefined); + expect(TabsHost).toHaveBeenCalledTimes(1); + expect(TabsHost.mock.calls[0][0].tabBarItemLabelVisibilityMode).toBe(undefined); }); it.each(SUPPORTED_TAB_BAR_MINIMIZE_BEHAVIORS)( 'supports %s minimize behavior', @@ -496,8 +499,8 @@ describe('Native props validation', () => { }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabs).toHaveBeenCalledTimes(1); - expect(BottomTabs.mock.calls[0][0].tabBarMinimizeBehavior).toBe(minimizeBehavior); + expect(TabsHost).toHaveBeenCalledTimes(1); + expect(TabsHost.mock.calls[0][0].tabBarMinimizeBehavior).toBe(minimizeBehavior); } ); it.each([ @@ -518,8 +521,8 @@ describe('Native props validation', () => { expect(warn).toHaveBeenCalledWith( `Unsupported minimizeBehavior: ${minimizeBehavior}. Supported values are: ${SUPPORTED_TAB_BAR_MINIMIZE_BEHAVIORS.map((effect) => `"${effect}"`).join(', ')}` ); - expect(BottomTabs).toHaveBeenCalledTimes(1); - expect(BottomTabs.mock.calls[0][0].tabBarMinimizeBehavior).toBe(undefined); + expect(TabsHost).toHaveBeenCalledTimes(1); + expect(TabsHost.mock.calls[0][0].tabBarMinimizeBehavior).toBe(undefined); }); }); @@ -560,10 +563,10 @@ describe('Misc', () => { index: () => , }); expect(screen.getByTestId('index')).toBeVisible(); - expect(BottomTabs).toHaveBeenCalledTimes(1); - expect(BottomTabs.mock.calls[0][0].bottomAccessory).toBeDefined(); + expect(TabsHost).toHaveBeenCalledTimes(1); + expect(TabsHost.mock.calls[0][0].bottomAccessory).toBeDefined(); - const bottomAccessoryFn = BottomTabs.mock.calls[0][0].bottomAccessory!; + const bottomAccessoryFn = TabsHost.mock.calls[0][0].bottomAccessory!; const regularRender = bottomAccessoryFn('regular'); const inlineRender = bottomAccessoryFn('inline'); @@ -582,7 +585,7 @@ describe('Misc', () => { { hidden: true, expected: true }, { hidden: false, expected: false }, { hidden: undefined, expected: undefined }, - ])('passes hidden=$hidden prop to BottomTabs', ({ hidden, expected }) => { + ])('passes hidden=$hidden prop to TabsHost', ({ hidden, expected }) => { renderRouter({ _layout: () => (