expo-dev-launcher 5.2.0-canary-20250709-136b77f → 5.2.0-canary-20250713-8f814f8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,13 +20,13 @@ expoModule {
20
20
  }
21
21
 
22
22
  group = "host.exp.exponent"
23
- version = "5.2.0-canary-20250709-136b77f"
23
+ version = "5.2.0-canary-20250713-8f814f8"
24
24
 
25
25
  android {
26
26
  namespace "expo.modules.devlauncher"
27
27
  defaultConfig {
28
28
  versionCode 9
29
- versionName "5.2.0-canary-20250709-136b77f"
29
+ versionName "5.2.0-canary-20250713-8f814f8"
30
30
  }
31
31
 
32
32
  buildTypes {
@@ -7,7 +7,6 @@ import androidx.lifecycle.ViewModel
7
7
  import androidx.lifecycle.viewModelScope
8
8
  import expo.modules.devlauncher.DevLauncherController
9
9
  import expo.modules.devlauncher.MeQuery
10
- import expo.modules.devlauncher.services.HttpClientService
11
10
  import expo.modules.devlauncher.services.PackagerInfo
12
11
  import expo.modules.devlauncher.services.PackagerService
13
12
  import expo.modules.devlauncher.services.SessionService
@@ -19,6 +18,7 @@ import kotlinx.coroutines.launch
19
18
 
20
19
  sealed interface HomeAction {
21
20
  class OpenApp(val url: String) : HomeAction
21
+ object RefetchRunningApps : HomeAction
22
22
  }
23
23
 
24
24
  typealias HomeActionHandler = (HomeAction) -> Unit
@@ -26,15 +26,14 @@ typealias HomeActionHandler = (HomeAction) -> Unit
26
26
  data class HomeState(
27
27
  val appName: String = "BareExpo",
28
28
  val runningPackagers: Set<PackagerInfo> = emptySet(),
29
+ val isFetchingPackagers: Boolean = false,
29
30
  val currentAccount: MeQuery.Account? = null
30
31
  )
31
32
 
32
33
  class HomeViewModel() : ViewModel() {
33
- val httpClientService = inject<HttpClientService>()
34
34
  val devLauncherController = inject<DevLauncherController>()
35
35
  val sessionService = inject<SessionService>()
36
-
37
- val packagerService = PackagerService(httpClientService.httpClient, viewModelScope)
36
+ val packagerService = inject<PackagerService>()
38
37
 
39
38
  private var _state = mutableStateOf(
40
39
  HomeState(
@@ -50,6 +49,10 @@ class HomeViewModel() : ViewModel() {
50
49
  get() = _state.value
51
50
 
52
51
  init {
52
+ viewModelScope.launch {
53
+ packagerService.refetchedPackager()
54
+ }
55
+
53
56
  packagerService.runningPackagers.onEach { newPackagers ->
54
57
  _state.value = _state.value.copy(
55
58
  runningPackagers = newPackagers
@@ -67,6 +70,12 @@ class HomeViewModel() : ViewModel() {
67
70
  )
68
71
  }
69
72
  }.launchIn(viewModelScope)
73
+
74
+ packagerService.isLoading.onEach { isLoading ->
75
+ _state.value = _state.value.copy(
76
+ isFetchingPackagers = isLoading
77
+ )
78
+ }.launchIn(viewModelScope)
70
79
  }
71
80
 
72
81
  fun onAction(action: HomeAction) {
@@ -79,6 +88,8 @@ class HomeViewModel() : ViewModel() {
79
88
  Log.e("DevLauncher", "Failed to open app: ${action.url}", e)
80
89
  }
81
90
  }
91
+
92
+ HomeAction.RefetchRunningApps -> viewModelScope.launch { packagerService.refetchedPackager() }
82
93
  }
83
94
  }
84
95
  }
@@ -1,21 +1,32 @@
1
1
  package expo.modules.devlauncher.compose.screens
2
2
 
3
3
  import androidx.compose.foundation.Image
4
+ import androidx.compose.foundation.background
4
5
  import androidx.compose.foundation.border
6
+ import androidx.compose.foundation.layout.Box
5
7
  import androidx.compose.foundation.layout.Column
6
8
  import androidx.compose.foundation.layout.Row
9
+ import androidx.compose.foundation.layout.displayCutoutPadding
7
10
  import androidx.compose.foundation.layout.fillMaxWidth
8
11
  import androidx.compose.foundation.layout.padding
12
+ import androidx.compose.foundation.layout.systemBarsPadding
9
13
  import androidx.compose.foundation.shape.RoundedCornerShape
10
14
  import androidx.compose.foundation.text.KeyboardOptions
11
15
  import androidx.compose.runtime.Composable
12
16
  import androidx.compose.runtime.mutableStateOf
13
17
  import androidx.compose.runtime.remember
14
18
  import androidx.compose.ui.Modifier
19
+ import androidx.compose.ui.draw.clip
15
20
  import androidx.compose.ui.res.painterResource
16
21
  import androidx.compose.ui.text.input.KeyboardCapitalization
17
22
  import androidx.compose.ui.text.input.KeyboardType
18
23
  import androidx.compose.ui.tooling.preview.Preview
24
+ import androidx.compose.ui.unit.dp
25
+ import com.composables.core.Dialog
26
+ import com.composables.core.DialogPanel
27
+ import com.composables.core.Icon
28
+ import com.composables.core.Scrim
29
+ import com.composables.core.rememberDialogState
19
30
  import com.composeunstyled.Button
20
31
  import com.composeunstyled.TextField
21
32
  import expo.modules.devlauncher.R
@@ -23,14 +34,18 @@ import expo.modules.devlauncher.compose.HomeAction
23
34
  import expo.modules.devlauncher.compose.HomeState
24
35
  import expo.modules.devlauncher.compose.primitives.Accordion
25
36
  import expo.modules.devlauncher.compose.ui.AppHeader
37
+ import expo.modules.devlauncher.compose.ui.DevelopmentSessionHelper
26
38
  import expo.modules.devlauncher.compose.ui.RunningAppCard
27
39
  import expo.modules.devlauncher.compose.ui.ScreenHeaderContainer
28
40
  import expo.modules.devlauncher.compose.ui.SectionHeader
29
41
  import expo.modules.devmenu.compose.primitives.Divider
42
+ import expo.modules.devmenu.compose.primitives.Heading
30
43
  import expo.modules.devmenu.compose.primitives.RoundedSurface
44
+ import expo.modules.devmenu.compose.primitives.RowLayout
31
45
  import expo.modules.devmenu.compose.primitives.Spacer
32
46
  import expo.modules.devmenu.compose.primitives.Text
33
47
  import expo.modules.devmenu.compose.theme.Theme
48
+ import expo.modules.devmenu.compose.ui.MenuButton
34
49
 
35
50
  @Composable
36
51
  fun HomeScreen(
@@ -38,6 +53,45 @@ fun HomeScreen(
38
53
  onAction: (HomeAction) -> Unit,
39
54
  onProfileClick: () -> Unit
40
55
  ) {
56
+ val hasPackager = state.runningPackagers.isNotEmpty()
57
+ val dialogState = rememberDialogState(initiallyVisible = false)
58
+
59
+ Dialog(state = dialogState) {
60
+ Scrim()
61
+
62
+ DialogPanel(
63
+ modifier = Modifier
64
+ .displayCutoutPadding()
65
+ .systemBarsPadding()
66
+ .clip(RoundedCornerShape(12.dp))
67
+ .background(Theme.colors.background.default)
68
+ ) {
69
+ Column {
70
+ RowLayout(
71
+ rightComponent = {
72
+ Button(onClick = {
73
+ dialogState.visible = false
74
+ }) {
75
+ Icon(
76
+ painterResource(R.drawable._expodevclientcomponents_assets_xicon),
77
+ contentDescription = "Close dialog"
78
+ )
79
+ }
80
+ },
81
+ modifier = Modifier.padding(Theme.spacing.medium)
82
+ ) {
83
+ Heading("Development servers")
84
+ }
85
+
86
+ Divider()
87
+
88
+ Row(modifier = Modifier.padding(Theme.spacing.medium)) {
89
+ DevelopmentSessionHelper()
90
+ }
91
+ }
92
+ }
93
+ }
94
+
41
95
  Column {
42
96
  ScreenHeaderContainer(modifier = Modifier.padding(Theme.spacing.medium)) {
43
97
  AppHeader(
@@ -65,10 +119,16 @@ fun HomeScreen(
65
119
  )
66
120
  },
67
121
  rightIcon = {
68
- Image(
69
- painter = painterResource(R.drawable._expodevclientcomponents_assets_infoicon),
70
- contentDescription = "Terminal Icon"
71
- )
122
+ if (hasPackager) {
123
+ Button(onClick = {
124
+ dialogState.visible = true
125
+ }) {
126
+ Image(
127
+ painter = painterResource(R.drawable._expodevclientcomponents_assets_infoicon),
128
+ contentDescription = "Terminal Icon"
129
+ )
130
+ }
131
+ }
72
132
  }
73
133
  )
74
134
  }
@@ -77,15 +137,35 @@ fun HomeScreen(
77
137
 
78
138
  RoundedSurface {
79
139
  Column {
80
- for (packager in state.runningPackagers) {
81
- RunningAppCard(
82
- appIp = packager.url
83
- ) {
84
- onAction(HomeAction.OpenApp(packager.url))
140
+ if (hasPackager) {
141
+ for (packager in state.runningPackagers) {
142
+ RunningAppCard(
143
+ appIp = packager.url,
144
+ appName = packager.description
145
+ ) {
146
+ onAction(HomeAction.OpenApp(packager.url))
147
+ }
148
+ Divider()
149
+ }
150
+ } else {
151
+ Box(modifier = Modifier.padding(Theme.spacing.medium)) {
152
+ DevelopmentSessionHelper()
85
153
  }
86
154
  Divider()
87
155
  }
88
156
 
157
+ MenuButton(
158
+ onClick = {
159
+ onAction(HomeAction.RefetchRunningApps)
160
+ },
161
+ enabled = !state.isFetchingPackagers,
162
+ label = if (state.isFetchingPackagers) {
163
+ "Searching for development servers..."
164
+ } else {
165
+ "Fetch development servers"
166
+ }
167
+ )
168
+
89
169
  Accordion("Enter URL manually", initialState = false) {
90
170
  val url = remember { mutableStateOf("") }
91
171
 
@@ -116,9 +196,12 @@ fun HomeScreen(
116
196
 
117
197
  Spacer(Theme.spacing.tiny)
118
198
 
119
- Button(onClick = {
120
- onAction(HomeAction.OpenApp(url.value))
121
- }, modifier = Modifier.fillMaxWidth()) {
199
+ Button(
200
+ onClick = {
201
+ onAction(HomeAction.OpenApp(url.value))
202
+ },
203
+ modifier = Modifier.fillMaxWidth()
204
+ ) {
122
205
  Row(modifier = Modifier.padding(vertical = Theme.spacing.small)) {
123
206
  Text("Connect")
124
207
  }
@@ -20,7 +20,7 @@ fun BottomTabButton(
20
20
  modifier: Modifier = Modifier,
21
21
  onClick: () -> Unit
22
22
  ) {
23
- Button(onClick = onClick, modifier = modifier) {
23
+ Button(onClick = onClick, enabled = !isSelected, modifier = modifier) {
24
24
  Column(horizontalAlignment = Alignment.Companion.CenterHorizontally, modifier = Modifier.Companion.padding(Theme.spacing.small)) {
25
25
  Icon(
26
26
  painter = icon,
@@ -0,0 +1,52 @@
1
+ package expo.modules.devlauncher.compose.ui
2
+
3
+ import androidx.compose.foundation.BorderStroke
4
+ import androidx.compose.foundation.layout.Box
5
+ import androidx.compose.foundation.layout.Column
6
+ import androidx.compose.foundation.layout.fillMaxWidth
7
+ import androidx.compose.foundation.layout.padding
8
+ import androidx.compose.runtime.Composable
9
+ import androidx.compose.ui.Modifier
10
+ import androidx.compose.ui.tooling.preview.Preview
11
+ import expo.modules.devmenu.compose.primitives.Mono
12
+ import expo.modules.devmenu.compose.primitives.RoundedSurface
13
+ import expo.modules.devmenu.compose.primitives.Spacer
14
+ import expo.modules.devmenu.compose.primitives.Text
15
+ import expo.modules.devmenu.compose.theme.Theme
16
+
17
+ @Composable
18
+ fun DevelopmentSessionHelper() {
19
+ Column {
20
+ Text("Start a local development server with:")
21
+
22
+ Spacer(Theme.spacing.small)
23
+
24
+ RoundedSurface(
25
+ color = Theme.colors.background.secondary,
26
+ border = BorderStroke(
27
+ width = Theme.sizing.border.default,
28
+ color = Theme.colors.border.default
29
+ ),
30
+ modifier = Modifier
31
+ .fillMaxWidth()
32
+ ) {
33
+ Box(modifier = Modifier.padding(Theme.spacing.medium)) {
34
+ Mono("npx expo start")
35
+ }
36
+ }
37
+
38
+ Spacer(Theme.spacing.small)
39
+
40
+ Text("Then, select the local server when it appears here.")
41
+
42
+ Spacer(Theme.spacing.small)
43
+
44
+ Text("Alternatively, open the Camera app and scan the QR code that appears in your terminal")
45
+ }
46
+ }
47
+
48
+ @Preview(showBackground = true)
49
+ @Composable
50
+ fun DevelopmentSessionHelperPreview() {
51
+ DevelopmentSessionHelper()
52
+ }
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column
5
5
  import androidx.compose.foundation.layout.Row
6
6
  import androidx.compose.foundation.layout.padding
7
7
  import androidx.compose.foundation.layout.size
8
+ import androidx.compose.foundation.layout.width
8
9
  import androidx.compose.runtime.Composable
9
10
  import androidx.compose.ui.Modifier
10
11
  import androidx.compose.ui.res.painterResource
@@ -33,10 +34,8 @@ fun RunningAppCard(
33
34
  onClick = { onClick(appIp) },
34
35
  backgroundColor = Theme.colors.background.default
35
36
  ) {
36
- Column {
37
+ Column(modifier = Modifier.padding(Theme.spacing.medium)) {
37
38
  RowLayout(
38
- modifier = Modifier
39
- .padding(Theme.spacing.medium),
40
39
  leftComponent = {
41
40
  val iconColor = Theme.colors.status.success
42
41
 
@@ -65,8 +64,10 @@ fun RunningAppCard(
65
64
  }
66
65
 
67
66
  if (description != null) {
67
+ Spacer(Theme.spacing.tiny)
68
+
68
69
  Row {
69
- Spacer(Theme.spacing.small + Theme.sizing.icon.extraSmall)
70
+ Spacer(modifier = Modifier.width(Theme.sizing.icon.extraSmall))
70
71
 
71
72
  Text(
72
73
  text = description,
@@ -3,12 +3,16 @@ package expo.modules.devlauncher.services
3
3
  import com.apollographql.apollo.ApolloClient
4
4
  import com.apollographql.apollo.api.ApolloResponse
5
5
  import com.apollographql.apollo.api.http.HttpHeader
6
+ import com.apollographql.apollo.network.okHttpClient
6
7
  import expo.modules.devlauncher.MeQuery
7
8
 
8
- class ApolloClientService {
9
+ class ApolloClientService(
10
+ httpClientService: HttpClientService
11
+ ) {
9
12
  private var _client = ApolloClient
10
13
  .Builder()
11
14
  .serverUrl("https://exp.host/--/graphql")
15
+ .okHttpClient(httpClientService.httpClient)
12
16
  .build()
13
17
 
14
18
  val client: ApolloClient
@@ -28,6 +28,9 @@ object DependencyInjection {
28
28
  var devLauncherController: DevLauncherController? = null
29
29
  private set
30
30
 
31
+ var packagerService: PackagerService? = null
32
+ private set
33
+
31
34
  fun init(context: Context, devLauncherController: DevLauncherController) = synchronized(this) {
32
35
  if (wasInitialized) {
33
36
  return
@@ -46,14 +49,17 @@ object DependencyInjection {
46
49
  httpClientService = httpClient
47
50
  )
48
51
 
49
- val apolloClient = ApolloClientService()
52
+ val apolloClient = ApolloClientService(httpClient)
50
53
 
51
54
  apolloClientService = apolloClient
52
55
 
53
56
  sessionService = SessionService(
54
57
  sessionStore = context.applicationContext.getSharedPreferences("expo.modules.devlauncher.session", Context.MODE_PRIVATE),
55
- apolloClientService = apolloClient
58
+ apolloClientService = apolloClient,
59
+ httpClientService = httpClient
56
60
  )
61
+
62
+ packagerService = PackagerService(httpClient)
57
63
  }
58
64
  }
59
65
 
@@ -65,6 +71,7 @@ internal inline fun <reified T> injectService(): T {
65
71
  ImageLoaderService::class -> DependencyInjection.imageLoaderService
66
72
  HttpClientService::class -> DependencyInjection.httpClientService
67
73
  DevLauncherController::class -> DependencyInjection.devLauncherController
74
+ PackagerService::class -> DependencyInjection.packagerService
68
75
  else -> throw IllegalArgumentException("Unknown service type: ${T::class}")
69
76
  } as T
70
77
  }
@@ -1,9 +1,84 @@
1
1
  package expo.modules.devlauncher.services
2
2
 
3
+ import androidx.core.net.toUri
4
+ import expo.modules.devlauncher.helpers.await
3
5
  import okhttp3.OkHttpClient
6
+ import okhttp3.Request
7
+ import org.json.JSONObject
4
8
 
5
- class HttpClientService {
6
- val httpClient = OkHttpClient
7
- .Builder()
9
+ private const val restEndpoint = "https://exp.host/--/api/v2"
10
+
11
+ data class DevelopmentSession(
12
+ val description: String,
13
+ val source: String,
14
+ val url: String
15
+ ) {
16
+ constructor(json: JSONObject) : this(
17
+ description = json.getString("description"),
18
+ source = json.getString("source"),
19
+ url = json.getString("url").let { url ->
20
+ val parsed = url.toUri()
21
+
22
+ if (parsed.host != "expo-development-client") {
23
+ return@let url
24
+ }
25
+
26
+ val urlParameter = parsed.getQueryParameter("url")
27
+ urlParameter ?: url
28
+ }
29
+ )
30
+ }
31
+
32
+ class HttpClientService() {
33
+ private var currentSession: String? = null
34
+
35
+ val httpClient = OkHttpClient.Builder()
36
+ .addInterceptor { chain ->
37
+ val originalRequest = chain.request()
38
+ val session = currentSession
39
+ if (session == null || !originalRequest.url.toString().startsWith(restEndpoint)) {
40
+ return@addInterceptor chain.proceed(originalRequest)
41
+ }
42
+
43
+ val newRequest = originalRequest.newBuilder()
44
+ .header("expo-session", session)
45
+ .build()
46
+ chain.proceed(newRequest)
47
+ }
8
48
  .build()
49
+
50
+ internal fun setSession(sessionSecret: String?) {
51
+ currentSession = sessionSecret
52
+ }
53
+
54
+ suspend fun fetchDevelopmentSession(): List<DevelopmentSession> {
55
+ if (currentSession === null) {
56
+ return emptyList()
57
+ }
58
+
59
+ val request = Request.Builder()
60
+ .url("$restEndpoint/development-sessions")
61
+ .header("content-type", "application/json")
62
+ .build()
63
+
64
+ val response = request.await(httpClient)
65
+
66
+ if (!response.isSuccessful) {
67
+ throw IllegalStateException("Failed to fetch development session: ${response.code} ${response.message}")
68
+ }
69
+
70
+ val body = response.body?.string()
71
+ if (body == null) {
72
+ return emptyList()
73
+ }
74
+
75
+ return buildList {
76
+ val json = JSONObject(body)
77
+ val data = json.getJSONArray("data")
78
+ for (index in 0 until data.length()) {
79
+ val item = data.getJSONObject(index)
80
+ add(DevelopmentSession(json = item))
81
+ }
82
+ }
83
+ }
9
84
  }
@@ -1,18 +1,17 @@
1
1
  package expo.modules.devlauncher.services
2
2
 
3
+ import androidx.core.net.toUri
3
4
  import expo.modules.core.utilities.EmulatorUtilities
4
5
  import expo.modules.devlauncher.helpers.await
5
- import kotlinx.coroutines.CoroutineScope
6
6
  import kotlinx.coroutines.Dispatchers
7
- import kotlinx.coroutines.delay
7
+ import kotlinx.coroutines.async
8
+ import kotlinx.coroutines.awaitAll
9
+ import kotlinx.coroutines.coroutineScope
8
10
  import kotlinx.coroutines.flow.MutableStateFlow
9
11
  import kotlinx.coroutines.flow.asStateFlow
10
12
  import kotlinx.coroutines.flow.update
11
- import kotlinx.coroutines.isActive
12
- import kotlinx.coroutines.launch
13
- import okhttp3.OkHttpClient
13
+ import kotlinx.coroutines.withContext
14
14
  import okhttp3.Request
15
- import kotlin.time.Duration.Companion.seconds
16
15
 
17
16
  private val portsToCheck = arrayOf(8081, 8082, 8083, 8084, 8085, 19000, 19001, 19002)
18
17
  private val hostToCheck = if (EmulatorUtilities.isRunningOnEmulator()) {
@@ -22,65 +21,121 @@ private val hostToCheck = if (EmulatorUtilities.isRunningOnEmulator()) {
22
21
  }
23
22
 
24
23
  data class PackagerInfo(
25
- val host: String,
26
- val port: Int
24
+ val url: String,
25
+ val description: String? = null,
26
+ val isDevelopmentSession: Boolean = false
27
27
  ) {
28
+ internal val port = url.toUri().port
29
+
28
30
  internal fun createStatusPageRequest(): Request {
29
- val url = "http://$host:$port/status"
31
+ val url = "$url/status"
30
32
  return Request.Builder()
31
33
  .url(url)
32
34
  .build()
33
35
  }
34
-
35
- val url: String
36
- get() = "http://$host:$port"
37
36
  }
38
37
 
39
- private val defaultDelay = 3.seconds
40
-
41
38
  /*
42
39
  * Class responsible for discovering running packagers.
43
40
  */
44
41
  class PackagerService(
45
- private val httpClient: OkHttpClient,
46
- scope: CoroutineScope
42
+ private val httpClientService: HttpClientService
47
43
  ) {
48
- private val packagersToCheck = portsToCheck.map { PackagerInfo(hostToCheck, it) }
44
+ private val packagersToCheck = portsToCheck.map { port -> PackagerInfo("http://$hostToCheck:$port") }
49
45
 
50
46
  private val _runningPackagers = MutableStateFlow<Set<PackagerInfo>>(emptySet())
47
+ private val _isLoading = MutableStateFlow(false)
51
48
 
52
49
  val runningPackagers = _runningPackagers.asStateFlow()
50
+ val isLoading = _isLoading.asStateFlow()
51
+
52
+ private fun addPackager(packager: PackagerInfo) {
53
+ _runningPackagers.update { packagers ->
54
+ // The packager list contains a development session packager, which the same port and we are trying to add local packager.
55
+ // In this case, we can ignore the local packager, because the development session is more important.
56
+ val canBeIgnored = packagers.any { it.port == packager.port && it.isDevelopmentSession && !packager.isDevelopmentSession }
57
+ if (canBeIgnored) {
58
+ return@update packagers
59
+ }
60
+
61
+ if (!packagers.contains(packager)) {
62
+ packagers.plus(packager)
63
+ } else {
64
+ packagers
65
+ }
66
+ }
67
+ }
68
+
69
+ private fun removePackager(packager: PackagerInfo) {
70
+ _runningPackagers.update { packagers ->
71
+ if (packagers.contains(packager)) {
72
+ packagers.minus(packager)
73
+ } else {
74
+ packagers
75
+ }
76
+ }
77
+ }
78
+
79
+ private suspend fun fetchedLocalPackagers(): Unit = coroutineScope {
80
+ val jobs = packagersToCheck.map { packager ->
81
+ async {
82
+ val statusPageRequest = packager.createStatusPageRequest()
83
+ val isSuccessful = runCatching { statusPageRequest.await(httpClientService.httpClient) }
84
+ .getOrNull()
85
+ ?.use { response -> response.isSuccessful }
86
+ ?: false
53
87
 
54
- init {
55
- scope.launch(Dispatchers.IO) {
56
- while (isActive) {
57
- for (packager in packagersToCheck) {
58
- val statusPageRequest = packager.createStatusPageRequest()
59
- launch {
60
- val data = runCatching { statusPageRequest.await(httpClient) }.getOrNull()
61
- val isSuccessful = data?.isSuccessful == true
62
- data?.close()
63
- if (isSuccessful) {
64
- _runningPackagers.update {
65
- if (!it.contains(packager)) {
66
- it.plus(packager)
67
- } else {
68
- it
69
- }
70
- }
71
- } else {
72
- _runningPackagers.update {
73
- if (it.contains(packager)) {
74
- it.minus(packager)
75
- } else {
76
- it
77
- }
78
- }
79
- }
80
- }
88
+ if (isSuccessful) {
89
+ addPackager(packager)
90
+ } else {
91
+ removePackager(packager)
81
92
  }
82
- delay(defaultDelay)
83
93
  }
84
94
  }
95
+
96
+ jobs.awaitAll()
97
+ }
98
+
99
+ private suspend fun fetchedDevelopmentSession() {
100
+ val developmentSession = httpClientService.fetchDevelopmentSession()
101
+ val newPackagers = developmentSession.map { session ->
102
+ PackagerInfo(
103
+ url = session.url,
104
+ description = session.description,
105
+ isDevelopmentSession = true
106
+ )
107
+ }
108
+
109
+ _runningPackagers.update { packagers ->
110
+ // We remove all existing development session packagers and
111
+ // also local packagers with the same port as the new development session packagers
112
+ val filteredPackager = packagers.filter {
113
+ !it.isDevelopmentSession || newPackagers.all { newPackager -> newPackager.port != it.port }
114
+ }
115
+
116
+ filteredPackager.plus(
117
+ developmentSession.map { session ->
118
+ PackagerInfo(
119
+ url = session.url,
120
+ description = session.description,
121
+ isDevelopmentSession = true
122
+ )
123
+ }
124
+ ).toSet()
125
+ }
126
+ }
127
+
128
+ suspend fun refetchedPackager() {
129
+ // It's loading already, so we don't need to do anything.
130
+ if (isLoading.value) {
131
+ return
132
+ }
133
+
134
+ _isLoading.update { true }
135
+ withContext(context = Dispatchers.Default) {
136
+ fetchedDevelopmentSession()
137
+ fetchedLocalPackagers()
138
+ _isLoading.update { false }
139
+ }
85
140
  }
86
141
  }
@@ -25,7 +25,8 @@ sealed interface UserState {
25
25
 
26
26
  class SessionService(
27
27
  val sessionStore: SharedPreferences,
28
- private val apolloClientService: ApolloClientService
28
+ private val apolloClientService: ApolloClientService,
29
+ private val httpClientService: HttpClientService
29
30
  ) {
30
31
  data class User(
31
32
  val isFetching: Boolean = false,
@@ -73,6 +74,7 @@ class SessionService(
73
74
  fun setSession(newSession: Session?) {
74
75
  newSession.saveToPreferences(sessionStore)
75
76
  apolloClientService.setSession(newSession?.sessionSecret)
77
+ httpClientService.setSession(newSession?.sessionSecret)
76
78
  _session.update { newSession }
77
79
  }
78
80
 
@@ -95,6 +97,7 @@ class SessionService(
95
97
  private fun restoreSession(): Session? {
96
98
  val newSession = Session.loadFromPreferences(sessionStore)
97
99
  apolloClientService.setSession(newSession?.sessionSecret)
100
+ httpClientService.setSession(newSession?.sessionSecret)
98
101
  return newSession
99
102
  }
100
103
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "expo-dev-launcher",
3
3
  "title": "Expo Development Launcher",
4
- "version": "5.2.0-canary-20250709-136b77f",
4
+ "version": "5.2.0-canary-20250713-8f814f8",
5
5
  "description": "Pre-release version of the Expo development launcher package for testing.",
6
6
  "repository": {
7
7
  "type": "git",
@@ -15,10 +15,10 @@
15
15
  "license": "MIT",
16
16
  "homepage": "https://docs.expo.dev",
17
17
  "dependencies": {
18
- "expo-dev-menu": "6.2.0-canary-20250709-136b77f",
19
- "expo-manifests": "0.17.0-canary-20250709-136b77f"
18
+ "expo-dev-menu": "6.2.0-canary-20250713-8f814f8",
19
+ "expo-manifests": "0.17.0-canary-20250713-8f814f8"
20
20
  },
21
21
  "peerDependencies": {
22
- "expo": "54.0.0-canary-20250709-136b77f"
22
+ "expo": "54.0.0-canary-20250713-8f814f8"
23
23
  }
24
24
  }