expo-dev-launcher 5.2.0-canary-20250701-6a945c5 → 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.
Files changed (58) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/android/build.gradle +2 -2
  3. package/android/src/debug/java/expo/modules/devlauncher/compose/BindingView.kt +2 -14
  4. package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherBottomTabsNavigator.kt +13 -23
  5. package/android/src/debug/java/expo/modules/devlauncher/compose/HomeViewModel.kt +84 -0
  6. package/android/src/debug/java/expo/modules/devlauncher/compose/ProfileViewModel.kt +30 -44
  7. package/android/src/debug/java/expo/modules/devlauncher/compose/primitives/Accordion.kt +1 -1
  8. package/android/src/debug/java/expo/modules/devlauncher/compose/primitives/Asyncimage.kt +40 -0
  9. package/android/src/debug/java/expo/modules/devlauncher/compose/routes/Home.kt +23 -0
  10. package/android/src/debug/java/expo/modules/devlauncher/compose/{screens → routes}/Profile.kt +8 -2
  11. package/android/src/debug/java/expo/modules/devlauncher/compose/routes/Settings.kt +16 -0
  12. package/android/src/debug/java/expo/modules/devlauncher/compose/screens/{Home.kt → HomeScreen.kt} +18 -10
  13. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/AccountAvatar.kt +37 -0
  14. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/AccountSelector.kt +42 -9
  15. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/AppHeader.kt +26 -18
  16. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/BottomTabBar.kt +3 -3
  17. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/RunningAppCard.kt +25 -26
  18. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/SectionHeader.kt +6 -16
  19. package/android/src/debug/java/expo/modules/devlauncher/launcher/DevLauncherActivity.kt +36 -51
  20. package/android/src/debug/java/expo/modules/devlauncher/services/DependencyInjection.kt +37 -3
  21. package/android/src/debug/java/expo/modules/devlauncher/services/HttpClientService.kt +9 -0
  22. package/android/src/debug/java/expo/modules/devlauncher/services/ImageLoaderService.kt +69 -0
  23. package/android/src/debug/java/expo/modules/devlauncher/services/PackagerService.kt +1 -0
  24. package/android/src/debug/java/expo/modules/devlauncher/services/SessionService.kt +90 -3
  25. package/ios/Assets.xcassets/branch-icon.imageset/Contents.json +56 -0
  26. package/ios/Assets.xcassets/branch-icon.imageset/branch-icon-light.png +0 -0
  27. package/ios/Assets.xcassets/branch-icon.imageset/branch-icon-light@2x.png +0 -0
  28. package/ios/Assets.xcassets/branch-icon.imageset/branch-icon-light@3x.png +0 -0
  29. package/ios/Assets.xcassets/branch-icon.imageset/branch-icon.png +0 -0
  30. package/ios/Assets.xcassets/branch-icon.imageset/branch-icon@2x.png +0 -0
  31. package/ios/Assets.xcassets/branch-icon.imageset/branch-icon@3x.png +0 -0
  32. package/ios/Assets.xcassets/update-icon.imageset/Contents.json +56 -0
  33. package/ios/Assets.xcassets/update-icon.imageset/update-icon-light.png +0 -0
  34. package/ios/Assets.xcassets/update-icon.imageset/update-icon-light@2x.png +0 -0
  35. package/ios/Assets.xcassets/update-icon.imageset/update-icon-light@3x.png +0 -0
  36. package/ios/Assets.xcassets/update-icon.imageset/update-icon.png +0 -0
  37. package/ios/Assets.xcassets/update-icon.imageset/update-icon@2x.png +0 -0
  38. package/ios/Assets.xcassets/update-icon.imageset/update-icon@3x.png +0 -0
  39. package/ios/EXDevLauncherController.m +2 -4
  40. package/ios/SwiftUI/Data.swift +34 -0
  41. package/ios/SwiftUI/DevLauncherViewModel.swift +24 -15
  42. package/ios/SwiftUI/DevLauncherViews.swift +3 -11
  43. package/ios/SwiftUI/GraphQL/APIClient.swift +11 -6
  44. package/ios/SwiftUI/GraphQL/Models.swift +12 -0
  45. package/ios/SwiftUI/GraphQL/Queries.swift +3 -15
  46. package/ios/SwiftUI/HomeTabView.swift +5 -3
  47. package/ios/SwiftUI/SettingsTabView.swift +4 -4
  48. package/ios/SwiftUI/UpdatesTab/NotSignedInView.swift +32 -0
  49. package/ios/SwiftUI/UpdatesTab/NotUsingUpdatesView.swift +38 -0
  50. package/ios/SwiftUI/UpdatesTab/UpdateRow.swift +76 -0
  51. package/ios/SwiftUI/UpdatesTab/UpdateUtils.swift +5 -0
  52. package/ios/SwiftUI/UpdatesTab/UpdatesListView.swift +196 -0
  53. package/ios/SwiftUI/UpdatesTab/UpdatesTabView.swift +37 -0
  54. package/package.json +4 -4
  55. package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherAction.kt +0 -7
  56. package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherViewModel.kt +0 -43
  57. package/ios/SwiftUI/ExtensionsTabView.swift +0 -68
  58. /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
