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.
Files changed (74) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/android/build.gradle +15 -2
  3. package/android/src/debug/AndroidManifest.xml +6 -1
  4. package/android/src/debug/java/expo/modules/devlauncher/compose/AuthActivity.kt +136 -0
  5. package/android/src/debug/java/expo/modules/devlauncher/compose/BindingView.kt +1 -6
  6. package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherBottomTabsNavigator.kt +12 -31
  7. package/android/src/debug/java/expo/modules/devlauncher/compose/HomeViewModel.kt +84 -0
  8. package/android/src/debug/java/expo/modules/devlauncher/compose/ProfileViewModel.kt +81 -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 +2 -2
  11. package/android/src/debug/java/expo/modules/devlauncher/compose/primitives/Asyncimage.kt +40 -0
  12. package/android/src/debug/java/expo/modules/devlauncher/compose/routes/Home.kt +23 -0
  13. package/android/src/debug/java/expo/modules/devlauncher/compose/routes/Profile.kt +70 -0
  14. package/android/src/debug/java/expo/modules/devlauncher/compose/routes/Settings.kt +16 -0
  15. package/android/src/debug/java/expo/modules/devlauncher/compose/screens/{Home.kt → HomeScreen.kt} +29 -17
  16. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/AccountAvatar.kt +37 -0
  17. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/AccountSelector.kt +70 -0
  18. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/ActionButton.kt +35 -0
  19. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/AppHeader.kt +26 -18
  20. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/BottomTabBar.kt +3 -3
  21. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/ProfileLayout.kt +45 -0
  22. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/RunningAppCard.kt +91 -0
  23. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/SectionHeader.kt +6 -16
  24. package/android/src/debug/java/expo/modules/devlauncher/compose/ui/SignUp.kt +49 -0
  25. package/android/src/debug/java/expo/modules/devlauncher/services/ApolloClientService.kt +41 -0
  26. package/android/src/debug/java/expo/modules/devlauncher/services/DependencyInjection.kt +79 -0
  27. package/android/src/debug/java/expo/modules/devlauncher/services/HttpClientService.kt +9 -0
  28. package/android/src/debug/java/expo/modules/devlauncher/services/ImageLoaderService.kt +69 -0
  29. package/android/src/debug/java/expo/modules/devlauncher/services/PackagerService.kt +4 -0
  30. package/android/src/debug/java/expo/modules/devlauncher/services/SessionService.kt +114 -0
  31. package/android/src/main/graphql/GetBranches.graphql +32 -0
  32. package/android/src/main/graphql/GetUpdates.graphql +34 -0
  33. package/android/src/main/graphql/Me.graphql +18 -0
  34. package/android/src/main/graphql/schema.graphqls +9850 -0
  35. package/ios/Assets.xcassets/branch-icon.imageset/Contents.json +56 -0
  36. package/ios/Assets.xcassets/branch-icon.imageset/branch-icon-light.png +0 -0
  37. package/ios/Assets.xcassets/branch-icon.imageset/branch-icon-light@2x.png +0 -0
  38. package/ios/Assets.xcassets/branch-icon.imageset/branch-icon-light@3x.png +0 -0
  39. package/ios/Assets.xcassets/branch-icon.imageset/branch-icon.png +0 -0
  40. package/ios/Assets.xcassets/branch-icon.imageset/branch-icon@2x.png +0 -0
  41. package/ios/Assets.xcassets/branch-icon.imageset/branch-icon@3x.png +0 -0
  42. package/ios/Assets.xcassets/update-icon.imageset/Contents.json +56 -0
  43. package/ios/Assets.xcassets/update-icon.imageset/update-icon-light.png +0 -0
  44. package/ios/Assets.xcassets/update-icon.imageset/update-icon-light@2x.png +0 -0
  45. package/ios/Assets.xcassets/update-icon.imageset/update-icon-light@3x.png +0 -0
  46. package/ios/Assets.xcassets/update-icon.imageset/update-icon.png +0 -0
  47. package/ios/Assets.xcassets/update-icon.imageset/update-icon@2x.png +0 -0
  48. package/ios/Assets.xcassets/update-icon.imageset/update-icon@3x.png +0 -0
  49. package/ios/EXDevLauncherController.m +2 -4
  50. package/ios/SwiftUI/AccountSheet.swift +173 -89
  51. package/ios/SwiftUI/Data.swift +34 -0
  52. package/ios/SwiftUI/DevLauncherErrors.swift +38 -0
  53. package/ios/SwiftUI/DevLauncherViewModel.swift +119 -62
  54. package/ios/SwiftUI/{DevLauncherSwiftUIViews.swift → DevLauncherViews.swift} +3 -68
  55. package/ios/SwiftUI/DevServersView.swift +26 -17
  56. package/ios/SwiftUI/GraphQL/APIClient.swift +83 -0
  57. package/ios/SwiftUI/GraphQL/Models.swift +116 -0
  58. package/ios/SwiftUI/GraphQL/Queries.swift +150 -0
  59. package/ios/SwiftUI/HomeTabView.swift +5 -3
  60. package/ios/SwiftUI/Navigation/Navigation.swift +58 -3
  61. package/ios/SwiftUI/SettingsTabView.swift +70 -0
  62. package/ios/SwiftUI/UpdatesTab/NotSignedInView.swift +32 -0
  63. package/ios/SwiftUI/UpdatesTab/NotUsingUpdatesView.swift +38 -0
  64. package/ios/SwiftUI/UpdatesTab/UpdateRow.swift +76 -0
  65. package/ios/SwiftUI/UpdatesTab/UpdateUtils.swift +5 -0
  66. package/ios/SwiftUI/UpdatesTab/UpdatesListView.swift +196 -0
  67. package/ios/SwiftUI/UpdatesTab/UpdatesTabView.swift +37 -0
  68. package/ios/SwiftUI/Utils/Avatar.swift +66 -0
  69. package/ios/SwiftUI/Utils/Utils.swift +22 -0
  70. package/package.json +4 -4
  71. package/android/src/debug/java/expo/modules/devlauncher/compose/DevLauncherViewModel.kt +0 -11
  72. package/android/src/debug/java/expo/modules/devlauncher/compose/screens/SignUp.kt +0 -100
  73. package/ios/SwiftUI/ExtensionsTabView.swift +0 -68
  74. /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
@@ -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-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-20250630-547cd82"
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, lazyViewModel: Lazy<DevLauncherViewModel>) : LinearLayout(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
  }
@@ -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.screens.HomeScreen
20
- import expo.modules.devlauncher.compose.screens.HomeScreenState
21
- import expo.modules.devlauncher.compose.screens.SettingsScreen
22
- import expo.modules.devlauncher.compose.screens.SignUp
23
- import expo.modules.devlauncher.compose.ui.BottomSheet
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> { DefaultScreenContainer { HomeScreen(HomeScreenState(appName = "BareExpo", onProfileClick = { bottomSheetState.jumpTo(Full) })) } }
79
- composable<Settings> { DefaultScreenContainer { SettingsScreen() } }
67
+ composable<Home> {
68
+ HomeRoute(onProfileClick = { bottomSheetState.jumpTo(Full) })
69
+ }
70
+ composable<Settings> {
71
+ SettingsRoute()
72
+ }
80
73
  }
81
74
  }
82
75
 
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
- }
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.small)
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
+ }