expo-dev-launcher 5.2.0-canary-20250630-547cd82 → 5.2.0-canary-20250709-136b77f
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 +9 -0
- package/android/build.gradle +15 -2
- package/android/src/debug/AndroidManifest.xml +6 -1
- package/android/src/debug/java/expo/modules/devlauncher/compose/AuthActivity.kt +136 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/BindingView.kt +1 -6
- package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherBottomTabsNavigator.kt +12 -31
- package/android/src/debug/java/expo/modules/devlauncher/compose/HomeViewModel.kt +84 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/ProfileViewModel.kt +81 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/Session.kt +33 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/primitives/Accordion.kt +2 -2
- package/android/src/debug/java/expo/modules/devlauncher/compose/primitives/Asyncimage.kt +40 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/routes/Home.kt +23 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/routes/Profile.kt +70 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/routes/Settings.kt +16 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/screens/{Home.kt → HomeScreen.kt} +29 -17
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/AccountAvatar.kt +37 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/AccountSelector.kt +70 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/ActionButton.kt +35 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/AppHeader.kt +26 -18
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/BottomTabBar.kt +3 -3
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/ProfileLayout.kt +45 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/RunningAppCard.kt +91 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/SectionHeader.kt +6 -16
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/SignUp.kt +49 -0
- package/android/src/debug/java/expo/modules/devlauncher/services/ApolloClientService.kt +41 -0
- package/android/src/debug/java/expo/modules/devlauncher/services/DependencyInjection.kt +79 -0
- package/android/src/debug/java/expo/modules/devlauncher/services/HttpClientService.kt +9 -0
- package/android/src/debug/java/expo/modules/devlauncher/services/ImageLoaderService.kt +69 -0
- package/android/src/debug/java/expo/modules/devlauncher/services/PackagerService.kt +4 -0
- package/android/src/debug/java/expo/modules/devlauncher/services/SessionService.kt +114 -0
- package/android/src/main/graphql/GetBranches.graphql +32 -0
- package/android/src/main/graphql/GetUpdates.graphql +34 -0
- package/android/src/main/graphql/Me.graphql +18 -0
- package/android/src/main/graphql/schema.graphqls +9850 -0
- package/ios/Assets.xcassets/branch-icon.imageset/Contents.json +56 -0
- package/ios/Assets.xcassets/branch-icon.imageset/branch-icon-light.png +0 -0
- package/ios/Assets.xcassets/branch-icon.imageset/branch-icon-light@2x.png +0 -0
- package/ios/Assets.xcassets/branch-icon.imageset/branch-icon-light@3x.png +0 -0
- package/ios/Assets.xcassets/branch-icon.imageset/branch-icon.png +0 -0
- package/ios/Assets.xcassets/branch-icon.imageset/branch-icon@2x.png +0 -0
- package/ios/Assets.xcassets/branch-icon.imageset/branch-icon@3x.png +0 -0
- package/ios/Assets.xcassets/update-icon.imageset/Contents.json +56 -0
- package/ios/Assets.xcassets/update-icon.imageset/update-icon-light.png +0 -0
- package/ios/Assets.xcassets/update-icon.imageset/update-icon-light@2x.png +0 -0
- package/ios/Assets.xcassets/update-icon.imageset/update-icon-light@3x.png +0 -0
- package/ios/Assets.xcassets/update-icon.imageset/update-icon.png +0 -0
- package/ios/Assets.xcassets/update-icon.imageset/update-icon@2x.png +0 -0
- package/ios/Assets.xcassets/update-icon.imageset/update-icon@3x.png +0 -0
- package/ios/EXDevLauncherController.m +2 -4
- package/ios/SwiftUI/AccountSheet.swift +173 -89
- package/ios/SwiftUI/Data.swift +34 -0
- package/ios/SwiftUI/DevLauncherErrors.swift +38 -0
- package/ios/SwiftUI/DevLauncherViewModel.swift +119 -62
- package/ios/SwiftUI/{DevLauncherSwiftUIViews.swift → DevLauncherViews.swift} +3 -68
- package/ios/SwiftUI/DevServersView.swift +26 -17
- package/ios/SwiftUI/GraphQL/APIClient.swift +83 -0
- package/ios/SwiftUI/GraphQL/Models.swift +116 -0
- package/ios/SwiftUI/GraphQL/Queries.swift +150 -0
- package/ios/SwiftUI/HomeTabView.swift +5 -3
- package/ios/SwiftUI/Navigation/Navigation.swift +58 -3
- package/ios/SwiftUI/SettingsTabView.swift +70 -0
- package/ios/SwiftUI/UpdatesTab/NotSignedInView.swift +32 -0
- package/ios/SwiftUI/UpdatesTab/NotUsingUpdatesView.swift +38 -0
- package/ios/SwiftUI/UpdatesTab/UpdateRow.swift +76 -0
- package/ios/SwiftUI/UpdatesTab/UpdateUtils.swift +5 -0
- package/ios/SwiftUI/UpdatesTab/UpdatesListView.swift +196 -0
- package/ios/SwiftUI/UpdatesTab/UpdatesTabView.swift +37 -0
- package/ios/SwiftUI/Utils/Avatar.swift +66 -0
- package/ios/SwiftUI/Utils/Utils.swift +22 -0
- package/package.json +4 -4
- package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherViewModel.kt +0 -11
- package/android/src/debug/java/expo/modules/devlauncher/compose/screens/SignUp.kt +0 -100
- package/ios/SwiftUI/ExtensionsTabView.swift +0 -68
- /package/android/src/debug/java/expo/modules/devlauncher/compose/screens/{Settings.kt → SettingsScreen.kt} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
### 🐛 Bug fixes
|
|
12
12
|
|
|
13
13
|
- [iOS] Fix missing CDP headers when using static frameworks. ([#37448](https://github.com/expo/expo/pull/37448) by [@alanjhughes](https://github.com/alanjhughes))
|
|
14
|
+
- [Android] Use same strings in UI as iOS. ([#37786](https://github.com/expo/expo/pull/37786) by [@douglowder](https://github.com/douglowder))
|
|
14
15
|
|
|
15
16
|
### 💡 Others
|
|
16
17
|
|
|
@@ -20,6 +21,14 @@
|
|
|
20
21
|
|
|
21
22
|
- Added support for React Native 0.80.x. ([#37400](https://github.com/expo/expo/pull/37400) by [@gabrieldonadel](https://github.com/gabrieldonadel))
|
|
22
23
|
|
|
24
|
+
## 5.1.16 - 2025-07-03
|
|
25
|
+
|
|
26
|
+
_This version does not introduce any user-facing changes._
|
|
27
|
+
|
|
28
|
+
## 5.1.15 - 2025-07-02
|
|
29
|
+
|
|
30
|
+
_This version does not introduce any user-facing changes._
|
|
31
|
+
|
|
23
32
|
## 5.1.14 - 2025-06-26
|
|
24
33
|
|
|
25
34
|
### 🐛 Bug fixes
|
package/android/build.gradle
CHANGED
|
@@ -5,6 +5,7 @@ buildscript {
|
|
|
5
5
|
dependencies {
|
|
6
6
|
classpath("org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:${kotlinVersion}")
|
|
7
7
|
classpath("org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin:${kotlinVersion}")
|
|
8
|
+
classpath("com.apollographql.apollo:apollo-gradle-plugin:4.3.1")
|
|
8
9
|
}
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -12,19 +13,20 @@ apply plugin: 'com.android.library'
|
|
|
12
13
|
apply plugin: 'expo-module-gradle-plugin'
|
|
13
14
|
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
|
|
14
15
|
apply plugin: 'org.jetbrains.kotlin.plugin.serialization'
|
|
16
|
+
apply plugin: 'com.apollographql.apollo'
|
|
15
17
|
|
|
16
18
|
expoModule {
|
|
17
19
|
canBePublished false
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
group = "host.exp.exponent"
|
|
21
|
-
version = "5.2.0-canary-
|
|
23
|
+
version = "5.2.0-canary-20250709-136b77f"
|
|
22
24
|
|
|
23
25
|
android {
|
|
24
26
|
namespace "expo.modules.devlauncher"
|
|
25
27
|
defaultConfig {
|
|
26
28
|
versionCode 9
|
|
27
|
-
versionName "5.2.0-canary-
|
|
29
|
+
versionName "5.2.0-canary-20250709-136b77f"
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
buildTypes {
|
|
@@ -39,6 +41,15 @@ android {
|
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
apollo {
|
|
45
|
+
service("service") {
|
|
46
|
+
packageName.set("expo.modules.devlauncher")
|
|
47
|
+
introspection {
|
|
48
|
+
endpointUrl.set("https://api.expo.dev/graphql")
|
|
49
|
+
schemaFile.set(file("src/main/graphql/schema.graphqls"))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
42
53
|
repositories {
|
|
43
54
|
// ref: https://www.baeldung.com/maven-local-repository
|
|
44
55
|
mavenLocal()
|
|
@@ -87,6 +98,8 @@ dependencies {
|
|
|
87
98
|
implementation "androidx.compose.ui:ui-tooling:$composeVersion"
|
|
88
99
|
implementation "androidx.navigation:navigation-compose:2.9.0"
|
|
89
100
|
|
|
101
|
+
implementation("com.apollographql.apollo:apollo-runtime:4.3.1")
|
|
102
|
+
|
|
90
103
|
implementation("com.composables:core:1.31.1")
|
|
91
104
|
|
|
92
105
|
testImplementation 'androidx.test:core:1.4.0'
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
android:name="expo.modules.devlauncher.launcher.DevLauncherActivity"
|
|
5
5
|
android:exported="true"
|
|
6
6
|
android:launchMode="singleTask"
|
|
7
|
-
android:theme="@style/Theme.DevLauncher.LauncherActivity"
|
|
7
|
+
android:theme="@style/Theme.DevLauncher.LauncherActivity" />
|
|
8
|
+
|
|
9
|
+
<activity
|
|
10
|
+
android:name="expo.modules.devlauncher.compose.AuthActivity"
|
|
11
|
+
android:exported="true"
|
|
12
|
+
android:launchMode="singleTask">
|
|
8
13
|
<intent-filter>
|
|
9
14
|
<action android:name="android.intent.action.VIEW" />
|
|
10
15
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
package expo.modules.devlauncher.compose
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.content.Intent.ACTION_VIEW
|
|
6
|
+
import android.os.Bundle
|
|
7
|
+
import androidx.activity.result.contract.ActivityResultContract
|
|
8
|
+
import androidx.appcompat.app.AppCompatActivity
|
|
9
|
+
import androidx.browser.customtabs.CustomTabsIntent
|
|
10
|
+
import androidx.core.net.toUri
|
|
11
|
+
import java.net.URLEncoder
|
|
12
|
+
|
|
13
|
+
enum class AuthRequestType(val type: String) {
|
|
14
|
+
LOGIN("login"),
|
|
15
|
+
SIGNUP("signup");
|
|
16
|
+
|
|
17
|
+
companion object {
|
|
18
|
+
fun fromString(type: String): AuthRequestType {
|
|
19
|
+
return entries.firstOrNull { it.type == type } ?: LOGIN
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private const val SESSION_KEY = "session"
|
|
25
|
+
private const val USERNAME_KEY = "username"
|
|
26
|
+
private const val AUTH_REQUEST_TYPE_KEY = "auth_request_type"
|
|
27
|
+
|
|
28
|
+
private const val WEBSITE_ORIGIN = "https://expo.dev"
|
|
29
|
+
private const val REDIRECT_BASE = "expo-dev-launcher://auth"
|
|
30
|
+
|
|
31
|
+
sealed interface AuthResult {
|
|
32
|
+
data class Success(val sessionSecret: String, val username: String) : AuthResult
|
|
33
|
+
object Canceled : AuthResult
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class AuthActivity : AppCompatActivity() {
|
|
37
|
+
class Contract : ActivityResultContract<AuthRequestType, AuthResult>() {
|
|
38
|
+
override fun createIntent(context: Context, type: AuthRequestType): Intent {
|
|
39
|
+
return Intent(context, AuthActivity::class.java).apply {
|
|
40
|
+
action = ACTION_VIEW
|
|
41
|
+
putExtra(AUTH_REQUEST_TYPE_KEY, type.type)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override fun parseResult(resultCode: Int, intent: Intent?): AuthResult {
|
|
46
|
+
if (resultCode == RESULT_CANCELED || intent == null) {
|
|
47
|
+
return AuthResult.Canceled
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
val sessionSecret = intent.getStringExtra(SESSION_KEY) ?: return AuthResult.Canceled
|
|
51
|
+
val username = intent.getStringExtra(USERNAME_KEY) ?: return AuthResult.Canceled
|
|
52
|
+
|
|
53
|
+
return AuthResult.Success(
|
|
54
|
+
sessionSecret = sessionSecret,
|
|
55
|
+
username = username
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
var wasStarted = false
|
|
61
|
+
|
|
62
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
63
|
+
super.onCreate(savedInstanceState)
|
|
64
|
+
|
|
65
|
+
val extraType = intent.getStringExtra(AUTH_REQUEST_TYPE_KEY)
|
|
66
|
+
if (extraType != null) {
|
|
67
|
+
wasStarted = true
|
|
68
|
+
openWebBrowserAsync(startUrl = createAuthUrl(type = AuthRequestType.fromString(extraType)))
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override fun onResume() {
|
|
73
|
+
super.onResume()
|
|
74
|
+
// onNewIntent will handle the response from the web browser
|
|
75
|
+
if (intent?.data?.host == "expo-dev-launcher") {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// We just open the browser
|
|
80
|
+
if (wasStarted) {
|
|
81
|
+
wasStarted = false
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
val resultIntent = Intent()
|
|
86
|
+
setResult(RESULT_CANCELED, resultIntent)
|
|
87
|
+
finish()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
override fun onNewIntent(intent: Intent) {
|
|
91
|
+
super.onNewIntent(intent)
|
|
92
|
+
|
|
93
|
+
if (intent.action == ACTION_VIEW && intent.data?.scheme == "expo-dev-launcher" && intent.data?.host == "auth") {
|
|
94
|
+
val sessionSecret = intent.data?.getQueryParameter("session_secret")
|
|
95
|
+
val userNameOrEmail = intent.data?.getQueryParameter("username_or_email")
|
|
96
|
+
|
|
97
|
+
if (sessionSecret.isNullOrEmpty() || userNameOrEmail.isNullOrEmpty()) {
|
|
98
|
+
setResult(RESULT_CANCELED)
|
|
99
|
+
finish()
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
val resultIntent = Intent().apply {
|
|
104
|
+
putExtra(SESSION_KEY, sessionSecret)
|
|
105
|
+
putExtra(USERNAME_KEY, userNameOrEmail)
|
|
106
|
+
}
|
|
107
|
+
setResult(RESULT_OK, resultIntent)
|
|
108
|
+
finish()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private fun openWebBrowserAsync(startUrl: String) {
|
|
113
|
+
requireNotNull(startUrl)
|
|
114
|
+
|
|
115
|
+
val intent = createCustomTabsIntent()
|
|
116
|
+
intent.data = startUrl.toUri()
|
|
117
|
+
|
|
118
|
+
startActivity(intent)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private fun createCustomTabsIntent(): Intent {
|
|
122
|
+
val builder = CustomTabsIntent.Builder()
|
|
123
|
+
builder.setShowTitle(false)
|
|
124
|
+
|
|
125
|
+
val intent = builder.build().intent
|
|
126
|
+
|
|
127
|
+
// We cannot use builder's method enableUrlBarHiding, because there is no corresponding disable method and some browsers enables it by default.
|
|
128
|
+
intent.putExtra(CustomTabsIntent.EXTRA_ENABLE_URLBAR_HIDING, false)
|
|
129
|
+
|
|
130
|
+
return intent
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private fun createAuthUrl(type: AuthRequestType): String {
|
|
134
|
+
return "${WEBSITE_ORIGIN}/${type.type}?confirm_account=1&app_redirect_uri=${URLEncoder.encode(REDIRECT_BASE, "UTF-8")}"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -2,20 +2,15 @@ package expo.modules.devlauncher.compose
|
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.widget.LinearLayout
|
|
5
|
-
import androidx.compose.runtime.collectAsState
|
|
6
|
-
import androidx.compose.runtime.getValue
|
|
7
5
|
import androidx.compose.ui.platform.ComposeView
|
|
8
6
|
import expo.modules.devmenu.compose.theme.AppTheme
|
|
9
7
|
|
|
10
|
-
class BindingView(context: Context
|
|
11
|
-
val viewModel by lazyViewModel
|
|
12
|
-
|
|
8
|
+
class BindingView(context: Context) : LinearLayout(context) {
|
|
13
9
|
init {
|
|
14
10
|
addView(
|
|
15
11
|
ComposeView(context).apply {
|
|
16
12
|
setContent {
|
|
17
13
|
AppTheme {
|
|
18
|
-
val runningPackager by viewModel.packagerService.runningPackagers.collectAsState()
|
|
19
14
|
DevLauncherBottomTabsNavigator()
|
|
20
15
|
}
|
|
21
16
|
}
|
package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherBottomTabsNavigator.kt
CHANGED
|
@@ -10,29 +10,19 @@ import androidx.compose.foundation.verticalScroll
|
|
|
10
10
|
import androidx.compose.runtime.Composable
|
|
11
11
|
import androidx.compose.ui.Modifier
|
|
12
12
|
import androidx.compose.ui.graphics.painter.Painter
|
|
13
|
-
import androidx.compose.ui.tooling.preview.Preview
|
|
14
13
|
import androidx.navigation.compose.NavHost
|
|
15
14
|
import androidx.navigation.compose.composable
|
|
16
15
|
import androidx.navigation.compose.rememberNavController
|
|
17
|
-
import com.composables.core.SheetDetent.Companion.Hidden
|
|
18
16
|
import expo.modules.devlauncher.compose.primitives.DefaultScaffold
|
|
19
|
-
import expo.modules.devlauncher.compose.
|
|
20
|
-
import expo.modules.devlauncher.compose.
|
|
21
|
-
import expo.modules.devlauncher.compose.
|
|
22
|
-
import expo.modules.devlauncher.compose.
|
|
23
|
-
import expo.modules.devlauncher.compose.
|
|
17
|
+
import expo.modules.devlauncher.compose.routes.Home
|
|
18
|
+
import expo.modules.devlauncher.compose.routes.HomeRoute
|
|
19
|
+
import expo.modules.devlauncher.compose.routes.ProfileRoute
|
|
20
|
+
import expo.modules.devlauncher.compose.routes.Settings
|
|
21
|
+
import expo.modules.devlauncher.compose.routes.SettingsRoute
|
|
24
22
|
import expo.modules.devlauncher.compose.ui.BottomTabBar
|
|
25
23
|
import expo.modules.devlauncher.compose.ui.Full
|
|
26
24
|
import expo.modules.devlauncher.compose.ui.rememberBottomSheetState
|
|
27
|
-
import expo.modules.devmenu.compose.theme.AppTheme
|
|
28
25
|
import expo.modules.devmenu.compose.theme.Theme
|
|
29
|
-
import kotlinx.serialization.Serializable
|
|
30
|
-
|
|
31
|
-
@Serializable
|
|
32
|
-
object Home
|
|
33
|
-
|
|
34
|
-
@Serializable
|
|
35
|
-
object Settings
|
|
36
26
|
|
|
37
27
|
@Composable
|
|
38
28
|
fun DefaultScreenContainer(
|
|
@@ -59,7 +49,6 @@ data class Tab(
|
|
|
59
49
|
@Composable
|
|
60
50
|
fun DevLauncherBottomTabsNavigator() {
|
|
61
51
|
val navController = rememberNavController()
|
|
62
|
-
|
|
63
52
|
val bottomSheetState = rememberBottomSheetState()
|
|
64
53
|
|
|
65
54
|
DefaultScaffold(bottomTab = {
|
|
@@ -75,22 +64,14 @@ fun DevLauncherBottomTabsNavigator() {
|
|
|
75
64
|
ExitTransition.None
|
|
76
65
|
}
|
|
77
66
|
) {
|
|
78
|
-
composable<Home> {
|
|
79
|
-
|
|
67
|
+
composable<Home> {
|
|
68
|
+
HomeRoute(onProfileClick = { bottomSheetState.jumpTo(Full) })
|
|
69
|
+
}
|
|
70
|
+
composable<Settings> {
|
|
71
|
+
SettingsRoute()
|
|
72
|
+
}
|
|
80
73
|
}
|
|
81
74
|
}
|
|
82
75
|
|
|
83
|
-
|
|
84
|
-
SignUp(onClose = {
|
|
85
|
-
bottomSheetState.targetDetent = Hidden
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
@Composable
|
|
91
|
-
@Preview(showBackground = true)
|
|
92
|
-
fun DevLauncherBottomTabsNavigatorPreview() {
|
|
93
|
-
AppTheme {
|
|
94
|
-
DevLauncherBottomTabsNavigator()
|
|
95
|
-
}
|
|
76
|
+
ProfileRoute(bottomSheetState)
|
|
96
77
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
package expo.modules.devlauncher.compose
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import androidx.compose.runtime.mutableStateOf
|
|
5
|
+
import androidx.core.net.toUri
|
|
6
|
+
import androidx.lifecycle.ViewModel
|
|
7
|
+
import androidx.lifecycle.viewModelScope
|
|
8
|
+
import expo.modules.devlauncher.DevLauncherController
|
|
9
|
+
import expo.modules.devlauncher.MeQuery
|
|
10
|
+
import expo.modules.devlauncher.services.HttpClientService
|
|
11
|
+
import expo.modules.devlauncher.services.PackagerInfo
|
|
12
|
+
import expo.modules.devlauncher.services.PackagerService
|
|
13
|
+
import expo.modules.devlauncher.services.SessionService
|
|
14
|
+
import expo.modules.devlauncher.services.UserState
|
|
15
|
+
import expo.modules.devlauncher.services.inject
|
|
16
|
+
import kotlinx.coroutines.flow.launchIn
|
|
17
|
+
import kotlinx.coroutines.flow.onEach
|
|
18
|
+
import kotlinx.coroutines.launch
|
|
19
|
+
|
|
20
|
+
sealed interface HomeAction {
|
|
21
|
+
class OpenApp(val url: String) : HomeAction
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
typealias HomeActionHandler = (HomeAction) -> Unit
|
|
25
|
+
|
|
26
|
+
data class HomeState(
|
|
27
|
+
val appName: String = "BareExpo",
|
|
28
|
+
val runningPackagers: Set<PackagerInfo> = emptySet(),
|
|
29
|
+
val currentAccount: MeQuery.Account? = null
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
class HomeViewModel() : ViewModel() {
|
|
33
|
+
val httpClientService = inject<HttpClientService>()
|
|
34
|
+
val devLauncherController = inject<DevLauncherController>()
|
|
35
|
+
val sessionService = inject<SessionService>()
|
|
36
|
+
|
|
37
|
+
val packagerService = PackagerService(httpClientService.httpClient, viewModelScope)
|
|
38
|
+
|
|
39
|
+
private var _state = mutableStateOf(
|
|
40
|
+
HomeState(
|
|
41
|
+
runningPackagers = packagerService.runningPackagers.value,
|
|
42
|
+
currentAccount = when (val userState = sessionService.user.value) {
|
|
43
|
+
UserState.Fetching, UserState.LoggedOut -> null
|
|
44
|
+
is UserState.LoggedIn -> userState.selectedAccount
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
val state
|
|
50
|
+
get() = _state.value
|
|
51
|
+
|
|
52
|
+
init {
|
|
53
|
+
packagerService.runningPackagers.onEach { newPackagers ->
|
|
54
|
+
_state.value = _state.value.copy(
|
|
55
|
+
runningPackagers = newPackagers
|
|
56
|
+
)
|
|
57
|
+
}.launchIn(viewModelScope)
|
|
58
|
+
|
|
59
|
+
sessionService.user.onEach { newUser ->
|
|
60
|
+
when (newUser) {
|
|
61
|
+
UserState.Fetching, UserState.LoggedOut -> _state.value = _state.value.copy(
|
|
62
|
+
currentAccount = null
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
is UserState.LoggedIn -> _state.value = _state.value.copy(
|
|
66
|
+
currentAccount = newUser.selectedAccount
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}.launchIn(viewModelScope)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fun onAction(action: HomeAction) {
|
|
73
|
+
when (action) {
|
|
74
|
+
is HomeAction.OpenApp ->
|
|
75
|
+
viewModelScope.launch {
|
|
76
|
+
try {
|
|
77
|
+
devLauncherController.loadApp(action.url.toUri(), mainActivity = null)
|
|
78
|
+
} catch (e: Exception) {
|
|
79
|
+
Log.e("DevLauncher", "Failed to open app: ${action.url}", e)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
package expo.modules.devlauncher.compose
|
|
2
|
+
|
|
3
|
+
import androidx.lifecycle.ViewModel
|
|
4
|
+
import androidx.lifecycle.viewModelScope
|
|
5
|
+
import expo.modules.devlauncher.services.SessionService
|
|
6
|
+
import expo.modules.devlauncher.services.UserState
|
|
7
|
+
import expo.modules.devlauncher.services.inject
|
|
8
|
+
import kotlinx.coroutines.flow.MutableStateFlow
|
|
9
|
+
import kotlinx.coroutines.flow.asStateFlow
|
|
10
|
+
import kotlinx.coroutines.flow.launchIn
|
|
11
|
+
import kotlinx.coroutines.flow.onEach
|
|
12
|
+
import kotlinx.coroutines.flow.update
|
|
13
|
+
|
|
14
|
+
data class Account(
|
|
15
|
+
val id: String,
|
|
16
|
+
val name: String,
|
|
17
|
+
val avatar: String?,
|
|
18
|
+
val isSelected: Boolean = false
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
sealed interface ProfileState {
|
|
22
|
+
class LoggedIn(
|
|
23
|
+
val accounts: List<Account> = emptyList()
|
|
24
|
+
) : ProfileState
|
|
25
|
+
|
|
26
|
+
object Fetching : ProfileState
|
|
27
|
+
object LoggedOut : ProfileState
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class ProfileViewModel : ViewModel() {
|
|
31
|
+
sealed interface Action {
|
|
32
|
+
class LogIn(val sessionSecret: String) : Action
|
|
33
|
+
class SwitchAccount(val account: Account) : Action
|
|
34
|
+
object SignOut : Action
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
val session = inject<SessionService>()
|
|
38
|
+
|
|
39
|
+
private val _state = MutableStateFlow<ProfileState>(ProfileState.LoggedOut)
|
|
40
|
+
|
|
41
|
+
val state
|
|
42
|
+
get() = _state.asStateFlow()
|
|
43
|
+
|
|
44
|
+
init {
|
|
45
|
+
session.user.onEach { newUserState ->
|
|
46
|
+
when (newUserState) {
|
|
47
|
+
UserState.LoggedOut -> _state.update { ProfileState.LoggedOut }
|
|
48
|
+
UserState.Fetching -> _state.update { ProfileState.Fetching }
|
|
49
|
+
is UserState.LoggedIn -> _state.update {
|
|
50
|
+
ProfileState.LoggedIn(
|
|
51
|
+
accounts = newUserState.data.meUserActor?.accounts?.map { account ->
|
|
52
|
+
Account(
|
|
53
|
+
id = account.id,
|
|
54
|
+
name = account.name,
|
|
55
|
+
avatar = account.ownerUserActor?.profilePhoto,
|
|
56
|
+
isSelected = account.id == newUserState.selectedAccount?.id
|
|
57
|
+
)
|
|
58
|
+
} ?: emptyList()
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
.launchIn(viewModelScope)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fun onAction(action: Action) {
|
|
67
|
+
when (action) {
|
|
68
|
+
is Action.LogIn -> {
|
|
69
|
+
session.setSession(
|
|
70
|
+
Session(action.sessionSecret)
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
Action.SignOut -> {
|
|
75
|
+
session.setSession(null)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
is Action.SwitchAccount -> session.switchAccount(action.account.id)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package expo.modules.devlauncher.compose
|
|
2
|
+
|
|
3
|
+
import android.content.SharedPreferences
|
|
4
|
+
import org.json.JSONObject
|
|
5
|
+
import androidx.core.content.edit
|
|
6
|
+
|
|
7
|
+
data class Session(
|
|
8
|
+
val sessionSecret: String
|
|
9
|
+
) {
|
|
10
|
+
val token: String
|
|
11
|
+
get() = JSONObject(sessionSecret).getString("id")
|
|
12
|
+
|
|
13
|
+
val version: Int
|
|
14
|
+
get() = JSONObject(sessionSecret).getInt("version")
|
|
15
|
+
|
|
16
|
+
val expiresAt: Long
|
|
17
|
+
get() = JSONObject(sessionSecret).getLong("expires_at")
|
|
18
|
+
|
|
19
|
+
companion object {
|
|
20
|
+
fun loadFromPreferences(preferences: SharedPreferences): Session? {
|
|
21
|
+
val sessionSecret = preferences.getString("session_secret", null) ?: return null
|
|
22
|
+
return Session(sessionSecret)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fun Session?.saveToPreferences(preferences: SharedPreferences) {
|
|
28
|
+
if (this == null) {
|
|
29
|
+
preferences.edit(commit = true) { remove("session_secret") }
|
|
30
|
+
} else {
|
|
31
|
+
preferences.edit(commit = true) { putString("session_secret", sessionSecret) }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -50,7 +50,7 @@ fun Accordion(
|
|
|
50
50
|
Row(
|
|
51
51
|
verticalAlignment = Alignment.CenterVertically,
|
|
52
52
|
modifier = Modifier
|
|
53
|
-
.padding(Theme.spacing.
|
|
53
|
+
.padding(Theme.spacing.medium)
|
|
54
54
|
) {
|
|
55
55
|
Icon(
|
|
56
56
|
painter = painterResource(expo.modules.devmenu.R.drawable._expodevclientcomponents_assets_chevronrighticon),
|
|
@@ -91,7 +91,7 @@ fun Accordion(
|
|
|
91
91
|
@Composable
|
|
92
92
|
@Preview(showBackground = true, heightDp = 200)
|
|
93
93
|
fun AccordionVariantPreview() {
|
|
94
|
-
Accordion(text = "Enter URL") {
|
|
94
|
+
Accordion(text = "Enter URL manually") {
|
|
95
95
|
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ac nisl interdum, mattis purus a, consequat ipsum. Aliquam sem mauris, egestas a elit a, lacinia efficitur nisi. Maecenas scelerisque erat nisi, ac interdum mauris volutpat vel. Proin sed lectus at purus interdum porta. Ut mollis feugiat dignissim.")
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package expo.modules.devlauncher.compose.primitives
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.Image
|
|
4
|
+
import androidx.compose.runtime.Composable
|
|
5
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
6
|
+
import androidx.compose.runtime.getValue
|
|
7
|
+
import androidx.compose.runtime.mutableStateOf
|
|
8
|
+
import androidx.compose.runtime.remember
|
|
9
|
+
import androidx.compose.runtime.rememberCoroutineScope
|
|
10
|
+
import androidx.compose.runtime.setValue
|
|
11
|
+
import androidx.compose.ui.graphics.ImageBitmap
|
|
12
|
+
import androidx.compose.ui.graphics.asImageBitmap
|
|
13
|
+
import expo.modules.devlauncher.services.ImageLoaderService
|
|
14
|
+
import expo.modules.devlauncher.services.inject
|
|
15
|
+
import kotlinx.coroutines.launch
|
|
16
|
+
|
|
17
|
+
@Composable
|
|
18
|
+
fun AsyncImage(
|
|
19
|
+
url: String
|
|
20
|
+
) {
|
|
21
|
+
val imageLoaderService = inject<ImageLoaderService>()
|
|
22
|
+
val scope = rememberCoroutineScope()
|
|
23
|
+
var imageBitmap by remember { mutableStateOf<ImageBitmap?>(null) }
|
|
24
|
+
|
|
25
|
+
LaunchedEffect(url) {
|
|
26
|
+
scope.launch {
|
|
27
|
+
val image = imageLoaderService.loadImage(url)
|
|
28
|
+
if (image != null) {
|
|
29
|
+
imageBitmap = image.asImageBitmap()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
imageBitmap?.let {
|
|
35
|
+
Image(
|
|
36
|
+
bitmap = it,
|
|
37
|
+
contentDescription = url
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package expo.modules.devlauncher.compose.routes
|
|
2
|
+
|
|
3
|
+
import androidx.compose.runtime.Composable
|
|
4
|
+
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
5
|
+
import expo.modules.devlauncher.compose.DefaultScreenContainer
|
|
6
|
+
import expo.modules.devlauncher.compose.HomeViewModel
|
|
7
|
+
import expo.modules.devlauncher.compose.screens.HomeScreen
|
|
8
|
+
import kotlinx.serialization.Serializable
|
|
9
|
+
|
|
10
|
+
@Serializable
|
|
11
|
+
object Home
|
|
12
|
+
|
|
13
|
+
@Composable
|
|
14
|
+
fun HomeRoute(onProfileClick: () -> Unit) {
|
|
15
|
+
DefaultScreenContainer {
|
|
16
|
+
val viewModel = viewModel<HomeViewModel>()
|
|
17
|
+
HomeScreen(
|
|
18
|
+
state = viewModel.state,
|
|
19
|
+
onAction = viewModel::onAction,
|
|
20
|
+
onProfileClick = onProfileClick
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
package expo.modules.devlauncher.compose.routes
|
|
2
|
+
|
|
3
|
+
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
4
|
+
import androidx.compose.runtime.Composable
|
|
5
|
+
import androidx.compose.runtime.getValue
|
|
6
|
+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
7
|
+
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
8
|
+
import com.composables.core.ModalBottomSheetState
|
|
9
|
+
import com.composables.core.SheetDetent.Companion.Hidden
|
|
10
|
+
import expo.modules.devlauncher.compose.AuthActivity
|
|
11
|
+
import expo.modules.devlauncher.compose.AuthRequestType
|
|
12
|
+
import expo.modules.devlauncher.compose.AuthResult
|
|
13
|
+
import expo.modules.devlauncher.compose.ProfileState
|
|
14
|
+
import expo.modules.devlauncher.compose.ProfileViewModel
|
|
15
|
+
import expo.modules.devlauncher.compose.ui.AccountSelector
|
|
16
|
+
import expo.modules.devlauncher.compose.ui.BottomSheet
|
|
17
|
+
import expo.modules.devlauncher.compose.ui.ProfileLayout
|
|
18
|
+
import expo.modules.devlauncher.compose.ui.SignUp
|
|
19
|
+
|
|
20
|
+
@Composable
|
|
21
|
+
fun ProfileRoute(
|
|
22
|
+
bottomSheetState: ModalBottomSheetState,
|
|
23
|
+
viewModel: ProfileViewModel = viewModel()
|
|
24
|
+
) {
|
|
25
|
+
val state by viewModel.state.collectAsStateWithLifecycle()
|
|
26
|
+
|
|
27
|
+
val authLauncher = rememberLauncherForActivityResult(AuthActivity.Contract()) { result ->
|
|
28
|
+
when (result) {
|
|
29
|
+
is AuthResult.Canceled -> {}
|
|
30
|
+
is AuthResult.Success -> {
|
|
31
|
+
viewModel.onAction(ProfileViewModel.Action.LogIn(result.sessionSecret))
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
BottomSheet(bottomSheetState) {
|
|
37
|
+
ProfileLayout(onClose = {
|
|
38
|
+
bottomSheetState.targetDetent = Hidden
|
|
39
|
+
}) {
|
|
40
|
+
when (state) {
|
|
41
|
+
is ProfileState.LoggedIn -> {
|
|
42
|
+
val state = state as ProfileState.LoggedIn
|
|
43
|
+
AccountSelector(
|
|
44
|
+
accounts = state.accounts,
|
|
45
|
+
onSignOut = {
|
|
46
|
+
viewModel.onAction(ProfileViewModel.Action.SignOut)
|
|
47
|
+
},
|
|
48
|
+
onClick = { account ->
|
|
49
|
+
viewModel.onAction(ProfileViewModel.Action.SwitchAccount(account))
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ProfileState.LoggedOut -> {
|
|
55
|
+
SignUp(
|
|
56
|
+
onLogIn = {
|
|
57
|
+
authLauncher.launch(AuthRequestType.LOGIN)
|
|
58
|
+
},
|
|
59
|
+
onSignUp = {
|
|
60
|
+
authLauncher.launch(AuthRequestType.SIGNUP)
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ProfileState.Fetching -> {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|