@@ -20,13 +20,13 @@ expoModule {
20
20
  }
21
21
 
22
22
  group = "host.exp.exponent"
23
- version = "5.2.0-canary-20250701-6a945c5"
23
+ version = "5.2.0-canary-20250709-136b77f"
24
24
 
25
25
  android {
26
26
  namespace "expo.modules.devlauncher"
27
27
  defaultConfig {
28
28
  versionCode 9
29
- versionName "5.2.0-canary-20250701-6a945c5"
29
+ versionName "5.2.0-canary-20250709-136b77f"
30
30
  }
31
31
 
32
32
  buildTypes {
@@ -2,28 +2,16 @@ 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
- import androidx.compose.runtime.remember
8
5
  import androidx.compose.ui.platform.ComposeView
9
- import expo.modules.devmenu.AppInfo
10
6
  import expo.modules.devmenu.compose.theme.AppTheme
11
7
 
12
- class BindingView(context: Context, val viewModel: DevLauncherViewModel) : LinearLayout(context) {
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
- val nativeAppInfo = remember { AppInfo.getNativeAppInfo(context) }
20
- DevLauncherBottomTabsNavigator(
21
- DevLauncherState(
22
- appName = nativeAppInfo.appName,
23
- runningPackagers = runningPackager,
24
- onAction = viewModel::onAction
25
- )
26
- )
14
+ DevLauncherBottomTabsNavigator()
27
15
  }
28
16
  }
29
17
  }
@@ -14,21 +14,15 @@ import androidx.navigation.compose.NavHost
14
14
  import androidx.navigation.compose.composable
15
15
  import androidx.navigation.compose.rememberNavController
16
16
  import expo.modules.devlauncher.compose.primitives.DefaultScaffold
17
- import expo.modules.devlauncher.compose.screens.HomeScreen
18
- import expo.modules.devlauncher.compose.screens.Profile
19
- import expo.modules.devlauncher.compose.screens.SettingsScreen
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
20
22
  import expo.modules.devlauncher.compose.ui.BottomTabBar
21
23
  import expo.modules.devlauncher.compose.ui.Full
22
24
  import expo.modules.devlauncher.compose.ui.rememberBottomSheetState
23
- import expo.modules.devlauncher.services.PackagerInfo
24
25
  import expo.modules.devmenu.compose.theme.Theme
25
- import kotlinx.serialization.Serializable
26
-
27
- @Serializable
28
- object Home
29
-
30
- @Serializable
31
- object Settings
32
26
 
33
27
  @Composable
34
28
  fun DefaultScreenContainer(
@@ -52,16 +46,8 @@ data class Tab(
52
46
  val screen: Any
53
47
  )
54
48
 
55
- data class DevLauncherState(
56
- val appName: String = "BareExpo",
57
- val runningPackagers: Set<PackagerInfo> = emptySet<PackagerInfo>(),
58
- val onAction: DevLauncherActionHandler = {}
59
- )
60
-
61
49
  @Composable
62
- fun DevLauncherBottomTabsNavigator(
63
- state: DevLauncherState
64
- ) {
50
+ fun DevLauncherBottomTabsNavigator() {
65
51
  val navController = rememberNavController()
66
52
  val bottomSheetState = rememberBottomSheetState()
67
53
 
@@ -78,10 +64,14 @@ fun DevLauncherBottomTabsNavigator(
78
64
  ExitTransition.None
79
65
  }
80
66
  ) {
81
- composable<Home> { DefaultScreenContainer { HomeScreen(state, onProfileClick = { bottomSheetState.jumpTo(Full) }) } }
82
- composable<Settings> { DefaultScreenContainer { SettingsScreen() } }
67
+ composable<Home> {
68
+ HomeRoute(onProfileClick = { bottomSheetState.jumpTo(Full) })
69
+ }
70
+ composable<Settings> {
71
+ SettingsRoute()
72
+ }
83
73
  }
84
74
  }
85
75
 
86
- Profile(bottomSheetState)
76
+ ProfileRoute(bottomSheetState)
87
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
+ }
@@ -2,39 +2,39 @@ package expo.modules.devlauncher.compose
2
2
 
