Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions apps/bare-expo/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2923,7 +2923,7 @@ PODS:
- ReactNativeDependencies
- RNWorklets
- Yoga
- RNScreens (4.19.0):
- RNScreens (4.20.0):
- hermes-engine
- RCTRequired
- RCTTypeSafety
Expand All @@ -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
Expand Down Expand Up @@ -3827,7 +3827,7 @@ SPEC CHECKSUMS:
EXUpdates: 83e4d666a085b44149f3b21d5bd057ad37b2c3e5
EXUpdatesInterface: 1436757deb0d574b84bba063bd024c315e0ec08b
FBLazyVector: 3a7ea85f6009224ad89f7daeda516f189e6b5759
hermes-engine: 6bb3000824be2770010ae038914fa26721255c8e
hermes-engine: f631dcabc3dc2d46dc5f32c6c79d48d1d9e9aac6
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/bare-expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -76,13 +78,27 @@ data class DevSession(
val source: DevSessionSource,
val hostname: String? = null,
val config: Map<String, Any>? = null,
val platform: DevSessionPlatform? = null
val platform: DevSessionPlatform? = null,
val iconUrl: String? = null
)

data class DevSessionResponse(
val data: List<DevSession>
)

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(
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -204,7 +236,55 @@ class HomeAppViewModel(

val feedbackState = MutableStateFlow(FeedbackState())

val developmentServers: StateFlow<List<DevSession>> = 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<List<DevSession>> = flow {
while (true) {
try {
val sessions = restClient.sendAuthenticatedApiV2Request<DevSessionResponse>(
Expand All @@ -223,6 +303,57 @@ class HomeAppViewModel(
initialValue = emptyList()
)

private val localDevelopmentServers: StateFlow<List<DevSession>> = 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<DevSession>()
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<List<DevSession>> =
combine(
localDevelopmentServers,
remoteDevelopmentServers
) { local, remote ->
val merged = mutableMapOf<String, DevSession>()

// 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<BranchDetailsQuery.ById?> {
return refreshableFlow(scope = viewModelScope, fetcher = {
service.branchDetails(branchName, appId)
Expand Down
Loading
Loading