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.
Files changed (40) hide show
  1. package/android/build.gradle +15 -2
  2. package/android/src/debug/AndroidManifest.xml +6 -1
  3. package/android/src/debug/java/expo/modules/devlauncher/compose/AuthActivity.kt +136 -0
  4. package/android/src/debug/java/expo/modules/devlauncher/compose/BindingView.kt +11 -4
  5. package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherAction.kt +7 -0
  6. package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherBottomTabsNavigator.kt +13 -22
  7. package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherViewModel.kt +33 -1
  8. package/android/src/debug/java/expo/modules/devlauncher/compose/ProfileViewModel.kt +95 -0
  9. package/android/src/debug/java/expo/modules/devlauncher/compose/Session.kt +33 -0
  10. package/android/src/debug/java/expo/modules/devlauncher/compose/primitives/Accordion.kt +1 -1
  11. package/android/src/debug/java/expo/modules/devlauncher/compose/screens/Home.kt +18 -14
  12. package/android/src/debug/java/expo/modules/devlauncher/compose/screens/Profile.kt +64 -0
  13. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/AccountSelector.kt +37 -0
  14. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/ActionButton.kt +35 -0
  15. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/ProfileLayout.kt +45 -0
  16. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/RunningAppCard.kt +92 -0
  17. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/SignUp.kt +49 -0
  18. package/android/src/debug/java/expo/modules/devlauncher/launcher/DevLauncherActivity.kt +51 -36
  19. package/android/src/debug/java/expo/modules/devlauncher/services/ApolloClientService.kt +41 -0
  20. package/android/src/debug/java/expo/modules/devlauncher/services/DependencyInjection.kt +45 -0
  21. package/android/src/debug/java/expo/modules/devlauncher/services/PackagerService.kt +3 -0
  22. package/android/src/debug/java/expo/modules/devlauncher/services/SessionService.kt +27 -0
  23. package/android/src/main/graphql/GetBranches.graphql +32 -0
  24. package/android/src/main/graphql/GetUpdates.graphql +34 -0
  25. package/android/src/main/graphql/Me.graphql +18 -0
  26. package/android/src/main/graphql/schema.graphqls +9850 -0
  27. package/ios/SwiftUI/AccountSheet.swift +173 -89
  28. package/ios/SwiftUI/DevLauncherErrors.swift +38 -0
  29. package/ios/SwiftUI/DevLauncherViewModel.swift +104 -56
  30. package/ios/SwiftUI/{DevLauncherSwiftUIViews.swift → DevLauncherViews.swift} +0 -57
  31. package/ios/SwiftUI/DevServersView.swift +26 -17
  32. package/ios/SwiftUI/GraphQL/APIClient.swift +78 -0
  33. package/ios/SwiftUI/GraphQL/Models.swift +104 -0
  34. package/ios/SwiftUI/GraphQL/Queries.swift +162 -0
  35. package/ios/SwiftUI/Navigation/Navigation.swift +58 -3
  36. package/ios/SwiftUI/SettingsTabView.swift +70 -0
  37. package/ios/SwiftUI/Utils/Avatar.swift +66 -0
  38. package/ios/SwiftUI/Utils/Utils.swift +22 -0
  39. package/package.json +4 -4
  40. package/android/src/debug/java/expo/modules/devlauncher/compose/screens/SignUp.kt +0 -100
@@ -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-20250630-547cd82"
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-20250630-547cd82"
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, lazyViewModel: Lazy<DevLauncherViewModel>) : LinearLayout(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
- DevLauncherBottomTabsNavigator()
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
  }
@@ -0,0 +1,7 @@
1
+ package expo.modules.devlauncher.compose
2
+
3
+ sealed interface DevLauncherAction {
4
+ class OpenApp(val url: String) : DevLauncherAction
5
+ }
6
+
7
+ typealias DevLauncherActionHandler = (DevLauncherAction) -> Unit
@@ -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.HomeScreenState
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.devmenu.compose.theme.AppTheme
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(HomeScreenState(appName = "BareExpo", onProfileClick = { bottomSheetState.jumpTo(Full) })) } }
81
+ composable<Home> { DefaultScreenContainer { HomeScreen(state, onProfileClick = { bottomSheetState.jumpTo(Full) }) } }
79
82
  composable<Settings> { DefaultScreenContainer { SettingsScreen() } }
80
83
  }
81
84
  }
82
85
 
83
- BottomSheet(bottomSheetState) {
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 : ViewModel() {
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.small)
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: HomeScreenState) {
36
+ fun HomeScreen(state: DevLauncherState, onProfileClick: () -> Unit) {
40
37
  Column {
41
38
  ScreenHeaderContainer(modifier = Modifier.padding(Theme.spacing.medium)) {
42
- AppHeader(state.appName, onProfileClick = state.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
- MenuButton("http://10.0.2.2:8081")
76
- Divider()
77
- MenuButton("Fetch development")
78
- Divider()
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 = {}, modifier = Modifier.fillMaxWidth()) {
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 = HomeScreenState("BareExpo"))
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
+ }