3
3
  import androidx.lifecycle.ViewModel
4
4
  import androidx.lifecycle.viewModelScope
5
- import expo.modules.devlauncher.services.ApolloClientService
6
5
  import expo.modules.devlauncher.services.SessionService
6
+ import expo.modules.devlauncher.services.UserState
7
7
  import expo.modules.devlauncher.services.inject
8
- import kotlinx.coroutines.Dispatchers
9
8
  import kotlinx.coroutines.flow.MutableStateFlow
10
9
  import kotlinx.coroutines.flow.asStateFlow
11
10
  import kotlinx.coroutines.flow.launchIn
12
11
  import kotlinx.coroutines.flow.onEach
13
12
  import kotlinx.coroutines.flow.update
14
- import kotlinx.coroutines.launch
15
13
 
16
- sealed interface ProfileState {
17
- data class Account(
18
- val name: String,
19
- val avatar: String?
20
- )
14
+ data class Account(
15
+ val id: String,
16
+ val name: String,
17
+ val avatar: String?,
18
+ val isSelected: Boolean = false
19
+ )
21
20
 
21
+ sealed interface ProfileState {
22
22
  class LoggedIn(
23
- val isLoading: Boolean = true,
24
23
  val accounts: List<Account> = emptyList()
25
24
  ) : ProfileState
26
25
 
26
+ object Fetching : ProfileState
27
27
  object LoggedOut : ProfileState
28
28
  }
29
29
 
30
30
  class ProfileViewModel : ViewModel() {
31
31
  sealed interface Action {
32
32
  class LogIn(val sessionSecret: String) : Action
33
+ class SwitchAccount(val account: Account) : Action
33
34
  object SignOut : Action
34
35
  }
35
36
 
36
- val sessionSession = inject<SessionService>()
37
- val apolloClientService = inject<ApolloClientService>()
37
+ val session = inject<SessionService>()
38
38
 
39
39
  private val _state = MutableStateFlow<ProfileState>(ProfileState.LoggedOut)
40
40
 
@@ -42,54 +42,40 @@ class ProfileViewModel : ViewModel() {
42
42
  get() = _state.asStateFlow()
43
43
 
44
44
  init {
45
- sessionSession.session.onEach { newSession ->
46
- if (newSession != null) {
47
- fetchMe()
48
- _state.update {
49
- ProfileState.LoggedIn()
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
+ )
50
60
  }
51
- } else {
52
- _state.update { ProfileState.LoggedOut }
53
61
  }
54
62
  }
55
63
  .launchIn(viewModelScope)
56
64
  }
57
65
 
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
66
  fun onAction(action: Action) {
83
67
  when (action) {
84
68
  is Action.LogIn -> {
85
- sessionSession.setSession(
69
+ session.setSession(
86
70
  Session(action.sessionSecret)
87
71
  )
88
72
  }
89
73
 
90
74
  Action.SignOut -> {
91
- sessionSession.setSession(null)
75
+ session.setSession(null)
92
76
  }
77
+
78
+ is Action.SwitchAccount -> session.switchAccount(action.account.id)
93
79
  }
94
80
  }
95
81
  }
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- package expo.modules.devlauncher.compose.screens
1
+ package expo.modules.devlauncher.compose.routes
2
2
 
3
3
  import androidx.activity.compose.rememberLauncherForActivityResult
4
4
  import androidx.compose.runtime.Composable
@@ -18,7 +18,7 @@ import expo.modules.devlauncher.compose.ui.ProfileLayout
18
18
  import expo.modules.devlauncher.compose.ui.SignUp
19
19
 
20
20
  @Composable
21
- fun Profile(
21
+ fun ProfileRoute(
22
22
  bottomSheetState: ModalBottomSheetState,
23
23
  viewModel: ProfileViewModel = viewModel()
24
24
  ) {
@@ -44,6 +44,9 @@ fun Profile(
44
44
  accounts = state.accounts,
45
45
  onSignOut = {
46
46
  viewModel.onAction(ProfileViewModel.Action.SignOut)
47
+ },
48
+ onClick = { account ->
49
+ viewModel.onAction(ProfileViewModel.Action.SwitchAccount(account))
47
50
  }
48
51
  )
49
52
  }
@@ -58,6 +61,9 @@ fun Profile(
58
61
  }
59
62
  )
60
63
  }
64
+
65
+ ProfileState.Fetching -> {
66
+ }
61
67
  }
