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.
- package/android/build.gradle +2 -2
- package/android/src/debug/java/expo/modules/devlauncher/compose/HomeViewModel.kt +15 -4
- package/android/src/debug/java/expo/modules/devlauncher/compose/screens/HomeScreen.kt +95 -12
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/BottomTabButton.kt +1 -1
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/DevelopmentServerHelp.kt +52 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/RunningAppCard.kt +5 -4
- package/android/src/debug/java/expo/modules/devlauncher/services/ApolloClientService.kt +5 -1
- package/android/src/debug/java/expo/modules/devlauncher/services/DependencyInjection.kt +9 -2
- package/android/src/debug/java/expo/modules/devlauncher/services/HttpClientService.kt +78 -3
- package/android/src/debug/java/expo/modules/devlauncher/services/PackagerService.kt +100 -45
- package/android/src/debug/java/expo/modules/devlauncher/services/SessionService.kt +4 -1
- package/package.json +4 -4
package/android/build.gradle
CHANGED
|
@@ -20,13 +20,13 @@ expoModule {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
group = "host.exp.exponent"
|
|
23
|
-
version = "5.2.0-canary-
|
|
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-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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(
|
|
120
|
-
|
|
121
|
-
|
|
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(
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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.
|
|
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.
|
|
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
|
|
26
|
-
val
|
|
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 = "
|
|
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
|
|
46
|
-
scope: CoroutineScope
|
|
42
|
+
private val httpClientService: HttpClientService
|
|
47
43
|
) {
|
|
48
|
-
private val packagersToCheck = portsToCheck.map { PackagerInfo(hostToCheck
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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-
|
|
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-
|
|
19
|
-
"expo-manifests": "0.17.0-canary-
|
|
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-
|
|
22
|
+
"expo": "54.0.0-canary-20250713-8f814f8"
|
|
23
23
|
}
|
|
24
24
|
}
|