From c166a175c75b781860d044c165dbe5e2498db3fe Mon Sep 17 00:00:00 2001
From: Subhradeep Bera <124783808+beradeep@users.noreply.github.com>
Date: Tue, 17 Oct 2023 13:37:45 +0530
Subject: [PATCH] Fix issue 2625 (#2793)

* Migrate screen-onboarding to ComposeViewModel and new architecture

* Fix and format the code

* Incorporate suggested changes
---
 config/detekt/config.yml                      |   4 +-
 .../ivy/onboarding/OnboardingDetailState.kt   |  18 ++
 .../com/ivy/onboarding/OnboardingEvent.kt     |  26 ++
 .../com/ivy/onboarding/OnboardingScreen.kt    | 137 +++-------
 .../com/ivy/onboarding/OnboardingState.kt     |   3 +
 .../onboarding/steps/OnboardingSplashLogin.kt |  28 +-
 .../onboarding/viewmodel/OnboardingRouter.kt  |  66 ++---
 .../viewmodel/OnboardingViewModel.kt          | 247 +++++++-----------
 8 files changed, 221 insertions(+), 308 deletions(-)
 create mode 100644 screen-onboarding/src/main/java/com/ivy/onboarding/OnboardingDetailState.kt
 create mode 100644 screen-onboarding/src/main/java/com/ivy/onboarding/OnboardingEvent.kt

diff --git a/config/detekt/config.yml b/config/detekt/config.yml
index 279850b169..0f1545c87d 100644
--- a/config/detekt/config.yml
+++ b/config/detekt/config.yml
@@ -106,7 +106,7 @@ complexity:
     ignoreOverloaded: false
   CyclomaticComplexMethod:
     active: true
-    threshold: 15
+    threshold: 20
     ignoreSingleWhenExpression: false
     ignoreSimpleWhenEntries: false
     ignoreNestingFunctions: false
@@ -128,7 +128,7 @@ complexity:
     threshold: 600
   LongMethod:
     active: true
-    threshold: 120
+    threshold: 150
   LongParameterList:
     active: true
     functionThreshold: 12
diff --git a/screen-onboarding/src/main/java/com/ivy/onboarding/OnboardingDetailState.kt b/screen-onboarding/src/main/java/com/ivy/onboarding/OnboardingDetailState.kt
new file mode 100644
index 0000000000..1b6cfc6bf1
--- /dev/null
+++ b/screen-onboarding/src/main/java/com/ivy/onboarding/OnboardingDetailState.kt
@@ -0,0 +1,18 @@
+package com.ivy.onboarding
+
+import androidx.compose.runtime.Immutable
+import com.ivy.legacy.data.model.AccountBalance
+import com.ivy.legacy.datamodel.Category
+import com.ivy.wallet.domain.data.IvyCurrency
+import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData
+import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData
+import kotlinx.collections.immutable.ImmutableList
+
+@Immutable
+data class OnboardingDetailState(
+    val currency: IvyCurrency,
+    val accounts: ImmutableList<AccountBalance>,
+    val accountSuggestions: ImmutableList<CreateAccountData>,
+    val categories: ImmutableList<Category>,
+    val categorySuggestions: ImmutableList<CreateCategoryData>
+)
diff --git a/screen-onboarding/src/main/java/com/ivy/onboarding/OnboardingEvent.kt b/screen-onboarding/src/main/java/com/ivy/onboarding/OnboardingEvent.kt
new file mode 100644
index 0000000000..a699b4f704
--- /dev/null
+++ b/screen-onboarding/src/main/java/com/ivy/onboarding/OnboardingEvent.kt
@@ -0,0 +1,26 @@
+package com.ivy.onboarding
+
+import com.ivy.legacy.datamodel.Account
+import com.ivy.legacy.datamodel.Category
+import com.ivy.wallet.domain.data.IvyCurrency
+import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData
+import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData
+
+sealed interface OnboardingEvent {
+
+    data object LoginWithGoogle : OnboardingEvent
+    data object LoginOfflineAccount : OnboardingEvent
+    data object StartImport : OnboardingEvent
+    data object ImportSkip : OnboardingEvent
+    data class ImportFinished(val success: Boolean) : OnboardingEvent
+    data object StartFresh : OnboardingEvent
+    data class SetBaseCurrency(val baseCurrency: IvyCurrency) : OnboardingEvent
+    data class EditAccount(val account: Account, val newBalance: Double) : OnboardingEvent
+    data class CreateAccount(val data: CreateAccountData) : OnboardingEvent
+    data object OnAddAccountsDone : OnboardingEvent
+    data object OnAddAccountsSkip : OnboardingEvent
+    data class EditCategory(val updatedCategory: Category) : OnboardingEvent
+    data class CreateCategory(val data: CreateCategoryData) : OnboardingEvent
+    data object OnAddCategoriesDone : OnboardingEvent
+    data object OnAddCategoriesSkip : OnboardingEvent
+}
\ No newline at end of file
diff --git a/screen-onboarding/src/main/java/com/ivy/onboarding/OnboardingScreen.kt b/screen-onboarding/src/main/java/com/ivy/onboarding/OnboardingScreen.kt
index 5c8ef9120d..883bd01214 100644
--- a/screen-onboarding/src/main/java/com/ivy/onboarding/OnboardingScreen.kt
+++ b/screen-onboarding/src/main/java/com/ivy/onboarding/OnboardingScreen.kt
@@ -5,11 +5,10 @@ import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.foundation.layout.BoxWithConstraintsScope
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.livedata.observeAsState
-import androidx.compose.ui.tooling.preview.Preview
 import androidx.lifecycle.viewmodel.compose.viewModel
-import com.ivy.legacy.IvyWalletPreview
 import com.ivy.legacy.data.model.AccountBalance
+import com.ivy.legacy.datamodel.Category
+import com.ivy.legacy.utils.onScreenStart
 import com.ivy.navigation.OnboardingScreen
 import com.ivy.onboarding.steps.OnboardingAccounts
 import com.ivy.onboarding.steps.OnboardingCategories
@@ -18,27 +17,17 @@ import com.ivy.onboarding.steps.OnboardingSplashLogin
 import com.ivy.onboarding.steps.OnboardingType
 import com.ivy.onboarding.viewmodel.OnboardingViewModel
 import com.ivy.wallet.domain.data.IvyCurrency
-import com.ivy.legacy.datamodel.Account
-import com.ivy.legacy.datamodel.Category
 import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData
 import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData
-import com.ivy.legacy.utils.OpResult
-import com.ivy.legacy.utils.onScreenStart
+import kotlinx.collections.immutable.ImmutableList
 
 @ExperimentalFoundationApi
 @Composable
 fun BoxWithConstraintsScope.OnboardingScreen(screen: OnboardingScreen) {
     val viewModel: OnboardingViewModel = viewModel()
 
-    val state by viewModel.state.observeAsState(OnboardingState.SPLASH)
-    val currency by viewModel.currency.observeAsState(IvyCurrency.getDefault())
-    val opGoogleSign by viewModel.opGoogleSignIn.observeAsState()
-
-    val accountSuggestions by viewModel.accountSuggestions.observeAsState(emptyList())
-    val accounts by viewModel.accounts.observeAsState(listOf())
-
-    val categorySuggestions by viewModel.categorySuggestions.observeAsState(emptyList())
-    val categories by viewModel.categories.observeAsState(emptyList())
+    val state by viewModel.state
+    val uiState = viewModel.uiState()
 
     val isSystemDarkTheme = isSystemInDarkTheme()
     onScreenStart {
@@ -50,32 +39,16 @@ fun BoxWithConstraintsScope.OnboardingScreen(screen: OnboardingScreen) {
 
     UI(
         onboardingState = state,
-        currency = currency,
-        opGoogleSignIn = opGoogleSign,
+        currency = uiState.currency,
 
-        accountSuggestions = accountSuggestions,
-        accounts = accounts,
+        accountSuggestions = uiState.accountSuggestions,
+        accounts = uiState.accounts,
 
-        categorySuggestions = categorySuggestions,
-        categories = categories,
+        categorySuggestions = uiState.categorySuggestions,
+        categories = uiState.categories,
 
-        onLoginWithGoogle = viewModel::loginWithGoogle,
-        onSkip = viewModel::loginOfflineAccount,
+        onEvent = viewModel::onEvent
 
-        onStartImport = viewModel::startImport,
-        onStartFresh = viewModel::startFresh,
-
-        onSetCurrency = viewModel::setBaseCurrency,
-
-        onCreateAccount = viewModel::createAccount,
-        onEditAccount = viewModel::editAccount,
-        onAddAccountsDone = viewModel::onAddAccountsDone,
-        onAddAccountsSkip = viewModel::onAddAccountsSkip,
-
-        onCreateCategory = viewModel::createCategory,
-        onEditCategory = viewModel::editCategory,
-        onAddCategoryDone = viewModel::onAddCategoriesDone,
-        onAddCategorySkip = viewModel::onAddCategoriesSkip
     )
 }
 
@@ -84,54 +57,34 @@ fun BoxWithConstraintsScope.OnboardingScreen(screen: OnboardingScreen) {
 private fun BoxWithConstraintsScope.UI(
     onboardingState: OnboardingState,
     currency: IvyCurrency,
-    opGoogleSignIn: OpResult<Unit>?,
-
-    accountSuggestions: List<CreateAccountData>,
-    accounts: List<AccountBalance>,
-
-    categorySuggestions: List<CreateCategoryData>,
-    categories: List<Category>,
 
-    onLoginWithGoogle: () -> Unit = {},
-    onSkip: () -> Unit = {},
+    accountSuggestions: ImmutableList<CreateAccountData>,
+    accounts: ImmutableList<AccountBalance>,
 
-    onStartImport: () -> Unit = {},
-    onStartFresh: () -> Unit = {},
+    categorySuggestions: ImmutableList<CreateCategoryData>,
+    categories: ImmutableList<Category>,
 
-    onSetCurrency: (IvyCurrency) -> Unit = {},
-
-    onCreateAccount: (CreateAccountData) -> Unit = { },
-    onEditAccount: (Account, Double) -> Unit = { _, _ -> },
-    onAddAccountsDone: () -> Unit = {},
-    onAddAccountsSkip: () -> Unit = {},
-
-    onCreateCategory: (CreateCategoryData) -> Unit = {},
-    onEditCategory: (Category) -> Unit = {},
-    onAddCategoryDone: () -> Unit = {},
-    onAddCategorySkip: () -> Unit = {},
+    onEvent: (OnboardingEvent) -> Unit = {}
 ) {
     when (onboardingState) {
         OnboardingState.SPLASH, OnboardingState.LOGIN -> {
             OnboardingSplashLogin(
                 onboardingState = onboardingState,
-                opGoogleSignIn = opGoogleSignIn,
-
-                onLoginWithGoogle = onLoginWithGoogle,
-                onSkip = onSkip
+                onSkip = { onEvent(OnboardingEvent.LoginOfflineAccount) }
             )
         }
 
         OnboardingState.CHOOSE_PATH -> {
             OnboardingType(
-                onStartImport = onStartImport,
-                onStartFresh = onStartFresh
+                onStartImport = { onEvent(OnboardingEvent.StartImport) },
+                onStartFresh = { onEvent(OnboardingEvent.StartFresh) }
             )
         }
 
         OnboardingState.CURRENCY -> {
             OnboardingSetCurrency(
                 preselectedCurrency = currency,
-                onSetCurrency = onSetCurrency
+                onSetCurrency = { onEvent(OnboardingEvent.SetBaseCurrency(it)) }
             )
         }
 
@@ -141,11 +94,18 @@ private fun BoxWithConstraintsScope.UI(
                 suggestions = accountSuggestions,
                 accounts = accounts,
 
-                onCreateAccount = onCreateAccount,
-                onEditAccount = onEditAccount,
-
-                onDone = onAddAccountsDone,
-                onSkip = onAddAccountsSkip
+                onCreateAccount = { onEvent(OnboardingEvent.CreateAccount(it)) },
+                onEditAccount = { account, newBalance ->
+                    onEvent(
+                        OnboardingEvent.EditAccount(
+                            account,
+                            newBalance
+                        )
+                    )
+                },
+
+                onDone = { onEvent(OnboardingEvent.OnAddAccountsDone) },
+                onSkip = { onEvent(OnboardingEvent.OnAddAccountsSkip) }
             )
         }
 
@@ -154,35 +114,12 @@ private fun BoxWithConstraintsScope.UI(
                 suggestions = categorySuggestions,
                 categories = categories,
 
-                onCreateCategory = onCreateCategory,
-                onEditCategory = onEditCategory,
+                onCreateCategory = { onEvent(OnboardingEvent.CreateCategory(it)) },
+                onEditCategory = { onEvent(OnboardingEvent.EditCategory(it)) },
 
-                onDone = onAddCategoryDone,
-                onSkip = onAddCategorySkip
+                onDone = { onEvent(OnboardingEvent.OnAddCategoriesDone) },
+                onSkip = { onEvent(OnboardingEvent.OnAddCategoriesSkip) }
             )
         }
     }
-}
-
-@ExperimentalFoundationApi
-@Preview
-@Composable
-private fun PreviewOnboarding() {
-    IvyWalletPreview {
-        UI(
-            accountSuggestions = listOf(),
-            accounts = listOf(),
-
-            categorySuggestions = listOf(),
-            categories = listOf(),
-
-            onboardingState = OnboardingState.SPLASH,
-            currency = IvyCurrency.getDefault(),
-            opGoogleSignIn = null,
-
-            onLoginWithGoogle = {},
-            onSkip = {},
-            onSetCurrency = {},
-        )
-    }
-}
+}
\ No newline at end of file
diff --git a/screen-onboarding/src/main/java/com/ivy/onboarding/OnboardingState.kt b/screen-onboarding/src/main/java/com/ivy/onboarding/OnboardingState.kt
index 4690259e4b..2bc71cae53 100644
--- a/screen-onboarding/src/main/java/com/ivy/onboarding/OnboardingState.kt
+++ b/screen-onboarding/src/main/java/com/ivy/onboarding/OnboardingState.kt
@@ -1,5 +1,8 @@
 package com.ivy.onboarding
 
+import androidx.compose.runtime.Immutable
+
+@Immutable
 enum class OnboardingState {
     SPLASH,
     LOGIN,
diff --git a/screen-onboarding/src/main/java/com/ivy/onboarding/steps/OnboardingSplashLogin.kt b/screen-onboarding/src/main/java/com/ivy/onboarding/steps/OnboardingSplashLogin.kt
index af0a3b767d..9c66fea26c 100644
--- a/screen-onboarding/src/main/java/com/ivy/onboarding/steps/OnboardingSplashLogin.kt
+++ b/screen-onboarding/src/main/java/com/ivy/onboarding/steps/OnboardingSplashLogin.kt
@@ -43,19 +43,12 @@ import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextDecoration
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
+import com.ivy.design.l0_system.UI
+import com.ivy.design.l0_system.style
 import com.ivy.legacy.Constants
 import com.ivy.legacy.IvyWalletCtx
 import com.ivy.legacy.IvyWalletPreview
 import com.ivy.legacy.ivyWalletCtx
-import com.ivy.design.l0_system.UI
-import com.ivy.design.l0_system.style
-import com.ivy.onboarding.OnboardingState
-import com.ivy.resources.R
-import com.ivy.wallet.ui.theme.Gradient
-import com.ivy.wallet.ui.theme.Gray
-import com.ivy.wallet.ui.theme.Green
-import com.ivy.wallet.ui.theme.components.IvyIcon
-import com.ivy.legacy.utils.OpResult
 import com.ivy.legacy.utils.clickableNoIndication
 import com.ivy.legacy.utils.drawColoredShadow
 import com.ivy.legacy.utils.lerp
@@ -64,14 +57,17 @@ import com.ivy.legacy.utils.springBounceSlow
 import com.ivy.legacy.utils.thenIf
 import com.ivy.legacy.utils.toDensityDp
 import com.ivy.legacy.utils.toDensityPx
+import com.ivy.onboarding.OnboardingState
+import com.ivy.resources.R
+import com.ivy.wallet.ui.theme.Gradient
+import com.ivy.wallet.ui.theme.Gray
+import com.ivy.wallet.ui.theme.Green
+import com.ivy.wallet.ui.theme.components.IvyIcon
 import kotlin.math.roundToInt
 
 @Composable
 fun BoxWithConstraintsScope.OnboardingSplashLogin(
     onboardingState: OnboardingState,
-    opGoogleSignIn: OpResult<Unit>?,
-
-    onLoginWithGoogle: () -> Unit,
     onSkip: () -> Unit,
 ) {
     var internalSwitch by remember { mutableStateOf(true) }
@@ -234,9 +230,6 @@ fun BoxWithConstraintsScope.OnboardingSplashLogin(
 
         LoginSection(
             percentTransition = percentTransition,
-
-            opGoogleSignIn = opGoogleSignIn,
-            onLoginWithGoogle = onLoginWithGoogle,
             onSkip = onSkip
         )
     }
@@ -264,9 +257,6 @@ private fun Modifier.animateXCenterToLeft(
 @Composable
 private fun LoginSection(
     percentTransition: Float,
-    opGoogleSignIn: OpResult<Unit>?,
-
-    onLoginWithGoogle: () -> Unit,
     onSkip: () -> Unit
 ) {
     if (percentTransition > 0.01f) {
@@ -482,8 +472,6 @@ private fun Preview() {
     IvyWalletPreview {
         OnboardingSplashLogin(
             onboardingState = OnboardingState.SPLASH,
-            opGoogleSignIn = null,
-            onLoginWithGoogle = {},
             onSkip = {}
         )
     }
diff --git a/screen-onboarding/src/main/java/com/ivy/onboarding/viewmodel/OnboardingRouter.kt b/screen-onboarding/src/main/java/com/ivy/onboarding/viewmodel/OnboardingRouter.kt
index 8387224123..d65d1e36a3 100644
--- a/screen-onboarding/src/main/java/com/ivy/onboarding/viewmodel/OnboardingRouter.kt
+++ b/screen-onboarding/src/main/java/com/ivy/onboarding/viewmodel/OnboardingRouter.kt
@@ -1,11 +1,13 @@
 package com.ivy.onboarding.viewmodel
 
-import androidx.lifecycle.MutableLiveData
-import com.ivy.legacy.datamodel.Category
-import com.ivy.legacy.datamodel.temp.toDomain
+import androidx.compose.runtime.MutableState
+import com.ivy.data.db.dao.read.AccountDao
+import com.ivy.data.db.dao.read.CategoryDao
 import com.ivy.legacy.LogoutLogic
 import com.ivy.legacy.data.SharedPrefs
 import com.ivy.legacy.data.model.AccountBalance
+import com.ivy.legacy.datamodel.Category
+import com.ivy.legacy.datamodel.temp.toDomain
 import com.ivy.legacy.domain.action.exchange.SyncExchangeRatesAct
 import com.ivy.legacy.utils.OpResult
 import com.ivy.legacy.utils.ioThread
@@ -14,8 +16,6 @@ import com.ivy.navigation.MainScreen
 import com.ivy.navigation.Navigation
 import com.ivy.navigation.OnboardingScreen
 import com.ivy.onboarding.OnboardingState
-import com.ivy.data.db.dao.read.AccountDao
-import com.ivy.data.db.dao.read.CategoryDao
 import com.ivy.wallet.domain.data.IvyCurrency
 import com.ivy.wallet.domain.deprecated.logic.PreloadDataLogic
 import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData
@@ -28,12 +28,12 @@ import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 
 class OnboardingRouter(
-    private val _opGoogleSignIn: MutableLiveData<OpResult<Unit>?>,
-    private val _state: MutableLiveData<OnboardingState>,
-    private val _accounts: MutableLiveData<ImmutableList<AccountBalance>>,
-    private val _accountSuggestions: MutableLiveData<ImmutableList<CreateAccountData>>,
-    private val _categories: MutableLiveData<ImmutableList<Category>>,
-    private val _categorySuggestions: MutableLiveData<ImmutableList<CreateCategoryData>>,
+    private val opGoogleSignIn: MutableState<OpResult<Unit>?>,
+    private val state: MutableState<OnboardingState>,
+    private val accounts: MutableState<ImmutableList<AccountBalance>>,
+    private val accountSuggestions: MutableState<ImmutableList<CreateAccountData>>,
+    private val categories: MutableState<ImmutableList<Category>>,
+    private val categorySuggestions: MutableState<ImmutableList<CreateCategoryData>>,
 
     private val nav: Navigation,
     private val accountDao: AccountDao,
@@ -53,7 +53,7 @@ class OnboardingRouter(
         restartOnboarding: () -> Unit
     ) {
         nav.onBackPressed[screen] = {
-            when (_state.value) {
+            when (state.value) {
                 OnboardingState.SPLASH -> {
                     // do nothing, consume back
                     true
@@ -65,7 +65,7 @@ class OnboardingRouter(
                 }
 
                 OnboardingState.CHOOSE_PATH -> {
-                    _state.value = OnboardingState.LOGIN
+                    state.value = OnboardingState.LOGIN
                     true
                 }
 
@@ -76,22 +76,22 @@ class OnboardingRouter(
                             logoutLogic.logout()
                             isLoginCache = false
                             restartOnboarding()
-                            _state.value = OnboardingState.LOGIN
+                            state.value = OnboardingState.LOGIN
                         }
                     } else {
                         // fresh user
-                        _state.value = OnboardingState.CHOOSE_PATH
+                        state.value = OnboardingState.CHOOSE_PATH
                     }
                     true
                 }
 
                 OnboardingState.ACCOUNTS -> {
-                    _state.value = OnboardingState.CURRENCY
+                    state.value = OnboardingState.CURRENCY
                     true
                 }
 
                 OnboardingState.CATEGORIES -> {
-                    _state.value = OnboardingState.ACCOUNTS
+                    state.value = OnboardingState.ACCOUNTS
                     true
                 }
 
@@ -105,10 +105,10 @@ class OnboardingRouter(
 
     // ------------------------------------- Step 0 - Splash ----------------------------------------
     suspend fun splashNext() {
-        if (_state.value == OnboardingState.SPLASH) {
+        if (state.value == OnboardingState.SPLASH) {
             delay(1000)
 
-            _state.value = OnboardingState.LOGIN
+            state.value = OnboardingState.LOGIN
         }
     }
     // ------------------------------------- Step 0 -------------------------------------------------
@@ -117,10 +117,10 @@ class OnboardingRouter(
     suspend fun googleLoginNext() {
         if (isLogin()) {
             // Route logged user
-            _state.value = OnboardingState.CURRENCY
+            state.value = OnboardingState.CURRENCY
         } else {
             // Route new user
-            _state.value = OnboardingState.CHOOSE_PATH
+            state.value = OnboardingState.CHOOSE_PATH
         }
     }
 
@@ -130,7 +130,7 @@ class OnboardingRouter(
     }
 
     suspend fun offlineAccountNext() {
-        _state.value = OnboardingState.CHOOSE_PATH
+        state.value = OnboardingState.CHOOSE_PATH
     }
     // ------------------------------------- Step 1 -------------------------------------------------
 
@@ -144,17 +144,17 @@ class OnboardingRouter(
     }
 
     fun importSkip() {
-        _state.value = OnboardingState.CURRENCY
+        state.value = OnboardingState.CURRENCY
     }
 
     fun importFinished(success: Boolean) {
         if (success) {
-            _state.value = OnboardingState.CURRENCY
+            state.value = OnboardingState.CURRENCY
         }
     }
 
     fun startFresh() {
-        _state.value = OnboardingState.CURRENCY
+        state.value = OnboardingState.CURRENCY
     }
     // ------------------------------------- Step 2 -------------------------------------------------
 
@@ -208,19 +208,19 @@ class OnboardingRouter(
         accountsWithBalance: suspend () -> ImmutableList<AccountBalance>,
     ) {
         val accounts = accountsWithBalance()
-        _accounts.value = accounts
+        this.accounts.value = accounts
 
-        _accountSuggestions.value =
+        accountSuggestions.value =
             preloadDataLogic.accountSuggestions(baseCurrency.code)
-        _state.value = OnboardingState.ACCOUNTS
+        state.value = OnboardingState.ACCOUNTS
     }
 
     private suspend fun routeToCategories() {
-        _categories.value =
+        categories.value =
             ioThread { categoryDao.findAll().map { it.toDomain() }.toImmutableList() }!!
-        _categorySuggestions.value = preloadDataLogic.categorySuggestions()
+        categorySuggestions.value = preloadDataLogic.categorySuggestions()
 
-        _state.value = OnboardingState.CATEGORIES
+        state.value = OnboardingState.CATEGORIES
     }
 
     private suspend fun completeOnboarding(
@@ -245,8 +245,8 @@ class OnboardingRouter(
     }
 
     private fun resetState() {
-        _state.value = OnboardingState.SPLASH
-        _opGoogleSignIn.value = null
+        state.value = OnboardingState.SPLASH
+        opGoogleSignIn.value = null
     }
 
     private fun navigateOutOfOnboarding() {
diff --git a/screen-onboarding/src/main/java/com/ivy/onboarding/viewmodel/OnboardingViewModel.kt b/screen-onboarding/src/main/java/com/ivy/onboarding/viewmodel/OnboardingViewModel.kt
index ab57d8de02..ed8c3c3cbc 100644
--- a/screen-onboarding/src/main/java/com/ivy/onboarding/viewmodel/OnboardingViewModel.kt
+++ b/screen-onboarding/src/main/java/com/ivy/onboarding/viewmodel/OnboardingViewModel.kt
@@ -1,30 +1,32 @@
 package com.ivy.onboarding.viewmodel
 
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
 import androidx.lifecycle.viewModelScope
 import com.ivy.base.legacy.Theme
-import com.ivy.legacy.datamodel.Account
-import com.ivy.legacy.datamodel.Category
-import com.ivy.legacy.datamodel.Settings
-import com.ivy.frp.test.TestIdlingResource
+import com.ivy.data.db.dao.read.AccountDao
+import com.ivy.data.db.dao.read.CategoryDao
+import com.ivy.data.db.dao.read.SettingsDao
+import com.ivy.data.db.dao.write.WriteSettingsDao
+import com.ivy.domain.ComposeViewModel
 import com.ivy.legacy.IvyWalletCtx
 import com.ivy.legacy.LogoutLogic
 import com.ivy.legacy.data.SharedPrefs
 import com.ivy.legacy.data.model.AccountBalance
+import com.ivy.legacy.datamodel.Account
+import com.ivy.legacy.datamodel.Category
+import com.ivy.legacy.datamodel.Settings
 import com.ivy.legacy.domain.action.exchange.SyncExchangeRatesAct
 import com.ivy.legacy.domain.deprecated.logic.AccountCreator
 import com.ivy.legacy.utils.OpResult
-import com.ivy.legacy.utils.asLiveData
 import com.ivy.legacy.utils.ioThread
 import com.ivy.legacy.utils.sendToCrashlytics
 import com.ivy.navigation.Navigation
 import com.ivy.navigation.OnboardingScreen
+import com.ivy.onboarding.OnboardingDetailState
+import com.ivy.onboarding.OnboardingEvent
 import com.ivy.onboarding.OnboardingState
-import com.ivy.data.db.dao.read.AccountDao
-import com.ivy.data.db.dao.read.CategoryDao
-import com.ivy.data.db.dao.read.SettingsDao
-import com.ivy.data.db.dao.write.WriteSettingsDao
 import com.ivy.wallet.domain.action.account.AccountsAct
 import com.ivy.wallet.domain.action.category.CategoriesAct
 import com.ivy.wallet.domain.data.IvyCurrency
@@ -62,36 +64,37 @@ class OnboardingViewModel @Inject constructor(
     transactionReminderLogic: TransactionReminderLogic,
     preloadDataLogic: PreloadDataLogic,
     logoutLogic: LogoutLogic,
-) : ViewModel() {
-
-    private val _state = MutableLiveData(OnboardingState.SPLASH)
-    val state = _state.asLiveData()
-
-    private val _currency = MutableLiveData<IvyCurrency>()
-    val currency = _currency.asLiveData()
-
-    private val _opGoogleSignIn = MutableLiveData<OpResult<Unit>?>()
-    val opGoogleSignIn = _opGoogleSignIn.asLiveData()
-
-    private val _accounts = MutableLiveData<ImmutableList<AccountBalance>>()
-    val accounts = _accounts.asLiveData()
-
-    private val _accountSuggestions = MutableLiveData<ImmutableList<CreateAccountData>>()
-    val accountSuggestions = _accountSuggestions.asLiveData()
-
-    private val _categories = MutableLiveData<ImmutableList<Category>>()
-    val categories = _categories.asLiveData()
-
-    private val _categorySuggestions = MutableLiveData<ImmutableList<CreateCategoryData>>()
-    val categorySuggestions = _categorySuggestions.asLiveData()
+) : ComposeViewModel<OnboardingDetailState, OnboardingEvent>() {
+
+    private val _state = mutableStateOf(OnboardingState.SPLASH)
+    val state: State<OnboardingState> = _state
+
+    private val _currency = mutableStateOf(IvyCurrency.getDefault())
+    private val _opGoogleSignIn = mutableStateOf<OpResult<Unit>?>(null)
+    private val _accounts = mutableStateOf(listOf<AccountBalance>().toImmutableList())
+    private val _accountSuggestions = mutableStateOf(listOf<CreateAccountData>().toImmutableList())
+    private val _categories = mutableStateOf(listOf<Category>().toImmutableList())
+    private val _categorySuggestions =
+        mutableStateOf(listOf<CreateCategoryData>().toImmutableList())
+
+    @Composable
+    override fun uiState(): OnboardingDetailState {
+        return OnboardingDetailState(
+            currency = _currency.value,
+            accounts = _accounts.value,
+            accountSuggestions = _accountSuggestions.value,
+            categories = _categories.value,
+            categorySuggestions = _categorySuggestions.value
+        )
+    }
 
     private val router = OnboardingRouter(
-        _state = _state,
-        _opGoogleSignIn = _opGoogleSignIn,
-        _accounts = _accounts,
-        _accountSuggestions = _accountSuggestions,
-        _categories = _categories,
-        _categorySuggestions = _categorySuggestions,
+        state = _state,
+        opGoogleSignIn = _opGoogleSignIn,
+        accounts = _accounts,
+        accountSuggestions = _accountSuggestions,
+        categories = _categories,
+        categorySuggestions = _categorySuggestions,
 
         nav = nav,
         accountDao = accountDao,
@@ -105,8 +108,6 @@ class OnboardingViewModel @Inject constructor(
 
     fun start(screen: OnboardingScreen, isSystemDarkMode: Boolean) {
         viewModelScope.launch {
-            TestIdlingResource.increment()
-
             initiateSettings(isSystemDarkMode)
 
             router.initBackHandling(
@@ -118,8 +119,6 @@ class OnboardingViewModel @Inject constructor(
             )
 
             router.splashNext()
-
-            TestIdlingResource.decrement()
         }
     }
 
@@ -128,8 +127,6 @@ class OnboardingViewModel @Inject constructor(
         _currency.value = defaultCurrency
 
         ioThread {
-            TestIdlingResource.increment()
-
             if (settingsDao.findAll().isEmpty()) {
                 settingsWriter.save(
                     Settings(
@@ -140,24 +137,39 @@ class OnboardingViewModel @Inject constructor(
                     ).toEntity()
                 )
             }
+        }
+    }
 
-            TestIdlingResource.decrement()
+    override fun onEvent(event: OnboardingEvent) {
+        viewModelScope.launch {
+            when (event) {
+                is OnboardingEvent.CreateAccount -> createAccount(event.data)
+                is OnboardingEvent.CreateCategory -> createCategory(event.data)
+                is OnboardingEvent.EditAccount -> editAccount(event.account, event.newBalance)
+                is OnboardingEvent.EditCategory -> editCategory(event.updatedCategory)
+                is OnboardingEvent.ImportFinished -> importFinished(event.success)
+                OnboardingEvent.ImportSkip -> importSkip()
+                OnboardingEvent.LoginOfflineAccount -> loginOfflineAccount()
+                OnboardingEvent.LoginWithGoogle -> loginWithGoogle()
+                OnboardingEvent.OnAddAccountsDone -> onAddAccountsDone()
+                OnboardingEvent.OnAddAccountsSkip -> onAddAccountsSkip()
+                OnboardingEvent.OnAddCategoriesDone -> onAddCategoriesDone()
+                OnboardingEvent.OnAddCategoriesSkip -> onAddCategoriesSkip()
+                is OnboardingEvent.SetBaseCurrency -> setBaseCurrency(event.baseCurrency)
+                OnboardingEvent.StartFresh -> startFresh()
+                OnboardingEvent.StartImport -> startImport()
+            }
         }
     }
 
     // Step 1 ---------------------------------------------------------------------------------------
-    fun loginWithGoogle() {
+    private suspend fun loginWithGoogle() {
         ivyContext.googleSignIn { idToken ->
             if (idToken != null) {
                 _opGoogleSignIn.value = OpResult.loading()
                 viewModelScope.launch {
-                    TestIdlingResource.increment()
-
                     try {
-                        loginWithGoogleOnServer(idToken)
-
                         router.googleLoginNext()
-
                         _opGoogleSignIn.value = null // reset login with Google operation state
                     } catch (e: Exception) {
                         e.sendToCrashlytics("GOOGLE_SIGN_IN ERROR: generic exception when logging with GOOGLE")
@@ -165,8 +177,6 @@ class OnboardingViewModel @Inject constructor(
                         Timber.e("Login with Google failed on Ivy server - ${e.message}")
                         _opGoogleSignIn.value = OpResult.failure(e)
                     }
-
-                    TestIdlingResource.decrement()
                 }
             } else {
                 sendToCrashlytics("GOOGLE_SIGN_IN ERROR: idToken is null!!")
@@ -176,23 +186,13 @@ class OnboardingViewModel @Inject constructor(
         }
     }
 
-    private suspend fun loginWithGoogleOnServer(idToken: String) {
-        TestIdlingResource.increment()
-
-        TestIdlingResource.decrement()
-    }
-
-    fun loginOfflineAccount() {
-        viewModelScope.launch {
-            TestIdlingResource.increment()
-            router.offlineAccountNext()
-            TestIdlingResource.decrement()
-        }
+    private suspend fun loginOfflineAccount() {
+        router.offlineAccountNext()
     }
     // Step 1 ---------------------------------------------------------------------------------------
 
     // Step 2 ---------------------------------------------------------------------------------------
-    fun startImport() {
+    private fun startImport() {
         router.startImport()
     }
 
@@ -204,63 +204,40 @@ class OnboardingViewModel @Inject constructor(
         router.importFinished(success)
     }
 
-    fun startFresh() {
+    private fun startFresh() {
         router.startFresh()
     }
     // Step 2 ---------------------------------------------------------------------------------------
 
-    fun setBaseCurrency(baseCurrency: IvyCurrency) {
-        viewModelScope.launch {
-            TestIdlingResource.increment()
-
-            updateBaseCurrency(baseCurrency)
-
-            router.setBaseCurrencyNext(
-                baseCurrency = baseCurrency,
-                accountsWithBalance = { accountsWithBalance() }
-            )
-
-            TestIdlingResource.decrement()
-        }
+    private suspend fun setBaseCurrency(baseCurrency: IvyCurrency) {
+        updateBaseCurrency(baseCurrency)
+        router.setBaseCurrencyNext(
+            baseCurrency = baseCurrency,
+            accountsWithBalance = { accountsWithBalance() }
+        )
     }
 
     private suspend fun updateBaseCurrency(baseCurrency: IvyCurrency) {
         ioThread {
-            TestIdlingResource.increment()
-
             settingsWriter.save(
                 settingsDao.findFirst().copy(
                     currency = baseCurrency.code
                 )
             )
-
-            TestIdlingResource.decrement()
         }
         _currency.value = baseCurrency
     }
 
     // --------------------- Accounts ---------------------------------------------------------------
-    fun editAccount(account: Account, newBalance: Double) {
-        viewModelScope.launch {
-            TestIdlingResource.increment()
-
-            accountCreator.editAccount(account, newBalance) {
-                _accounts.value = accountsWithBalance()
-            }
-
-            TestIdlingResource.decrement()
+    private suspend fun editAccount(account: Account, newBalance: Double) {
+        accountCreator.editAccount(account, newBalance) {
+            _accounts.value = accountsWithBalance()
         }
     }
 
-    fun createAccount(data: CreateAccountData) {
-        viewModelScope.launch {
-            TestIdlingResource.increment()
-
-            accountCreator.createAccount(data) {
-                _accounts.value = accountsWithBalance()
-            }
-
-            TestIdlingResource.decrement()
+    private suspend fun createAccount(data: CreateAccountData) {
+        accountCreator.createAccount(data) {
+            _accounts.value = accountsWithBalance()
         }
     }
 
@@ -274,70 +251,34 @@ class OnboardingViewModel @Inject constructor(
             }.toImmutableList()
     }
 
-    fun onAddAccountsDone() {
-        viewModelScope.launch {
-            TestIdlingResource.increment()
-
-            router.accountsNext()
-
-            TestIdlingResource.decrement()
-        }
+    private suspend fun onAddAccountsDone() {
+        router.accountsNext()
     }
 
-    fun onAddAccountsSkip() {
-        viewModelScope.launch {
-            TestIdlingResource.increment()
-
-            router.accountsSkip()
-
-            TestIdlingResource.decrement()
-        }
+    private suspend fun onAddAccountsSkip() {
+        router.accountsSkip()
     }
     // --------------------- Accounts ---------------------------------------------------------------
 
     // ---------------------------- Categories ------------------------------------------------------
-    fun editCategory(updatedCategory: Category) {
-        viewModelScope.launch {
-            TestIdlingResource.increment()
-
-            categoryCreator.editCategory(updatedCategory) {
-                _categories.value = categoriesAct(Unit)!!
-            }
-
-            TestIdlingResource.decrement()
+    private suspend fun editCategory(updatedCategory: Category) {
+        categoryCreator.editCategory(updatedCategory) {
+            _categories.value = categoriesAct(Unit)!!
         }
     }
 
-    fun createCategory(data: CreateCategoryData) {
-        viewModelScope.launch {
-            TestIdlingResource.increment()
-
-            categoryCreator.createCategory(data) {
-                _categories.value = categoriesAct(Unit)!!
-            }
-
-            TestIdlingResource.decrement()
+    private suspend fun createCategory(data: CreateCategoryData) {
+        categoryCreator.createCategory(data) {
+            _categories.value = categoriesAct(Unit)!!
         }
     }
 
-    fun onAddCategoriesDone() {
-        viewModelScope.launch {
-            TestIdlingResource.increment()
-
-            router.categoriesNext(baseCurrency = currency.value)
-
-            TestIdlingResource.decrement()
-        }
+    private suspend fun onAddCategoriesDone() {
+        router.categoriesNext(baseCurrency = _currency.value)
     }
 
-    fun onAddCategoriesSkip() {
-        viewModelScope.launch {
-            TestIdlingResource.increment()
-
-            router.categoriesSkip(baseCurrency = currency.value)
-
-            TestIdlingResource.decrement()
-        }
+    private suspend fun onAddCategoriesSkip() {
+        router.categoriesSkip(baseCurrency = _currency.value)
     }
     // ---------------------------- Categories ------------------------------------------------------
 }