62
68
  }
63
69
  }
@@ -0,0 +1,16 @@
1
+ package expo.modules.devlauncher.compose.routes
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import expo.modules.devlauncher.compose.DefaultScreenContainer
5
+ import expo.modules.devlauncher.compose.screens.SettingsScreen
6
+ import kotlinx.serialization.Serializable
7
+
8
+ @Serializable
9
+ object Settings
10
+
11
+ @Composable
12
+ fun SettingsRoute() {
13
+ DefaultScreenContainer {
14
+ SettingsScreen()
15
+ }
16
+ }
@@ -19,8 +19,8 @@ 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
+ import expo.modules.devlauncher.compose.HomeAction
23
+ import expo.modules.devlauncher.compose.HomeState
24
24
  import expo.modules.devlauncher.compose.primitives.Accordion
25
25
  import expo.modules.devlauncher.compose.ui.AppHeader
26
26
  import expo.modules.devlauncher.compose.ui.RunningAppCard
@@ -33,10 +33,18 @@ import expo.modules.devmenu.compose.primitives.Text
33
33
  import expo.modules.devmenu.compose.theme.Theme
34
34
 
35
35
  @Composable
36
- fun HomeScreen(state: DevLauncherState, onProfileClick: () -> Unit) {
36
+ fun HomeScreen(
37
+ state: HomeState,
38
+ onAction: (HomeAction) -> Unit,
39
+ onProfileClick: () -> Unit
40
+ ) {
37
41
  Column {
38
42
  ScreenHeaderContainer(modifier = Modifier.padding(Theme.spacing.medium)) {
39
- AppHeader(state.appName, onProfileClick = onProfileClick)
43
+ AppHeader(
44
+ appName = state.appName,
45
+ currentAccount = state.currentAccount,
46
+ onProfileClick = onProfileClick
47
+ )
40
48
  }
41
49
 
42
50
  Column(
@@ -49,7 +57,7 @@ fun HomeScreen(state: DevLauncherState, onProfileClick: () -> Unit) {
49
57
  Spacer(Theme.spacing.small)
50
58
 
51
59
  SectionHeader(
52
- "Development",
60
+ "Development servers",
53
61
  leftIcon = {
54
62
  Image(
55
63
  painter = painterResource(R.drawable._expodevclientcomponents_assets_terminalicon),
@@ -73,12 +81,12 @@ fun HomeScreen(state: DevLauncherState, onProfileClick: () -> Unit) {
73
81
  RunningAppCard(
74
82
  appIp = packager.url
75
83
  ) {
76
- state.onAction(DevLauncherAction.OpenApp(packager.url))
84
+ onAction(HomeAction.OpenApp(packager.url))
77
85
  }
78
86
  Divider()
79
87
  }
80
88
 
81
- Accordion("Enter URL", initialState = false) {
89
+ Accordion("Enter URL manually", initialState = false) {
82
90
  val url = remember { mutableStateOf("") }
83
91
 
84
92
  Column {
@@ -89,7 +97,7 @@ fun HomeScreen(state: DevLauncherState, onProfileClick: () -> Unit) {
89
97
  onValueChange = { newValue ->
90
98
  url.value = newValue
91
99
  },
92
- placeholder = "http://10.0.2.2:801",
100
+ placeholder = "http://10.0.2.2:8081",
93
101
  textStyle = Theme.typography.medium.font,
94
102
  maxLines = 1,
95
103
  modifier = Modifier
@@ -109,7 +117,7 @@ fun HomeScreen(state: DevLauncherState, onProfileClick: () -> Unit) {
109
117
  Spacer(Theme.spacing.tiny)
110
118
 
111
119
  Button(onClick = {
112
- state.onAction(DevLauncherAction.OpenApp(url.value))
120
+ onAction(HomeAction.OpenApp(url.value))
113
121
  }, modifier = Modifier.fillMaxWidth()) {
114
122
  Row(modifier = Modifier.padding(vertical = Theme.spacing.small)) {
115
123
  Text("Connect")
@@ -130,5 +138,5 @@ fun HomeScreen(state: DevLauncherState, onProfileClick: () -> Unit) {
130
138
  @Preview(showBackground = true)
131
139
  @Composable
132
140
  fun HomeScreenPreview() {
133
- HomeScreen(state = DevLauncherState(), onProfileClick = {})
141
+ HomeScreen(state = HomeState(), onAction = {}, onProfileClick = {})
134
142
  }
@@ -0,0 +1,37 @@
1
+ package expo.modules.devlauncher.compose.ui
2
+
3
+ import androidx.compose.foundation.layout.padding
4
+ import androidx.compose.foundation.layout.size
5
+ import androidx.compose.runtime.Composable
6
+ import androidx.compose.ui.Modifier
7
+ import androidx.compose.ui.res.painterResource
8
+ import androidx.compose.ui.unit.Dp
9
+ import com.composables.core.Icon
10
+ import expo.modules.devlauncher.R
11
+ import expo.modules.devlauncher.compose.primitives.AsyncImage
12
+ import expo.modules.devmenu.compose.primitives.RoundedSurface
13
+ import expo.modules.devmenu.compose.theme.Theme
14
+
15
+ @Composable
16
+ fun AccountAvatar(
17
+ url: String?,
18
+ size: Dp = Theme.sizing.icon.medium,
19
+ modifier: Modifier = Modifier
20
+ ) {
21
+ RoundedSurface(
22
+ borderRadius = Theme.sizing.borderRadius.full,
23
+ modifier = Modifier.size(size).then(modifier)
24
+ ) {
25
+ if (url != null) {
26
+ AsyncImage(
27
+ url = url
28
+ )
29
+ } else {
30
+ Icon(
31
+ painterResource(R.drawable._expodevclientcomponents_assets_buildingicon),
32
+ contentDescription = "Avatar",
33
+ modifier = Modifier.padding(Theme.spacing.micro)
34
+ )
35
+ }
36
+ }
37
+ }
@@ -1,27 +1,60 @@
1
1
  package expo.modules.devlauncher.compose.ui
2
2
 
3
3
  import androidx.compose.foundation.layout.Column
4
+ import androidx.compose.foundation.layout.padding
4
5
  import androidx.compose.runtime.Composable
5
- import com.apollographql.apollo.api.label
6
- import expo.modules.devlauncher.compose.ProfileState
6
+ import androidx.compose.ui.Modifier
7
+ import androidx.compose.ui.res.painterResource
8
+ import com.composables.core.Icon
9
+ import com.composeunstyled.Button
10
+ import expo.modules.devlauncher.R
11
+ import expo.modules.devlauncher.compose.Account
12
+ import expo.modules.devmenu.compose.primitives.Divider
7
13
  import expo.modules.devmenu.compose.primitives.RoundedSurface
14
+ import expo.modules.devmenu.compose.primitives.RowLayout
8
15
  import expo.modules.devmenu.compose.primitives.Spacer
16
+ import expo.modules.devmenu.compose.primitives.Text
9
17
  import expo.modules.devmenu.compose.theme.Theme
10
- import expo.modules.devmenu.compose.ui.MenuButton
11
18
 
12
19
  @Composable
13
20
  fun AccountSelector(
14
- accounts: List<ProfileState.Account>,
21
+ accounts: List<Account>,
22
+ onClick: (Account) -> Unit = {},
15
23
  onSignOut: () -> Unit = {}
16
24
  ) {
17
25
  Column {
18
26
  RoundedSurface {
19
27
  Column {
20
- for (account in accounts) {
21
- MenuButton(
22
-
23
- label = account.name
24
- )
28
+ for ((index, account) in accounts.withIndex()) {
29
+ Button(
30
+ onClick = { onClick(account) },
31
+ enabled = !account.isSelected
32
+ ) {
33
+ val avatar = @Composable {
34
+ RoundedSurface(borderRadius = Theme.sizing.borderRadius.full) {
35
+ AccountAvatar(
36
+ account.avatar
37
+ )
38
+ }
39
+ }
40
+ RowLayout(
41
+ modifier = Modifier.padding(Theme.spacing.small),
42
+ leftComponent = avatar,
43
+ rightComponent = {
44
+ if (account.isSelected) {
45
+ Icon(
46
+ painterResource(R.drawable._expodevclientcomponents_assets_checkicon),
47
+ contentDescription = "Checked"
48
+ )
49
+ }
50
+ }
51
+ ) {
52
+ Text(account.name)
53
+ }
54
+ }
55
+ if (index < accounts.size - 1) {
56
+ Divider()
57
+ }
25
58
  }
26
59
  }
27
60
  }