expo-dev-launcher 6.0.11 → 6.0.12
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/CHANGELOG.md +10 -0
- package/android/build.gradle +2 -2
- package/android/src/debug/java/expo/modules/devlauncher/DevLauncherController.kt +10 -1
- package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherBottomTabsNavigator.kt +83 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/primitives/CircularProgressBar.kt +3 -2
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/ServerUrlInput.kt +18 -10
- package/android/src/debug/java/expo/modules/devlauncher/compose/utils/UrlUtils.kt +23 -4
- package/ios/SwiftUI/DevServersView.swift +20 -13
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 6.0.12 — 2025-09-22
|
|
14
|
+
|
|
15
|
+
### 🎉 New features
|
|
16
|
+
|
|
17
|
+
- [Android] Adds loading state when connecting to a development server. ([#39873](https://github.com/expo/expo/pull/39873) by [@lukmccall](https://github.com/lukmccall))
|
|
18
|
+
|
|
19
|
+
### 🐛 Bug fixes
|
|
20
|
+
|
|
21
|
+
- [expo-dev-launcher] Fix manual URL entry: decode percent-encoded URLs, enable return key submit, and support dark mode text. ([#39840](https://github.com/expo/expo/pull/39840) by [@blazejkustra](https://github.com/blazejkustra))
|
|
22
|
+
|
|
13
23
|
## 6.0.11 — 2025-09-11
|
|
14
24
|
|
|
15
25
|
### 🐛 Bug fixes
|
package/android/build.gradle
CHANGED
|
@@ -20,13 +20,13 @@ expoModule {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
group = "host.exp.exponent"
|
|
23
|
-
version = "6.0.
|
|
23
|
+
version = "6.0.12"
|
|
24
24
|
|
|
25
25
|
android {
|
|
26
26
|
namespace "expo.modules.devlauncher"
|
|
27
27
|
defaultConfig {
|
|
28
28
|
versionCode 9
|
|
29
|
-
versionName "6.0.
|
|
29
|
+
versionName "6.0.12"
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
buildTypes {
|
|
@@ -7,6 +7,8 @@ import android.net.Uri
|
|
|
7
7
|
import android.os.Bundle
|
|
8
8
|
import android.util.Log
|
|
9
9
|
import androidx.annotation.UiThread
|
|
10
|
+
import androidx.compose.runtime.State
|
|
11
|
+
import androidx.compose.runtime.mutableStateOf
|
|
10
12
|
import androidx.core.net.toUri
|
|
11
13
|
import com.facebook.react.ReactActivity
|
|
12
14
|
import com.facebook.react.ReactActivityDelegate
|
|
@@ -107,6 +109,10 @@ class DevLauncherController private constructor() :
|
|
|
107
109
|
|
|
108
110
|
private var appIsLoading = false
|
|
109
111
|
|
|
112
|
+
private val _isLoadingToBundler = mutableStateOf(false)
|
|
113
|
+
val isLoadingToBundler: State<Boolean>
|
|
114
|
+
get() = _isLoadingToBundler
|
|
115
|
+
|
|
110
116
|
private var networkInterceptor: DevLauncherNetworkInterceptor? = null
|
|
111
117
|
private var pendingIntentExtras: Bundle? = null
|
|
112
118
|
|
|
@@ -130,6 +136,7 @@ class DevLauncherController private constructor() :
|
|
|
130
136
|
return
|
|
131
137
|
}
|
|
132
138
|
appIsLoading = true
|
|
139
|
+
_isLoadingToBundler.value = true
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
try {
|
|
@@ -163,6 +170,7 @@ class DevLauncherController private constructor() :
|
|
|
163
170
|
lifecycle.addListener(appLoaderListener)
|
|
164
171
|
mode = Mode.APP
|
|
165
172
|
|
|
173
|
+
_isLoadingToBundler.value = false
|
|
166
174
|
// Note that `launch` method is a suspend one. So the execution will be stopped here until the method doesn't finish.
|
|
167
175
|
if (appLoader.launch(appIntent)) {
|
|
168
176
|
recentlyOpedAppsRegistry.appWasOpened(parsedUrl.toString(), devLauncherUrl.queryParams, manifest)
|
|
@@ -179,6 +187,7 @@ class DevLauncherController private constructor() :
|
|
|
179
187
|
} catch (e: Exception) {
|
|
180
188
|
synchronized(this) {
|
|
181
189
|
appIsLoading = false
|
|
190
|
+
_isLoadingToBundler.value = false
|
|
182
191
|
}
|
|
183
192
|
throw e
|
|
184
193
|
}
|
|
@@ -234,7 +243,7 @@ class DevLauncherController private constructor() :
|
|
|
234
243
|
// used by appetize for snack
|
|
235
244
|
if (intent.getBooleanExtra("EXDevMenuDisableAutoLaunch", false)) {
|
|
236
245
|
canLaunchDevMenuOnStart = false
|
|
237
|
-
this.devMenuManager.setCanLaunchDevMenuOnStart(
|
|
246
|
+
this.devMenuManager.setCanLaunchDevMenuOnStart(false)
|
|
238
247
|
}
|
|
239
248
|
|
|
240
249
|
if (!isDevLauncherUrl(uri)) {
|
package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherBottomTabsNavigator.kt
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
1
1
|
package expo.modules.devlauncher.compose
|
|
2
2
|
|
|
3
3
|
import androidx.compose.animation.AnimatedContentTransitionScope
|
|
4
|
+
import androidx.compose.animation.AnimatedVisibility
|
|
4
5
|
import androidx.compose.animation.EnterTransition
|
|
5
6
|
import androidx.compose.animation.ExitTransition
|
|
6
7
|
import androidx.compose.animation.core.tween
|
|
8
|
+
import androidx.compose.animation.fadeIn
|
|
9
|
+
import androidx.compose.animation.fadeOut
|
|
10
|
+
import androidx.compose.animation.slideIn
|
|
11
|
+
import androidx.compose.animation.slideOut
|
|
12
|
+
import androidx.compose.foundation.background
|
|
13
|
+
import androidx.compose.foundation.clickable
|
|
14
|
+
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
15
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
16
|
+
import androidx.compose.foundation.layout.Box
|
|
17
|
+
import androidx.compose.foundation.layout.Row
|
|
18
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
19
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
20
|
+
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
21
|
+
import androidx.compose.foundation.layout.padding
|
|
22
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
7
23
|
import androidx.compose.runtime.Composable
|
|
24
|
+
import androidx.compose.runtime.getValue
|
|
8
25
|
import androidx.compose.runtime.remember
|
|
26
|
+
import androidx.compose.ui.Alignment
|
|
27
|
+
import androidx.compose.ui.Modifier
|
|
28
|
+
import androidx.compose.ui.draw.clip
|
|
29
|
+
import androidx.compose.ui.graphics.Color
|
|
30
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
31
|
+
import androidx.compose.ui.unit.IntOffset
|
|
32
|
+
import androidx.compose.ui.unit.dp
|
|
9
33
|
import androidx.navigation.compose.NavHost
|
|
10
34
|
import androidx.navigation.compose.composable
|
|
11
35
|
import androidx.navigation.compose.rememberNavController
|
|
36
|
+
import expo.modules.devlauncher.DevLauncherController
|
|
37
|
+
import expo.modules.devlauncher.compose.primitives.CircularProgressBar
|
|
12
38
|
import expo.modules.devlauncher.compose.primitives.DefaultScaffold
|
|
13
39
|
import expo.modules.devlauncher.compose.routes.CrashReport
|
|
14
40
|
import expo.modules.devlauncher.compose.routes.CrashReportRoute
|
|
@@ -21,6 +47,8 @@ import expo.modules.devlauncher.compose.routes.UpdatesRoute
|
|
|
21
47
|
import expo.modules.devlauncher.compose.ui.BottomTabBar
|
|
22
48
|
import expo.modules.devlauncher.compose.ui.Full
|
|
23
49
|
import expo.modules.devlauncher.compose.ui.rememberBottomSheetState
|
|
50
|
+
import expo.modules.devmenu.compose.newtheme.NewAppTheme
|
|
51
|
+
import expo.modules.devmenu.compose.primitives.NewText
|
|
24
52
|
|
|
25
53
|
@Composable
|
|
26
54
|
fun DevLauncherBottomTabsNavigator() {
|
|
@@ -106,4 +134,59 @@ fun DevLauncherBottomTabsNavigator() {
|
|
|
106
134
|
ProfileRoute(profileBottomSheetState)
|
|
107
135
|
|
|
108
136
|
DevelopmentServersRoute(developmentServersBottomSheetState)
|
|
137
|
+
|
|
138
|
+
val isVisible by (DevLauncherController.instance as DevLauncherController).isLoadingToBundler
|
|
139
|
+
|
|
140
|
+
AnimatedVisibility(
|
|
141
|
+
visible = isVisible,
|
|
142
|
+
enter = fadeIn(animationSpec = tween(durationMillis = 300)),
|
|
143
|
+
exit = fadeOut(animationSpec = tween(durationMillis = 300))
|
|
144
|
+
) {
|
|
145
|
+
Box(
|
|
146
|
+
modifier = Modifier
|
|
147
|
+
.fillMaxSize()
|
|
148
|
+
.background(Color.Black.copy(alpha = 0.6f))
|
|
149
|
+
.clickable(
|
|
150
|
+
interactionSource = remember { MutableInteractionSource() },
|
|
151
|
+
indication = null,
|
|
152
|
+
onClick = {
|
|
153
|
+
// Captures all clicks to block interaction with the underlying screen
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
) {
|
|
157
|
+
Row(
|
|
158
|
+
horizontalArrangement = Arrangement.SpaceBetween,
|
|
159
|
+
verticalAlignment = Alignment.CenterVertically,
|
|
160
|
+
modifier = Modifier
|
|
161
|
+
.align(Alignment.BottomStart)
|
|
162
|
+
.animateEnterExit(
|
|
163
|
+
enter = slideIn(
|
|
164
|
+
animationSpec = tween(durationMillis = 300),
|
|
165
|
+
initialOffset = { fullSize -> IntOffset(-fullSize.height, fullSize.width) }
|
|
166
|
+
),
|
|
167
|
+
exit = slideOut(
|
|
168
|
+
animationSpec = tween(durationMillis = 300),
|
|
169
|
+
targetOffset = { fullSize -> IntOffset(fullSize.height, fullSize.width) }
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp))
|
|
173
|
+
.background(NewAppTheme.colors.background.element)
|
|
174
|
+
.fillMaxWidth()
|
|
175
|
+
.navigationBarsPadding()
|
|
176
|
+
.padding(NewAppTheme.spacing.`3`)
|
|
177
|
+
.padding(top = NewAppTheme.spacing.`1`)
|
|
178
|
+
) {
|
|
179
|
+
NewText(
|
|
180
|
+
"Connecting to the development server...",
|
|
181
|
+
style = NewAppTheme.font.md.copy(
|
|
182
|
+
fontWeight = FontWeight.SemiBold
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
CircularProgressBar(
|
|
187
|
+
size = 20.dp
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
109
192
|
}
|
package/android/src/debug/java/expo/modules/devlauncher/compose/primitives/CircularProgressBar.kt
CHANGED
|
@@ -25,13 +25,14 @@ import kotlin.time.Duration.Companion.seconds
|
|
|
25
25
|
|
|
26
26
|
@Composable
|
|
27
27
|
fun CircularProgressBar(
|
|
28
|
+
modifier: Modifier = Modifier,
|
|
28
29
|
startAngle: Float = 270f,
|
|
29
30
|
size: Dp = 96.dp,
|
|
30
31
|
strokeWidth: Dp = size / 8,
|
|
31
32
|
duration: Duration = 1.seconds
|
|
32
33
|
) {
|
|
33
34
|
val backgroundColor = NewAppTheme.pallet.gray.`3`
|
|
34
|
-
val progressColor = NewAppTheme.pallet.blue.`
|
|
35
|
+
val progressColor = NewAppTheme.pallet.blue.`8`
|
|
35
36
|
|
|
36
37
|
val transition = rememberInfiniteTransition(label = "infiniteSpinningTransition")
|
|
37
38
|
|
|
@@ -44,7 +45,7 @@ fun CircularProgressBar(
|
|
|
44
45
|
label = "Progress Animation"
|
|
45
46
|
)
|
|
46
47
|
|
|
47
|
-
Canvas(modifier = Modifier.size(size)) {
|
|
48
|
+
Canvas(modifier = Modifier.size(size).then(modifier)) {
|
|
48
49
|
val strokeWidthPx = strokeWidth.toPx()
|
|
49
50
|
val arcSize = size.toPx() - strokeWidthPx
|
|
50
51
|
drawArc(
|
|
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|
|
9
9
|
import androidx.compose.foundation.layout.padding
|
|
10
10
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
11
11
|
import androidx.compose.foundation.text.KeyboardOptions
|
|
12
|
+
import androidx.compose.foundation.text.KeyboardActions
|
|
12
13
|
import androidx.compose.runtime.Composable
|
|
13
14
|
import androidx.compose.runtime.getValue
|
|
14
15
|
import androidx.compose.runtime.mutableStateOf
|
|
@@ -26,7 +27,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|
|
26
27
|
import androidx.compose.ui.unit.dp
|
|
27
28
|
import com.composeunstyled.TextField
|
|
28
29
|
import com.composeunstyled.TextInput
|
|
29
|
-
import expo.modules.devlauncher.compose.utils.
|
|
30
|
+
import expo.modules.devlauncher.compose.utils.sanitizeUrlString
|
|
30
31
|
import expo.modules.devmenu.compose.newtheme.NewAppTheme
|
|
31
32
|
import expo.modules.devmenu.compose.primitives.NewText
|
|
32
33
|
|
|
@@ -37,6 +38,18 @@ fun ServerUrlInput(
|
|
|
37
38
|
var url by remember { mutableStateOf("") }
|
|
38
39
|
val context = LocalContext.current
|
|
39
40
|
|
|
41
|
+
fun connectToURL() {
|
|
42
|
+
val sanitizedURL = sanitizeUrlString(url)
|
|
43
|
+
if (sanitizedURL != null) {
|
|
44
|
+
openApp(sanitizedURL)
|
|
45
|
+
url = ""
|
|
46
|
+
} else {
|
|
47
|
+
Toast
|
|
48
|
+
.makeText(context, "Invalid URL", Toast.LENGTH_SHORT)
|
|
49
|
+
.show()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
Column(
|
|
41
54
|
verticalArrangement = Arrangement.spacedBy(NewAppTheme.spacing.`2`)
|
|
42
55
|
) {
|
|
@@ -63,6 +76,9 @@ fun ServerUrlInput(
|
|
|
63
76
|
autoCorrectEnabled = false,
|
|
64
77
|
keyboardType = KeyboardType.Uri
|
|
65
78
|
),
|
|
79
|
+
keyboardActions = KeyboardActions(
|
|
80
|
+
onDone = { connectToURL() }
|
|
81
|
+
),
|
|
66
82
|
cursorBrush = SolidColor(NewAppTheme.colors.text.default.copy(alpha = 0.9f))
|
|
67
83
|
) {
|
|
68
84
|
TextInput(
|
|
@@ -85,15 +101,7 @@ fun ServerUrlInput(
|
|
|
85
101
|
textStyle = NewAppTheme.font.md.merge(
|
|
86
102
|
fontWeight = FontWeight.SemiBold
|
|
87
103
|
),
|
|
88
|
-
onClick = {
|
|
89
|
-
if (validateUrl(url)) {
|
|
90
|
-
openApp(url)
|
|
91
|
-
} else {
|
|
92
|
-
Toast
|
|
93
|
-
.makeText(context, "Invalid URL", Toast.LENGTH_SHORT)
|
|
94
|
-
.show()
|
|
95
|
-
}
|
|
96
|
-
}
|
|
104
|
+
onClick = { connectToURL() }
|
|
97
105
|
)
|
|
98
106
|
}
|
|
99
107
|
}
|
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
package expo.modules.devlauncher.compose.utils
|
|
2
2
|
|
|
3
3
|
import androidx.core.net.toUri
|
|
4
|
+
import java.net.URLDecoder
|
|
5
|
+
import java.nio.charset.StandardCharsets
|
|
6
|
+
|
|
7
|
+
fun sanitizeUrlString(urlString: String): String? {
|
|
8
|
+
var sanitizedUrl = urlString.trim()
|
|
9
|
+
|
|
10
|
+
val decodedUrl = try {
|
|
11
|
+
URLDecoder.decode(sanitizedUrl, StandardCharsets.UTF_8.toString())
|
|
12
|
+
} catch (_: Exception) {
|
|
13
|
+
sanitizedUrl
|
|
14
|
+
}
|
|
15
|
+
sanitizedUrl = decodedUrl
|
|
16
|
+
|
|
17
|
+
if (!sanitizedUrl.contains("://")) {
|
|
18
|
+
sanitizedUrl = "http://$sanitizedUrl"
|
|
19
|
+
}
|
|
4
20
|
|
|
5
|
-
fun validateUrl(url: String): Boolean {
|
|
6
21
|
return try {
|
|
7
|
-
val uri =
|
|
8
|
-
uri.scheme != null && uri.host != null
|
|
22
|
+
val uri = sanitizedUrl.toUri()
|
|
23
|
+
if (uri.scheme != null && uri.host != null) {
|
|
24
|
+
sanitizedUrl
|
|
25
|
+
} else {
|
|
26
|
+
null
|
|
27
|
+
}
|
|
9
28
|
} catch (_: Throwable) {
|
|
10
|
-
|
|
29
|
+
null
|
|
11
30
|
}
|
|
12
31
|
}
|
|
@@ -8,6 +8,10 @@ import Combine
|
|
|
8
8
|
private func sanitizeUrlString(_ urlString: String) -> String? {
|
|
9
9
|
var sanitizedUrl = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
10
10
|
|
|
11
|
+
if let decodedUrl = sanitizedUrl.removingPercentEncoding {
|
|
12
|
+
sanitizedUrl = decodedUrl
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
if !sanitizedUrl.contains("://") {
|
|
12
16
|
sanitizedUrl = "http://" + sanitizedUrl
|
|
13
17
|
}
|
|
@@ -26,6 +30,19 @@ struct DevServersView: View {
|
|
|
26
30
|
@State private var urlText = ""
|
|
27
31
|
@State private var cancellables = Set<AnyCancellable>()
|
|
28
32
|
|
|
33
|
+
private func connectToURL() {
|
|
34
|
+
if !urlText.isEmpty {
|
|
35
|
+
let sanitizedURL = sanitizeUrlString(urlText)
|
|
36
|
+
if let validURL = sanitizedURL {
|
|
37
|
+
viewModel.openApp(url: validURL)
|
|
38
|
+
withAnimation(.easeInOut(duration: 0.3)) {
|
|
39
|
+
showingURLInput = false
|
|
40
|
+
}
|
|
41
|
+
urlText = ""
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
29
46
|
var body: some View {
|
|
30
47
|
VStack(alignment: .leading, spacing: 12) {
|
|
31
48
|
header
|
|
@@ -81,7 +98,8 @@ struct DevServersView: View {
|
|
|
81
98
|
.disableAutocorrection(true)
|
|
82
99
|
.padding(.horizontal, 16)
|
|
83
100
|
.padding(.vertical, 12)
|
|
84
|
-
.foregroundColor(.
|
|
101
|
+
.foregroundColor(.primary)
|
|
102
|
+
.onSubmit(connectToURL)
|
|
85
103
|
#if !os(tvOS)
|
|
86
104
|
.overlay(
|
|
87
105
|
RoundedRectangle(cornerRadius: 5)
|
|
@@ -124,18 +142,7 @@ struct DevServersView: View {
|
|
|
124
142
|
}
|
|
125
143
|
|
|
126
144
|
private var connectButton: some View {
|
|
127
|
-
Button {
|
|
128
|
-
if !urlText.isEmpty {
|
|
129
|
-
let sanitizedURL = sanitizeUrlString(urlText)
|
|
130
|
-
if let validURL = sanitizedURL {
|
|
131
|
-
viewModel.openApp(url: validURL)
|
|
132
|
-
withAnimation(.easeInOut(duration: 0.3)) {
|
|
133
|
-
showingURLInput = false
|
|
134
|
-
}
|
|
135
|
-
urlText = ""
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
} label: {
|
|
145
|
+
Button(action: connectToURL) {
|
|
139
146
|
Text("Connect")
|
|
140
147
|
.font(.headline)
|
|
141
148
|
.foregroundColor(.white)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-dev-launcher",
|
|
3
3
|
"title": "Expo Development Launcher",
|
|
4
|
-
"version": "6.0.
|
|
4
|
+
"version": "6.0.12",
|
|
5
5
|
"description": "Pre-release version of the Expo development launcher package for testing.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"homepage": "https://docs.expo.dev",
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"expo-dev-menu": "7.0.
|
|
18
|
+
"expo-dev-menu": "7.0.12",
|
|
19
19
|
"expo-manifests": "~1.0.8"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"expo": "*"
|
|
23
23
|
},
|
|
24
|
-
"gitHead": "
|
|
24
|
+
"gitHead": "6523053d0d997d2a21f580d2752b2f873c122038"
|
|
25
25
|
}
|