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 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
@@ -20,13 +20,13 @@ expoModule {
20
20
  }
21
21
 
22
22
  group = "host.exp.exponent"
23
- version = "6.0.11"
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.11"
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(canLaunchDevMenuOnStart)
246
+ this.devMenuManager.setCanLaunchDevMenuOnStart(false)
238
247
  }
239
248
 
240
249
  if (!isDevLauncherUrl(uri)) {
@@ -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
  }
@@ -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.`5`
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.validateUrl
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 = url.toUri()
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
- false
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(.black)
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.11",
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.11",
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": "088e79428be97cf3ee11fc93e0e5a1fc1c8bea1e"
24
+ "gitHead": "6523053d0d997d2a21f580d2752b2f873c122038"
25
25
  }