expo-dev-launcher 5.2.0-canary-20250630-547cd82 → 5.2.0-canary-20250701-6a945c5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +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 +11 -4
- package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherAction.kt +7 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherBottomTabsNavigator.kt +13 -22
- package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherViewModel.kt +33 -1
- package/android/src/debug/java/expo/modules/devlauncher/compose/ProfileViewModel.kt +95 -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 +1 -1
- package/android/src/debug/java/expo/modules/devlauncher/compose/screens/Home.kt +18 -14
- package/android/src/debug/java/expo/modules/devlauncher/compose/screens/Profile.kt +64 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/AccountSelector.kt +37 -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/ProfileLayout.kt +45 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/RunningAppCard.kt +92 -0
- package/android/src/debug/java/expo/modules/devlauncher/compose/ui/SignUp.kt +49 -0
- package/android/src/debug/java/expo/modules/devlauncher/launcher/DevLauncherActivity.kt +51 -36
- package/android/src/debug/java/expo/modules/devlauncher/services/ApolloClientService.kt +41 -0
- package/android/src/debug/java/expo/modules/devlauncher/services/DependencyInjection.kt +45 -0
- package/android/src/debug/java/expo/modules/devlauncher/services/PackagerService.kt +3 -0
- package/android/src/debug/java/expo/modules/devlauncher/services/SessionService.kt +27 -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/SwiftUI/AccountSheet.swift +173 -89
- package/ios/SwiftUI/DevLauncherErrors.swift +38 -0
- package/ios/SwiftUI/DevLauncherViewModel.swift +104 -56
- package/ios/SwiftUI/{DevLauncherSwiftUIViews.swift → DevLauncherViews.swift} +0 -57
- package/ios/SwiftUI/DevServersView.swift +26 -17
- package/ios/SwiftUI/GraphQL/APIClient.swift +78 -0
- package/ios/SwiftUI/GraphQL/Models.swift +104 -0
- package/ios/SwiftUI/GraphQL/Queries.swift +162 -0
- package/ios/SwiftUI/Navigation/Navigation.swift +58 -3
- package/ios/SwiftUI/SettingsTabView.swift +70 -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/screens/SignUp.kt +0 -100
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-20250701-6a945c5"
|
|
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-20250701-6a945c5"
|
|
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
|
+
}
|
|
@@ -4,19 +4,26 @@ import android.content.Context
|
|
|
4
4
|
import android.widget.LinearLayout
|
|
5
5
|
import androidx.compose.runtime.collectAsState
|
|
6
6
|
import androidx.compose.runtime.getValue
|
|
7
|
+
import androidx.compose.runtime.remember
|
|
7
8
|
import androidx.compose.ui.platform.ComposeView
|
|
9
|
+
import expo.modules.devmenu.AppInfo
|
|
8
10
|
import expo.modules.devmenu.compose.theme.AppTheme
|
|
9
11
|
|
|
10
|
-
class BindingView(context: Context,
|
|
11
|
-
val viewModel by lazyViewModel
|
|
12
|
-
|
|
12
|
+
class BindingView(context: Context, val viewModel: DevLauncherViewModel) : LinearLayout(context) {
|
|
13
13
|
init {
|
|
14
14
|
addView(
|
|
15
15
|
ComposeView(context).apply {
|
|
16
16
|
setContent {
|
|
17
17
|
AppTheme {
|
|
18
18
|
val runningPackager by viewModel.packagerService.runningPackagers.collectAsState()
|
|
19
|
-
|
|
19
|
+
val nativeAppInfo = remember { AppInfo.getNativeAppInfo(context) }
|
|
20
|
+
DevLauncherBottomTabsNavigator(
|
|
21
|
+
DevLauncherState(
|
|
22
|
+
appName = nativeAppInfo.appName,
|
|
23
|
+
runningPackagers = runningPackager,
|
|
24
|
+
onAction = viewModel::onAction
|
|
25
|
+
)
|
|
26
|
+
)
|
|
20
27
|
}
|
|
21
28
|
}
|
|
22
29
|
}
|
package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherBottomTabsNavigator.kt
CHANGED
|
@@ -10,21 +10,17 @@ 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
17
|
import expo.modules.devlauncher.compose.screens.HomeScreen
|
|
20
|
-
import expo.modules.devlauncher.compose.screens.
|
|
18
|
+
import expo.modules.devlauncher.compose.screens.Profile
|
|
21
19
|
import expo.modules.devlauncher.compose.screens.SettingsScreen
|
|
22
|
-
import expo.modules.devlauncher.compose.screens.SignUp
|
|
23
|
-
import expo.modules.devlauncher.compose.ui.BottomSheet
|
|
24
20
|
import expo.modules.devlauncher.compose.ui.BottomTabBar
|
|
25
21
|
import expo.modules.devlauncher.compose.ui.Full
|
|
26
22
|
import expo.modules.devlauncher.compose.ui.rememberBottomSheetState
|
|
27
|
-
import expo.modules.
|
|
23
|
+
import expo.modules.devlauncher.services.PackagerInfo
|
|
28
24
|
import expo.modules.devmenu.compose.theme.Theme
|
|
29
25
|
import kotlinx.serialization.Serializable
|
|
30
26
|
|
|
@@ -56,10 +52,17 @@ data class Tab(
|
|
|
56
52
|
val screen: Any
|
|
57
53
|
)
|
|
58
54
|
|
|
55
|
+
data class DevLauncherState(
|
|
56
|
+
val appName: String = "BareExpo",
|
|
57
|
+
val runningPackagers: Set<PackagerInfo> = emptySet<PackagerInfo>(),
|
|
58
|
+
val onAction: DevLauncherActionHandler = {}
|
|
59
|
+
)
|
|
60
|
+
|
|
59
61
|
@Composable
|
|
60
|
-
fun DevLauncherBottomTabsNavigator(
|
|
62
|
+
fun DevLauncherBottomTabsNavigator(
|
|
63
|
+
state: DevLauncherState
|
|
64
|
+
) {
|
|
61
65
|
val navController = rememberNavController()
|
|
62
|
-
|
|
63
66
|
val bottomSheetState = rememberBottomSheetState()
|
|
64
67
|
|
|
65
68
|
DefaultScaffold(bottomTab = {
|
|
@@ -75,22 +78,10 @@ fun DevLauncherBottomTabsNavigator() {
|
|
|
75
78
|
ExitTransition.None
|
|
76
79
|
}
|
|
77
80
|
) {
|
|
78
|
-
composable<Home> { DefaultScreenContainer { HomeScreen(
|
|
81
|
+
composable<Home> { DefaultScreenContainer { HomeScreen(state, onProfileClick = { bottomSheetState.jumpTo(Full) }) } }
|
|
79
82
|
composable<Settings> { DefaultScreenContainer { SettingsScreen() } }
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
|
|
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
|
-
}
|
|
86
|
+
Profile(bottomSheetState)
|
|
96
87
|
}
|
|
@@ -1,11 +1,43 @@
|
|
|
1
1
|
package expo.modules.devlauncher.compose
|
|
2
2
|
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import androidx.core.net.toUri
|
|
3
5
|
import androidx.lifecycle.ViewModel
|
|
6
|
+
import androidx.lifecycle.ViewModelProvider
|
|
4
7
|
import androidx.lifecycle.viewModelScope
|
|
8
|
+
import expo.modules.devlauncher.DevLauncherController
|
|
5
9
|
import expo.modules.devlauncher.services.PackagerService
|
|
10
|
+
import kotlinx.coroutines.launch
|
|
6
11
|
import okhttp3.OkHttpClient
|
|
7
12
|
|
|
8
|
-
class DevLauncherViewModel
|
|
13
|
+
class DevLauncherViewModel(
|
|
14
|
+
val devLauncherController: DevLauncherController
|
|
15
|
+
) : ViewModel() {
|
|
9
16
|
val httpClient = OkHttpClient()
|
|
17
|
+
|
|
10
18
|
val packagerService = PackagerService(httpClient, viewModelScope)
|
|
19
|
+
|
|
20
|
+
fun onAction(action: DevLauncherAction) {
|
|
21
|
+
when (action) {
|
|
22
|
+
is DevLauncherAction.OpenApp ->
|
|
23
|
+
viewModelScope.launch {
|
|
24
|
+
try {
|
|
25
|
+
devLauncherController.loadApp(action.url.toUri(), mainActivity = null)
|
|
26
|
+
} catch (e: Exception) {
|
|
27
|
+
Log.e("DevLauncher", "Failed to open app: ${action.url}", e)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
companion object {
|
|
34
|
+
class Factory(
|
|
35
|
+
private val devLauncherController: DevLauncherController
|
|
36
|
+
) : ViewModelProvider.Factory {
|
|
37
|
+
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
|
38
|
+
@Suppress("UNCHECKED_CAST")
|
|
39
|
+
return DevLauncherViewModel(devLauncherController) as T
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
11
43
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
package expo.modules.devlauncher.compose
|
|
2
|
+
|
|
3
|
+
import androidx.lifecycle.ViewModel
|
|
4
|
+
import androidx.lifecycle.viewModelScope
|
|
5
|
+
import expo.modules.devlauncher.services.ApolloClientService
|
|
6
|
+
import expo.modules.devlauncher.services.SessionService
|
|
7
|
+
import expo.modules.devlauncher.services.inject
|
|
8
|
+
import kotlinx.coroutines.Dispatchers
|
|
9
|
+
import kotlinx.coroutines.flow.MutableStateFlow
|
|
10
|
+
import kotlinx.coroutines.flow.asStateFlow
|
|
11
|
+
import kotlinx.coroutines.flow.launchIn
|
|
12
|
+
import kotlinx.coroutines.flow.onEach
|
|
13
|
+
import kotlinx.coroutines.flow.update
|
|
14
|
+
import kotlinx.coroutines.launch
|
|
15
|
+
|
|
16
|
+
sealed interface ProfileState {
|
|
17
|
+
data class Account(
|
|
18
|
+
val name: String,
|
|
19
|
+
val avatar: String?
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
class LoggedIn(
|
|
23
|
+
val isLoading: Boolean = true,
|
|
24
|
+
val accounts: List<Account> = emptyList()
|
|
25
|
+
) : ProfileState
|
|
26
|
+
|
|
27
|
+
object LoggedOut : ProfileState
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class ProfileViewModel : ViewModel() {
|
|
31
|
+
sealed interface Action {
|
|
32
|
+
class LogIn(val sessionSecret: String) : Action
|
|
33
|
+
object SignOut : Action
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
val sessionSession = inject<SessionService>()
|
|
37
|
+
val apolloClientService = inject<ApolloClientService>()
|
|
38
|
+
|
|
39
|
+
private val _state = MutableStateFlow<ProfileState>(ProfileState.LoggedOut)
|
|
40
|
+
|
|
41
|
+
val state
|
|
42
|
+
get() = _state.asStateFlow()
|
|
43
|
+
|
|
44
|
+
init {
|
|
45
|
+
sessionSession.session.onEach { newSession ->
|
|
46
|
+
if (newSession != null) {
|
|
47
|
+
fetchMe()
|
|
48
|
+
_state.update {
|
|
49
|
+
ProfileState.LoggedIn()
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
_state.update { ProfileState.LoggedOut }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
.launchIn(viewModelScope)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private fun fetchMe() {
|
|
59
|
+
viewModelScope.launch(Dispatchers.IO) {
|
|
60
|
+
val me = apolloClientService.fetchMe()
|
|
61
|
+
_state.update { prevState ->
|
|
62
|
+
// User logged out in the meantime, we can ignore the result
|
|
63
|
+
if (prevState is ProfileState.LoggedOut) {
|
|
64
|
+
return@update prevState
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
val accounts = me.data?.meUserActor?.accounts?.map {
|
|
68
|
+
ProfileState.Account(
|
|
69
|
+
name = it.name,
|
|
70
|
+
avatar = it.ownerUserActor?.profilePhoto
|
|
71
|
+
)
|
|
72
|
+
} ?: emptyList()
|
|
73
|
+
|
|
74
|
+
ProfileState.LoggedIn(
|
|
75
|
+
isLoading = false,
|
|
76
|
+
accounts = accounts
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fun onAction(action: Action) {
|
|
83
|
+
when (action) {
|
|
84
|
+
is Action.LogIn -> {
|
|
85
|
+
sessionSession.setSession(
|
|
86
|
+
Session(action.sessionSecret)
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Action.SignOut -> {
|
|
91
|
+
sessionSession.setSession(null)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -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),
|
|
@@ -19,8 +19,11 @@ import androidx.compose.ui.tooling.preview.Preview
|
|
|
19
19
|
import com.composeunstyled.Button
|
|
20
20
|
import com.composeunstyled.TextField
|
|
21
21
|
import expo.modules.devlauncher.R
|
|
22
|
+
import expo.modules.devlauncher.compose.DevLauncherAction
|
|
23
|
+
import expo.modules.devlauncher.compose.DevLauncherState
|
|
22
24
|
import expo.modules.devlauncher.compose.primitives.Accordion
|
|
23
25
|
import expo.modules.devlauncher.compose.ui.AppHeader
|
|
26
|
+
import expo.modules.devlauncher.compose.ui.RunningAppCard
|
|
24
27
|
import expo.modules.devlauncher.compose.ui.ScreenHeaderContainer
|
|
25
28
|
import expo.modules.devlauncher.compose.ui.SectionHeader
|
|
26
29
|
import expo.modules.devmenu.compose.primitives.Divider
|
|
@@ -28,18 +31,12 @@ import expo.modules.devmenu.compose.primitives.RoundedSurface
|
|
|
28
31
|
import expo.modules.devmenu.compose.primitives.Spacer
|
|
29
32
|
import expo.modules.devmenu.compose.primitives.Text
|
|
30
33
|
import expo.modules.devmenu.compose.theme.Theme
|
|
31
|
-
import expo.modules.devmenu.compose.ui.MenuButton
|
|
32
|
-
|
|
33
|
-
data class HomeScreenState(
|
|
34
|
-
val appName: String,
|
|
35
|
-
val onProfileClick: () -> Unit = {}
|
|
36
|
-
)
|
|
37
34
|
|
|
38
35
|
@Composable
|
|
39
|
-
fun HomeScreen(state:
|
|
36
|
+
fun HomeScreen(state: DevLauncherState, onProfileClick: () -> Unit) {
|
|
40
37
|
Column {
|
|
41
38
|
ScreenHeaderContainer(modifier = Modifier.padding(Theme.spacing.medium)) {
|
|
42
|
-
AppHeader(state.appName, onProfileClick =
|
|
39
|
+
AppHeader(state.appName, onProfileClick = onProfileClick)
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
Column(
|
|
@@ -72,10 +69,15 @@ fun HomeScreen(state: HomeScreenState) {
|
|
|
72
69
|
|
|
73
70
|
RoundedSurface {
|
|
74
71
|
Column {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
for (packager in state.runningPackagers) {
|
|
73
|
+
RunningAppCard(
|
|
74
|
+
appIp = packager.url
|
|
75
|
+
) {
|
|
76
|
+
state.onAction(DevLauncherAction.OpenApp(packager.url))
|
|
77
|
+
}
|
|
78
|
+
Divider()
|
|
79
|
+
}
|
|
80
|
+
|
|
79
81
|
Accordion("Enter URL", initialState = false) {
|
|
80
82
|
val url = remember { mutableStateOf("") }
|
|
81
83
|
|
|
@@ -106,7 +108,9 @@ fun HomeScreen(state: HomeScreenState) {
|
|
|
106
108
|
|
|
107
109
|
Spacer(Theme.spacing.tiny)
|
|
108
110
|
|
|
109
|
-
Button(onClick = {
|
|
111
|
+
Button(onClick = {
|
|
112
|
+
state.onAction(DevLauncherAction.OpenApp(url.value))
|
|
113
|
+
}, modifier = Modifier.fillMaxWidth()) {
|
|
110
114
|
Row(modifier = Modifier.padding(vertical = Theme.spacing.small)) {
|
|
111
115
|
Text("Connect")
|
|
112
116
|
}
|
|
@@ -126,5 +130,5 @@ fun HomeScreen(state: HomeScreenState) {
|
|
|
126
130
|
@Preview(showBackground = true)
|
|
127
131
|
@Composable
|
|
128
132
|
fun HomeScreenPreview() {
|
|
129
|
-
HomeScreen(state =
|
|
133
|
+
HomeScreen(state = DevLauncherState(), onProfileClick = {})
|
|
130
134
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package expo.modules.devlauncher.compose.screens
|
|
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 Profile(
|
|
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
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
ProfileState.LoggedOut -> {
|
|
52
|
+
SignUp(
|
|
53
|
+
onLogIn = {
|
|
54
|
+
authLauncher.launch(AuthRequestType.LOGIN)
|
|
55
|
+
},
|
|
56
|
+
onSignUp = {
|
|
57
|
+
authLauncher.launch(AuthRequestType.SIGNUP)
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
package expo.modules.devlauncher.compose.ui
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.layout.Column
|
|
4
|
+
import androidx.compose.runtime.Composable
|
|
5
|
+
import com.apollographql.apollo.api.label
|
|
6
|
+
import expo.modules.devlauncher.compose.ProfileState
|
|
7
|
+
import expo.modules.devmenu.compose.primitives.RoundedSurface
|
|
8
|
+
import expo.modules.devmenu.compose.primitives.Spacer
|
|
9
|
+
import expo.modules.devmenu.compose.theme.Theme
|
|
10
|
+
import expo.modules.devmenu.compose.ui.MenuButton
|
|
11
|
+
|
|
12
|
+
@Composable
|
|
13
|
+
fun AccountSelector(
|
|
14
|
+
accounts: List<ProfileState.Account>,
|
|
15
|
+
onSignOut: () -> Unit = {}
|
|
16
|
+
) {
|
|
17
|
+
Column {
|
|
18
|
+
RoundedSurface {
|
|
19
|
+
Column {
|
|
20
|
+
for (account in accounts) {
|
|
21
|
+
MenuButton(
|
|
22
|
+
|
|
23
|
+
label = account.name
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Spacer(Theme.spacing.medium)
|
|
30
|
+
|
|
31
|
+
ActionButton(
|
|
32
|
+
"Log Out",
|
|
33
|
+
style = Theme.colors.button.tertiary,
|
|
34
|
+
onClick = onSignOut
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
}
|