expo-dev-menu 58.0.6 → 58.0.8
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 +7 -2
- package/android/src/debug/java/expo/modules/devmenu/DevMenuFragment.kt +3 -1
- package/android/src/debug/java/expo/modules/devmenu/fab/MovableFloatingActionButton.kt +2 -0
- package/android/src/debug/java/expo/modules/devmenu/launch/DevMenuLaunchOverrides.kt +22 -0
- package/android/src/debug/java/expo/modules/devmenu/launch/ExpoLauncherUrl.kt +59 -0
- package/android/src/debug/java/expo/modules/devmenu/launch/ExpoLauncherUrlLaunchParams.kt +19 -0
- package/android/src/testDebug/java/expo/modules/devmenu/launch/ExpoLauncherUrlLaunchParamsTest.kt +71 -0
- package/android/src/testDebug/java/expo/modules/devmenu/launch/ExpoLauncherUrlTest.kt +173 -0
- package/expo-dev-menu.podspec +3 -0
- package/ios/DevMenuManager.swift +2 -0
- package/ios/Launch/DevMenuManager+LaunchParams.swift +26 -0
- package/ios/Launch/ExpoLauncherURL.swift +71 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 58.0.8 — 2026-09-28
|
|
14
|
+
|
|
15
|
+
_This version does not introduce any user-facing changes._
|
|
16
|
+
|
|
17
|
+
## 58.0.7 — 2026-09-25
|
|
18
|
+
|
|
19
|
+
### 🎉 New features
|
|
20
|
+
|
|
21
|
+
- Add the `ExpoLauncherURL` parser for the reserved `__expo_*` deep linking URL params. ([#50287](https://github.com/expo/expo/pull/50287) by [@gabrieldonadel](https://github.com/gabrieldonadel))
|
|
22
|
+
|
|
13
23
|
## 58.0.6 — 2026-09-23
|
|
14
24
|
|
|
15
25
|
### 🐛 Bug fixes
|
package/android/build.gradle
CHANGED
|
@@ -12,7 +12,7 @@ apply plugin: 'expo-module-gradle-plugin'
|
|
|
12
12
|
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
|
|
13
13
|
|
|
14
14
|
group = 'host.exp.exponent'
|
|
15
|
-
version = '58.0.
|
|
15
|
+
version = '58.0.8'
|
|
16
16
|
|
|
17
17
|
def hasDevLauncher = findProject(":expo-dev-launcher") != null
|
|
18
18
|
def configureInRelease = findProperty("expo.devmenu.configureInRelease") == "true"
|
|
@@ -29,7 +29,7 @@ android {
|
|
|
29
29
|
|
|
30
30
|
defaultConfig {
|
|
31
31
|
versionCode 10
|
|
32
|
-
versionName '58.0.
|
|
32
|
+
versionName '58.0.8'
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
buildTypes {
|
|
@@ -140,4 +140,9 @@ dependencies {
|
|
|
140
140
|
debugOnlyApi 'io.github.lukmccall:radix-ui-colors:1.0.1'
|
|
141
141
|
|
|
142
142
|
debugOnlyApi "androidx.browser:browser:1.9.0"
|
|
143
|
+
|
|
144
|
+
testImplementation 'androidx.test:core:1.7.0'
|
|
145
|
+
testImplementation "com.google.truth:truth:1.4.5"
|
|
146
|
+
testImplementation 'io.mockk:mockk:1.12.3'
|
|
147
|
+
testImplementation "org.robolectric:robolectric:4.16"
|
|
143
148
|
}
|
|
@@ -34,6 +34,7 @@ import expo.modules.devmenu.detectors.ThreeFingerLongPressDetector
|
|
|
34
34
|
import expo.modules.devmenu.devtools.DevMenuDevToolsDelegate
|
|
35
35
|
import expo.modules.devmenu.fab.MovableFloatingActionButton
|
|
36
36
|
import expo.modules.devmenu.helpers.isAcceptingText
|
|
37
|
+
import expo.modules.devmenu.launch.DevMenuLaunchOverrides
|
|
37
38
|
import expo.modules.kotlin.weak
|
|
38
39
|
import java.lang.ref.WeakReference
|
|
39
40
|
|
|
@@ -68,7 +69,8 @@ class DevMenuFragment(
|
|
|
68
69
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
69
70
|
super.onCreate(savedInstanceState)
|
|
70
71
|
|
|
71
|
-
val shouldShowAtLaunch =
|
|
72
|
+
val shouldShowAtLaunch = DevMenuLaunchOverrides.canLaunchDevMenuOnStart &&
|
|
73
|
+
(preferences.showsAtLaunch || !preferences.isOnboardingFinished)
|
|
72
74
|
if (shouldShowAtLaunch) {
|
|
73
75
|
showMenuAtLaunch()
|
|
74
76
|
}
|
|
@@ -32,6 +32,7 @@ import androidx.compose.ui.unit.Dp
|
|
|
32
32
|
import androidx.compose.ui.unit.DpSize
|
|
33
33
|
import androidx.compose.ui.unit.dp
|
|
34
34
|
import expo.modules.devmenu.compose.DevMenuState
|
|
35
|
+
import expo.modules.devmenu.launch.DevMenuLaunchOverrides
|
|
35
36
|
import kotlinx.coroutines.CoroutineScope
|
|
36
37
|
import kotlinx.coroutines.coroutineScope
|
|
37
38
|
import kotlinx.coroutines.delay
|
|
@@ -96,6 +97,7 @@ fun MovableFloatingActionButton(
|
|
|
96
97
|
val fab = rememberFabState(bounds)
|
|
97
98
|
|
|
98
99
|
val isFabDisplayable = state.showFab &&
|
|
100
|
+
DevMenuLaunchOverrides.canShowFab &&
|
|
99
101
|
!state.isInPictureInPictureMode &&
|
|
100
102
|
bounds.x >= 0f &&
|
|
101
103
|
bounds.y >= 0f
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package expo.modules.devmenu.launch
|
|
2
|
+
|
|
3
|
+
import androidx.compose.runtime.getValue
|
|
4
|
+
import androidx.compose.runtime.mutableStateOf
|
|
5
|
+
import androidx.compose.runtime.setValue
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One-time dev menu switches from the launch URL. They live for the current process only and are never
|
|
9
|
+
* written to the preferences, so the next launch without them shows the menu and the tools button again.
|
|
10
|
+
*/
|
|
11
|
+
object DevMenuLaunchOverrides {
|
|
12
|
+
@Volatile
|
|
13
|
+
var canLaunchDevMenuOnStart = true
|
|
14
|
+
|
|
15
|
+
/** Compose state, so an open app hides the tools button as soon as a launch URL asks for it. */
|
|
16
|
+
var canShowFab by mutableStateOf(true)
|
|
17
|
+
|
|
18
|
+
internal fun reset() {
|
|
19
|
+
canLaunchDevMenuOnStart = true
|
|
20
|
+
canShowFab = true
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package expo.modules.devmenu.launch
|
|
2
|
+
|
|
3
|
+
import android.net.Uri
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parses the reserved `__expo_*` query params of a launch URL. A URL is a launcher command when it
|
|
7
|
+
* carries any `__expo_*` param or its host is the legacy `expo-development-client`.
|
|
8
|
+
* Keep in sync with `packages/expo-dev-menu/ios/Launch/ExpoLauncherURL.swift`.
|
|
9
|
+
*/
|
|
10
|
+
class ExpoLauncherUrl(val url: Uri) {
|
|
11
|
+
private val paramNames: Set<String> = if (url.isHierarchical) url.queryParameterNames else emptySet()
|
|
12
|
+
private val hasReservedParams = paramNames.any { it.startsWith(RESERVED_PREFIX) }
|
|
13
|
+
|
|
14
|
+
val isLegacyHost = url.host == LEGACY_HOST
|
|
15
|
+
val isLauncherCommand = isLegacyHost || hasReservedParams
|
|
16
|
+
|
|
17
|
+
/** From `__expo_url`, or the legacy `url` next to the legacy host. */
|
|
18
|
+
val targetUrl: Uri? = param("url", legacy = "url")?.takeIf { it.isNotEmpty() }?.let(Uri::parse)
|
|
19
|
+
|
|
20
|
+
/** `__expo_disable_onboarding=1`, or the legacy `disableOnboarding=1` next to the legacy host. */
|
|
21
|
+
val disablesOnboarding = param("disable_onboarding", legacy = "disableOnboarding") == "1"
|
|
22
|
+
|
|
23
|
+
/** `__expo_disable_fab=1`: hide the floating tools button for this process. */
|
|
24
|
+
val disablesFab = param("disable_fab") == "1"
|
|
25
|
+
|
|
26
|
+
/** `__expo_disable_auto_launch=1`: do not open the dev menu at launch in this process. */
|
|
27
|
+
val disablesAutoLaunch = param("disable_auto_launch") == "1"
|
|
28
|
+
|
|
29
|
+
/** The URL without its `__expo_*` params. */
|
|
30
|
+
val strippedUrl: Uri = if (!hasReservedParams) {
|
|
31
|
+
url
|
|
32
|
+
} else {
|
|
33
|
+
val remaining = url.encodedQuery.orEmpty().split('&')
|
|
34
|
+
.filter { it.isNotEmpty() && !Uri.decode(it.substringBefore('=')).startsWith(RESERVED_PREFIX) }
|
|
35
|
+
url.buildUpon().encodedQuery(remaining.joinToString("&").ifEmpty { null }).build()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The other query params, decoded. */
|
|
39
|
+
val passthroughParams: Map<String, String> = paramNames
|
|
40
|
+
.filterNot { it.startsWith(RESERVED_PREFIX) }
|
|
41
|
+
.associateWith { url.getQueryParameter(it).orEmpty() }
|
|
42
|
+
|
|
43
|
+
/** Whether [strippedUrl] still points somewhere an app can route: a host or a path. */
|
|
44
|
+
val remainderHasDestination: Boolean =
|
|
45
|
+
strippedUrl.host.let { !it.isNullOrEmpty() && it != LEGACY_HOST } ||
|
|
46
|
+
strippedUrl.path.let { !it.isNullOrEmpty() && it != "/" }
|
|
47
|
+
|
|
48
|
+
/** `__expo_<name>`, or the [legacy] param next to the legacy host. */
|
|
49
|
+
private fun param(name: String, legacy: String? = null): String? {
|
|
50
|
+
if (!url.isHierarchical) return null
|
|
51
|
+
return url.getQueryParameter(RESERVED_PREFIX + name)
|
|
52
|
+
?: legacy?.takeIf { isLegacyHost }?.let(url::getQueryParameter)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
companion object {
|
|
56
|
+
const val RESERVED_PREFIX = "__expo_"
|
|
57
|
+
const val LEGACY_HOST = "expo-development-client"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package expo.modules.devmenu.launch
|
|
2
|
+
|
|
3
|
+
import expo.modules.devmenu.DevMenuPreferences
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Applies the dev menu params of a launch URL. `__expo_disable_onboarding` finishes onboarding in the
|
|
7
|
+
* saved preferences; the other two are one-time switches for this process, see [DevMenuLaunchOverrides].
|
|
8
|
+
*/
|
|
9
|
+
fun ExpoLauncherUrl.applyDevMenuLaunchParams(preferences: DevMenuPreferences) {
|
|
10
|
+
if (disablesOnboarding) {
|
|
11
|
+
preferences.isOnboardingFinished = true
|
|
12
|
+
}
|
|
13
|
+
if (disablesFab) {
|
|
14
|
+
DevMenuLaunchOverrides.canShowFab = false
|
|
15
|
+
}
|
|
16
|
+
if (disablesAutoLaunch) {
|
|
17
|
+
DevMenuLaunchOverrides.canLaunchDevMenuOnStart = false
|
|
18
|
+
}
|
|
19
|
+
}
|
package/android/src/testDebug/java/expo/modules/devmenu/launch/ExpoLauncherUrlLaunchParamsTest.kt
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
package expo.modules.devmenu.launch
|
|
2
|
+
|
|
3
|
+
import android.net.Uri
|
|
4
|
+
import com.google.common.truth.Truth.assertThat
|
|
5
|
+
import expo.modules.devmenu.DevMenuPreferences
|
|
6
|
+
import io.mockk.mockk
|
|
7
|
+
import io.mockk.verify
|
|
8
|
+
import org.junit.Before
|
|
9
|
+
import org.junit.Test
|
|
10
|
+
import org.junit.runner.RunWith
|
|
11
|
+
import org.robolectric.RobolectricTestRunner
|
|
12
|
+
|
|
13
|
+
@RunWith(RobolectricTestRunner::class)
|
|
14
|
+
internal class ExpoLauncherUrlLaunchParamsTest {
|
|
15
|
+
private val preferences = mockk<DevMenuPreferences>(relaxed = true)
|
|
16
|
+
|
|
17
|
+
@Before
|
|
18
|
+
fun reset() {
|
|
19
|
+
DevMenuLaunchOverrides.reset()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Test
|
|
23
|
+
fun `disable fab hides the fab for this process only`() {
|
|
24
|
+
ExpoLauncherUrl(Uri.parse("exp://h:8081?__expo_disable_fab=1")).applyDevMenuLaunchParams(preferences)
|
|
25
|
+
|
|
26
|
+
assertThat(DevMenuLaunchOverrides.canShowFab).isFalse()
|
|
27
|
+
assertThat(DevMenuLaunchOverrides.canLaunchDevMenuOnStart).isTrue()
|
|
28
|
+
verify(exactly = 0) { preferences.showFab = any() }
|
|
29
|
+
verify(exactly = 0) { preferences.showsAtLaunch = any() }
|
|
30
|
+
verify(exactly = 0) { preferences.isOnboardingFinished = any() }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Test
|
|
34
|
+
fun `disable auto launch keeps the menu closed for this process only`() {
|
|
35
|
+
ExpoLauncherUrl(Uri.parse("exp://h:8081?__expo_disable_auto_launch=1")).applyDevMenuLaunchParams(preferences)
|
|
36
|
+
|
|
37
|
+
assertThat(DevMenuLaunchOverrides.canLaunchDevMenuOnStart).isFalse()
|
|
38
|
+
assertThat(DevMenuLaunchOverrides.canShowFab).isTrue()
|
|
39
|
+
verify(exactly = 0) { preferences.showsAtLaunch = any() }
|
|
40
|
+
verify(exactly = 0) { preferences.isOnboardingFinished = any() }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Test
|
|
44
|
+
fun `disable onboarding finishes onboarding in the preferences`() {
|
|
45
|
+
ExpoLauncherUrl(Uri.parse("exp://h:8081?__expo_disable_onboarding=1")).applyDevMenuLaunchParams(preferences)
|
|
46
|
+
|
|
47
|
+
verify(exactly = 1) { preferences.isOnboardingFinished = true }
|
|
48
|
+
assertThat(DevMenuLaunchOverrides.canShowFab).isTrue()
|
|
49
|
+
assertThat(DevMenuLaunchOverrides.canLaunchDevMenuOnStart).isTrue()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
fun `legacy disableFab and disableAutoLaunch are not applied here`() {
|
|
54
|
+
ExpoLauncherUrl(Uri.parse("scheme://expo-development-client/?url=http%3A%2F%2Fh%3A8081&disableFab=1&disableAutoLaunch=1"))
|
|
55
|
+
.applyDevMenuLaunchParams(preferences)
|
|
56
|
+
|
|
57
|
+
assertThat(DevMenuLaunchOverrides.canShowFab).isTrue()
|
|
58
|
+
assertThat(DevMenuLaunchOverrides.canLaunchDevMenuOnStart).isTrue()
|
|
59
|
+
verify(exactly = 0) { preferences.showFab = any() }
|
|
60
|
+
verify(exactly = 0) { preferences.showsAtLaunch = any() }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Test
|
|
64
|
+
fun `a plain url changes nothing`() {
|
|
65
|
+
ExpoLauncherUrl(Uri.parse("exp://h:8081?x=1")).applyDevMenuLaunchParams(preferences)
|
|
66
|
+
|
|
67
|
+
assertThat(DevMenuLaunchOverrides.canShowFab).isTrue()
|
|
68
|
+
assertThat(DevMenuLaunchOverrides.canLaunchDevMenuOnStart).isTrue()
|
|
69
|
+
verify(exactly = 0) { preferences.isOnboardingFinished = any() }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
package expo.modules.devmenu.launch
|
|
2
|
+
|
|
3
|
+
import android.net.Uri
|
|
4
|
+
import com.google.common.truth.Truth.assertThat
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
import org.junit.runner.RunWith
|
|
7
|
+
import org.robolectric.RobolectricTestRunner
|
|
8
|
+
|
|
9
|
+
@RunWith(RobolectricTestRunner::class)
|
|
10
|
+
internal class ExpoLauncherUrlTest {
|
|
11
|
+
@Test
|
|
12
|
+
fun `legacy host with url and disableOnboarding`() {
|
|
13
|
+
val raw = "exp+slug://expo-development-client/?url=http%3A%2F%2F10.0.0.5%3A8081&disableOnboarding=1"
|
|
14
|
+
val launch = ExpoLauncherUrl(Uri.parse(raw))
|
|
15
|
+
|
|
16
|
+
assertThat(launch.isLauncherCommand).isTrue()
|
|
17
|
+
assertThat(launch.isLegacyHost).isTrue()
|
|
18
|
+
assertThat(launch.targetUrl).isEqualTo(Uri.parse("http://10.0.0.5:8081"))
|
|
19
|
+
assertThat(launch.disablesOnboarding).isTrue()
|
|
20
|
+
assertThat(launch.disablesFab).isFalse()
|
|
21
|
+
assertThat(launch.disablesAutoLaunch).isFalse()
|
|
22
|
+
// The legacy `url=` form is kept intact so apps and expo-router keep working.
|
|
23
|
+
assertThat(launch.strippedUrl.toString()).isEqualTo(raw)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Test
|
|
27
|
+
fun `new shape with every reserved param`() {
|
|
28
|
+
val launch = ExpoLauncherUrl(
|
|
29
|
+
Uri.parse(
|
|
30
|
+
"exp+slug://?__expo_url=http%3A%2F%2F10.0.0.5%3A8081" +
|
|
31
|
+
"&__expo_disable_fab=1&__expo_disable_auto_launch=1&__expo_disable_onboarding=1"
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
assertThat(launch.isLauncherCommand).isTrue()
|
|
36
|
+
assertThat(launch.isLegacyHost).isFalse()
|
|
37
|
+
assertThat(launch.targetUrl).isEqualTo(Uri.parse("http://10.0.0.5:8081"))
|
|
38
|
+
assertThat(launch.disablesFab).isTrue()
|
|
39
|
+
assertThat(launch.disablesAutoLaunch).isTrue()
|
|
40
|
+
assertThat(launch.disablesOnboarding).isTrue()
|
|
41
|
+
assertThat(launch.remainderHasDestination).isFalse()
|
|
42
|
+
assertThat(launch.strippedUrl.query).isNull()
|
|
43
|
+
assertThat(launch.passthroughParams).isEmpty()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test
|
|
47
|
+
fun `app deep link carrying a reserved param`() {
|
|
48
|
+
val launch = ExpoLauncherUrl(Uri.parse("myapp://login?__expo_disable_fab=1&x=1"))
|
|
49
|
+
|
|
50
|
+
assertThat(launch.isLauncherCommand).isTrue()
|
|
51
|
+
assertThat(launch.targetUrl).isNull()
|
|
52
|
+
assertThat(launch.disablesFab).isTrue()
|
|
53
|
+
assertThat(launch.remainderHasDestination).isTrue()
|
|
54
|
+
assertThat(launch.strippedUrl.toString()).isEqualTo("myapp://login?x=1")
|
|
55
|
+
assertThat(launch.passthroughParams).containsExactly("x", "1")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Test
|
|
59
|
+
fun `new shape without a destination`() {
|
|
60
|
+
val launch = ExpoLauncherUrl(Uri.parse("myapp://?__expo_url=http%3A%2F%2Flocalhost%3A8081"))
|
|
61
|
+
|
|
62
|
+
assertThat(launch.targetUrl).isEqualTo(Uri.parse("http://localhost:8081"))
|
|
63
|
+
assertThat(launch.remainderHasDestination).isFalse()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Test
|
|
67
|
+
fun `plain app deep link is not a launcher command`() {
|
|
68
|
+
val raw = "myapp://login?x=1"
|
|
69
|
+
val launch = ExpoLauncherUrl(Uri.parse(raw))
|
|
70
|
+
|
|
71
|
+
assertThat(launch.isLauncherCommand).isFalse()
|
|
72
|
+
assertThat(launch.targetUrl).isNull()
|
|
73
|
+
assertThat(launch.strippedUrl.toString()).isEqualTo(raw)
|
|
74
|
+
assertThat(launch.passthroughParams).containsExactly("x", "1")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Test
|
|
78
|
+
fun `expo go url keeps the other params`() {
|
|
79
|
+
val launch = ExpoLauncherUrl(Uri.parse("exp://h:8081/--/p?__expo_disable_fab=1&x=1"))
|
|
80
|
+
|
|
81
|
+
assertThat(launch.isLauncherCommand).isTrue()
|
|
82
|
+
assertThat(launch.disablesFab).isTrue()
|
|
83
|
+
assertThat(launch.targetUrl).isNull()
|
|
84
|
+
assertThat(launch.remainderHasDestination).isTrue()
|
|
85
|
+
assertThat(launch.strippedUrl.toString()).isEqualTo("exp://h:8081/--/p?x=1")
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@Test
|
|
89
|
+
fun `params inside the target url are ignored`() {
|
|
90
|
+
val launch = ExpoLauncherUrl(
|
|
91
|
+
Uri.parse("exp+slug://?__expo_url=http%3A%2F%2Flocalhost%3A8081%2F%3F__expo_disable_fab%3D1")
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
assertThat(launch.disablesFab).isFalse()
|
|
95
|
+
assertThat(launch.targetUrl).isEqualTo(Uri.parse("http://localhost:8081/?__expo_disable_fab=1"))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@Test
|
|
99
|
+
fun `only exact values act`() {
|
|
100
|
+
val launch = ExpoLauncherUrl(
|
|
101
|
+
Uri.parse("exp://h:8081?__expo_disable_fab=0&__expo_disable_auto_launch=true&__expo_disable_onboarding=yes")
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
assertThat(launch.isLauncherCommand).isTrue()
|
|
105
|
+
assertThat(launch.disablesFab).isFalse()
|
|
106
|
+
assertThat(launch.disablesAutoLaunch).isFalse()
|
|
107
|
+
assertThat(launch.disablesOnboarding).isFalse()
|
|
108
|
+
assertThat(launch.strippedUrl.query).isNull()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@Test
|
|
112
|
+
fun `unknown reserved params are stripped`() {
|
|
113
|
+
val launch = ExpoLauncherUrl(Uri.parse("exp://h:8081?__expo_foo=1&x=1"))
|
|
114
|
+
|
|
115
|
+
assertThat(launch.isLauncherCommand).isTrue()
|
|
116
|
+
assertThat(launch.strippedUrl.toString()).isEqualTo("exp://h:8081?x=1")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Test
|
|
120
|
+
fun `legacy aliases only apply on the legacy host`() {
|
|
121
|
+
val raw = "exp://h:8081?disableOnboarding=1&disableFab=1&disableAutoLaunch=1&url=http%3A%2F%2Fother"
|
|
122
|
+
val launch = ExpoLauncherUrl(Uri.parse(raw))
|
|
123
|
+
|
|
124
|
+
assertThat(launch.isLauncherCommand).isFalse()
|
|
125
|
+
assertThat(launch.disablesOnboarding).isFalse()
|
|
126
|
+
assertThat(launch.disablesFab).isFalse()
|
|
127
|
+
assertThat(launch.disablesAutoLaunch).isFalse()
|
|
128
|
+
assertThat(launch.targetUrl).isNull()
|
|
129
|
+
assertThat(launch.strippedUrl.toString()).isEqualTo(raw)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@Test
|
|
133
|
+
fun `legacy host with url only`() {
|
|
134
|
+
val raw = "exp+slug://expo-development-client/?url=http%3A%2F%2F10.0.0.5%3A8081"
|
|
135
|
+
val launch = ExpoLauncherUrl(Uri.parse(raw))
|
|
136
|
+
|
|
137
|
+
assertThat(launch.isLauncherCommand).isTrue()
|
|
138
|
+
assertThat(launch.isLegacyHost).isTrue()
|
|
139
|
+
assertThat(launch.targetUrl).isEqualTo(Uri.parse("http://10.0.0.5:8081"))
|
|
140
|
+
assertThat(launch.disablesOnboarding).isFalse()
|
|
141
|
+
assertThat(launch.disablesFab).isFalse()
|
|
142
|
+
assertThat(launch.disablesAutoLaunch).isFalse()
|
|
143
|
+
assertThat(launch.strippedUrl.toString()).isEqualTo(raw)
|
|
144
|
+
assertThat(launch.passthroughParams).containsExactly("url", "http://10.0.0.5:8081")
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@Test
|
|
148
|
+
fun `legacy disableFab and disableAutoLaunch are left to the launcher`() {
|
|
149
|
+
val launch = ExpoLauncherUrl(
|
|
150
|
+
Uri.parse("exp+slug://expo-development-client/?url=http%3A%2F%2F10.0.0.5%3A8081&disableFab=1&disableAutoLaunch=1")
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
assertThat(launch.disablesFab).isFalse()
|
|
154
|
+
assertThat(launch.disablesAutoLaunch).isFalse()
|
|
155
|
+
assertThat(launch.passthroughParams).containsEntry("disableFab", "1")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@Test
|
|
159
|
+
fun `opaque uri does not throw`() {
|
|
160
|
+
val launch = ExpoLauncherUrl(Uri.parse("mailto:a@b.c"))
|
|
161
|
+
|
|
162
|
+
assertThat(launch.isLauncherCommand).isFalse()
|
|
163
|
+
assertThat(launch.targetUrl).isNull()
|
|
164
|
+
assertThat(launch.strippedUrl.toString()).isEqualTo("mailto:a@b.c")
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@Test
|
|
168
|
+
fun `preserves percent encoding of the other params`() {
|
|
169
|
+
val launch = ExpoLauncherUrl(Uri.parse("exp://h:8081/?snack-channel=a%2Bb&__expo_disable_fab=1"))
|
|
170
|
+
|
|
171
|
+
assertThat(launch.strippedUrl.toString()).isEqualTo("exp://h:8081/?snack-channel=a%2Bb")
|
|
172
|
+
}
|
|
173
|
+
}
|
package/expo-dev-menu.podspec
CHANGED
|
@@ -112,6 +112,9 @@ Pod::Spec.new do |s|
|
|
|
112
112
|
test_spec.dependency 'React-CoreModules'
|
|
113
113
|
# ExpoModulesCore requires React-hermes or React-jsc in tests, add ExpoModulesTestCore for the underlying dependencies
|
|
114
114
|
test_spec.dependency 'ExpoModulesTestCore'
|
|
115
|
+
test_spec.pod_target_xcconfig = {
|
|
116
|
+
'OTHER_LDFLAGS' => '$(inherited) -lc++'
|
|
117
|
+
}
|
|
115
118
|
test_spec.platforms = {
|
|
116
119
|
:ios => '16.4'
|
|
117
120
|
}
|
package/ios/DevMenuManager.swift
CHANGED
|
@@ -84,6 +84,7 @@ open class DevMenuManager: NSObject {
|
|
|
84
84
|
|
|
85
85
|
var packagerConnectionHandler: DevMenuPackagerConnectionHandler?
|
|
86
86
|
var canLaunchDevMenuOnStart = true
|
|
87
|
+
var canShowFloatingActionButton = true
|
|
87
88
|
@objc public var isReactAppRunning = false
|
|
88
89
|
|
|
89
90
|
private let appContextLock = NSLock()
|
|
@@ -617,6 +618,7 @@ open class DevMenuManager: NSObject {
|
|
|
617
618
|
}
|
|
618
619
|
|
|
619
620
|
let shouldShow = DevMenuPreferences.showFloatingActionButton
|
|
621
|
+
&& self.canShowFloatingActionButton
|
|
620
622
|
&& !self.isVisible
|
|
621
623
|
&& self.isReactAppRunning
|
|
622
624
|
&& !self.isNavigatingHome
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
extension DevMenuManager {
|
|
6
|
+
/**
|
|
7
|
+
Applies the dev menu params of a launch URL. `__expo_disable_onboarding=1` finishes onboarding in the
|
|
8
|
+
saved preferences. `__expo_disable_fab=1` and `__expo_disable_auto_launch=1` are one-time switches for
|
|
9
|
+
this process and never touch the preferences.
|
|
10
|
+
*/
|
|
11
|
+
@objc(applyLaunchParamsFromURL:)
|
|
12
|
+
public func applyLaunchParams(from url: URL) {
|
|
13
|
+
let launch = ExpoLauncherURL(url)
|
|
14
|
+
if launch.disablesOnboarding {
|
|
15
|
+
DevMenuPreferences.isOnboardingFinished = true
|
|
16
|
+
}
|
|
17
|
+
if launch.disablesFab {
|
|
18
|
+
canShowFloatingActionButton = false
|
|
19
|
+
updateFABVisibility()
|
|
20
|
+
}
|
|
21
|
+
if launch.disablesAutoLaunch {
|
|
22
|
+
canLaunchDevMenuOnStart = false
|
|
23
|
+
updateAutoLaunchObserver()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
Parses the reserved `__expo_*` query params of a launch URL. A URL is a launcher command when it
|
|
7
|
+
carries any `__expo_*` param or its host is the legacy `expo-development-client`.
|
|
8
|
+
Keep in sync with `packages/expo-dev-menu/android/src/debug/java/expo/modules/devmenu/launch/ExpoLauncherUrl.kt`.
|
|
9
|
+
*/
|
|
10
|
+
public struct ExpoLauncherURL {
|
|
11
|
+
public static let reservedPrefix = "__expo_"
|
|
12
|
+
public static let legacyHost = "expo-development-client"
|
|
13
|
+
|
|
14
|
+
public let url: URL
|
|
15
|
+
public let isLegacyHost: Bool
|
|
16
|
+
public let isLauncherCommand: Bool
|
|
17
|
+
/// From `__expo_url`, or the legacy `url` next to the legacy host.
|
|
18
|
+
public let targetURL: URL?
|
|
19
|
+
/// `__expo_disable_onboarding=1`, or the legacy `disableOnboarding=1` next to the legacy host.
|
|
20
|
+
public let disablesOnboarding: Bool
|
|
21
|
+
/// `__expo_disable_fab=1`: hide the floating tools button for this process.
|
|
22
|
+
public let disablesFab: Bool
|
|
23
|
+
/// `__expo_disable_auto_launch=1`: do not open the dev menu at launch in this process.
|
|
24
|
+
public let disablesAutoLaunch: Bool
|
|
25
|
+
/// The URL without its `__expo_*` params.
|
|
26
|
+
public let strippedURL: URL
|
|
27
|
+
/// The other query params, percent-decoded.
|
|
28
|
+
public let passthroughParams: [String: String]
|
|
29
|
+
/// Whether `strippedURL` still points somewhere an app can route: a host or a path.
|
|
30
|
+
public let remainderHasDestination: Bool
|
|
31
|
+
|
|
32
|
+
public init(_ url: URL) {
|
|
33
|
+
let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []
|
|
34
|
+
let isLegacyHost = url.host == Self.legacyHost
|
|
35
|
+
let isReserved = { (item: URLQueryItem) in item.name.hasPrefix(Self.reservedPrefix) }
|
|
36
|
+
// `__expo_<name>`, or the legacy param next to the legacy host.
|
|
37
|
+
func param(_ name: String, legacy: String? = nil) -> String? {
|
|
38
|
+
if let value = items.first(where: { $0.name == Self.reservedPrefix + name })?.value {
|
|
39
|
+
return value
|
|
40
|
+
}
|
|
41
|
+
guard isLegacyHost, let legacy else {
|
|
42
|
+
return nil
|
|
43
|
+
}
|
|
44
|
+
return items.first(where: { $0.name == legacy })?.value
|
|
45
|
+
}
|
|
46
|
+
let strippedURL = items.contains(where: isReserved) ? Self.strippingReservedParams(from: url) : url
|
|
47
|
+
let host = strippedURL.host ?? ""
|
|
48
|
+
|
|
49
|
+
self.url = url
|
|
50
|
+
self.isLegacyHost = isLegacyHost
|
|
51
|
+
self.isLauncherCommand = isLegacyHost || items.contains(where: isReserved)
|
|
52
|
+
self.targetURL = param("url", legacy: "url").flatMap { $0.isEmpty ? nil : URL(string: $0) }
|
|
53
|
+
self.disablesOnboarding = param("disable_onboarding", legacy: "disableOnboarding") == "1"
|
|
54
|
+
self.disablesFab = param("disable_fab") == "1"
|
|
55
|
+
self.disablesAutoLaunch = param("disable_auto_launch") == "1"
|
|
56
|
+
self.strippedURL = strippedURL
|
|
57
|
+
self.passthroughParams = items.filter { !isReserved($0) }.reduce(into: [:]) { $0[$1.name] = $1.value ?? "" }
|
|
58
|
+
self.remainderHasDestination = (!host.isEmpty && host != Self.legacyHost)
|
|
59
|
+
|| (!strippedURL.path.isEmpty && strippedURL.path != "/")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private static func strippingReservedParams(from url: URL) -> URL {
|
|
63
|
+
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
|
64
|
+
let items = components.percentEncodedQueryItems else {
|
|
65
|
+
return url
|
|
66
|
+
}
|
|
67
|
+
let remaining = items.filter { !($0.name.removingPercentEncoding ?? $0.name).hasPrefix(reservedPrefix) }
|
|
68
|
+
components.percentEncodedQueryItems = remaining.isEmpty ? nil : remaining
|
|
69
|
+
return components.url ?? url
|
|
70
|
+
}
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-dev-menu",
|
|
3
|
-
"version": "58.0.
|
|
3
|
+
"version": "58.0.8",
|
|
4
4
|
"description": "Expo/React Native module with the developer menu.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dev",
|
|
@@ -31,16 +31,16 @@
|
|
|
31
31
|
"@testing-library/react-native": "^13.3.0",
|
|
32
32
|
"@types/jest": "^29.2.1",
|
|
33
33
|
"@types/node": "^22.14.0",
|
|
34
|
-
"babel-preset-expo": "58.0.
|
|
34
|
+
"babel-preset-expo": "58.0.5",
|
|
35
35
|
"expo-module-scripts": "56.0.3",
|
|
36
36
|
"react": "19.3.0",
|
|
37
|
-
"react-native": "0.88.0-rc.
|
|
37
|
+
"react-native": "0.88.0-rc.2"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"expo": "*",
|
|
41
41
|
"react-native": "*"
|
|
42
42
|
},
|
|
43
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "74f6d2b90bc50bd8df7b160d6e6b2204b37a8ce7",
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "pnpm run build:lib && pnpm run build:plugin",
|
|
46
46
|
"build:lib": "expo-build src",
|