From b63f1469236a607b75d0fa9a3fe456743b34381f Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Sat, 6 Apr 2024 12:14:55 +0300 Subject: [PATCH] Disclaimer screen + bugfixes (#3098) * WIP: Disclaimer screen * Build UI components * WIP: Disclaimer screen * Implement the Disclaimer screen * Fix crash * Finish the Disclaimer screen * Make `stale` keep bugs * WIP: Fix memoization * Fix memoization issues * Bugfixes: memoization + Categories screen * Optimize memoization * Fix tags memoization * Get rid of legacy `EventBus` * Bump version to "4.6.2" (162) * Add "Report a bug" btn in Settings --- .github/workflows/stale.yml | 2 +- app/build.gradle.kts | 1 + app/src/main/java/com/ivy/IvyNavGraph.kt | 3 + .../main/java/com/ivy/wallet/RootViewModel.kt | 13 +- gradle/libs.versions.toml | 4 +- .../com/ivy/accounts/AccountsViewModel.kt | 23 +++- .../com/ivy/categories/CategoriesViewModel.kt | 12 +- screen/disclaimer/build.gradle.kts | 15 ++ .../com/ivy/disclaimer/DisclaimerScreen.kt | 83 +++++++++++ .../com/ivy/disclaimer/DisclaimerViewModel.kt | 92 +++++++++++++ .../com/ivy/disclaimer/DisclaimerViewState.kt | 18 +++ .../disclaimer/composables/AcceptTermsText.kt | 19 +++ .../ivy/disclaimer/composables/AgreeButton.kt | 22 +++ .../composables/AgreementCheckBox.kt | 29 ++++ .../composables/DisclaimerTopAppBar.kt | 20 +++ .../DisclaimerScreenPaparazziTest.kt | 44 ++++++ ...rScreenPaparazziTest_all checked[Dark].png | 3 + ...ScreenPaparazziTest_all checked[Light].png | 3 + ...ScreenPaparazziTest_none checked[Dark].png | 3 + ...creenPaparazziTest_none checked[Light].png | 3 + .../transaction/EditTransactionViewModel.kt | 10 +- .../java/com/ivy/loans/loan/LoanViewModel.kt | 6 +- .../loans/loandetails/LoanDetailsViewModel.kt | 12 +- .../main/java/com/ivy/main/MainViewModel.kt | 7 +- .../ivy/planned/edit/EditPlannedViewModel.kt | 24 ++-- .../java/com/ivy/settings/SettingsScreen.kt | 48 ++----- .../ivy/transactions/TransactionsViewModel.kt | 2 +- settings.gradle.kts | 1 + .../com/ivy/base/TestDispatchersProvider.kt | 9 +- .../main/java/com/ivy/base/di/BaseModule.kt | 27 ++++ .../backup/BackupDataUseCaseAndroidTest.kt | 4 +- .../main/java/com/ivy/data/DataObserver.kt | 44 ++++++ .../java/com/ivy/data/DataWriteEventBus.kt | 30 ---- .../src/main/java/com/ivy/data/ExactDsl.kt | 27 ---- .../java/com/ivy/data/InMemoryDataStore.kt | 59 -------- .../com/ivy/data/backup/BackupDataUseCase.kt | 5 + .../data/datasource/LocalLegalDataSource.kt | 26 ++++ .../ivy/data/di/RepositoryBindingsModule.kt | 5 + .../ivy/data/repository/AccountRepository.kt | 10 +- .../ivy/data/repository/CategoryRepository.kt | 10 +- .../ivy/data/repository/LegalRepository.kt | 6 + .../com/ivy/data/repository/TagsRepository.kt | 20 +-- .../data/repository/TransactionRepository.kt | 130 +++++++++--------- .../repository/fake/FakeAccountRepository.kt | 6 +- .../data/repository/fake/FakeTagRepository.kt | 7 +- .../repository/impl/AccountRepositoryImpl.kt | 56 ++++++-- .../repository/impl/CategoryRepositoryImpl.kt | 52 +++++-- .../repository/impl/LegalRepositoryImpl.kt | 22 +++ .../repository/impl/TagsRepositoryImpl.kt | 38 ++++- .../ivy/data/repository/mapper/TagMapper.kt | 20 +-- .../data/temp/migration/TempMigrationUtils.kt | 40 +++--- .../ivy/data/backup/BackupDataUseCaseTest.kt | 4 +- .../impl/AccountRepositoryImplTest.kt | 10 +- .../impl/CategoryRepositoryImplTest.kt | 10 +- .../ivy/domain/event/AccountUpdatedEvent.kt | 3 - .../java/com/ivy/domain/event/EventBus.kt | 35 ----- .../com/ivy/ui/component/OpenSourceCard.kt | 76 ++++++++++ .../ui/core/src/main/res/values/strings.xml | 3 +- .../com/ivy/ui/PaparazziScreenshotTest.kt | 33 +++++ .../component/OpenSourceCardPaparazziTest.kt | 28 ++++ ...eCardPaparazziTest_default state[Dark].png | 3 + ...CardPaparazziTest_default state[Light].png | 3 + .../main/java/com/ivy/navigation/Screens.kt | 2 + .../src/main/java/com/ivy/legacy/Constants.kt | 2 +- .../main/java/com/ivy/legacy/LogoutLogic.kt | 8 +- 65 files changed, 972 insertions(+), 423 deletions(-) create mode 100644 screen/disclaimer/build.gradle.kts create mode 100644 screen/disclaimer/src/main/java/com/ivy/disclaimer/DisclaimerScreen.kt create mode 100644 screen/disclaimer/src/main/java/com/ivy/disclaimer/DisclaimerViewModel.kt create mode 100644 screen/disclaimer/src/main/java/com/ivy/disclaimer/DisclaimerViewState.kt create mode 100644 screen/disclaimer/src/main/java/com/ivy/disclaimer/composables/AcceptTermsText.kt create mode 100644 screen/disclaimer/src/main/java/com/ivy/disclaimer/composables/AgreeButton.kt create mode 100644 screen/disclaimer/src/main/java/com/ivy/disclaimer/composables/AgreementCheckBox.kt create mode 100644 screen/disclaimer/src/main/java/com/ivy/disclaimer/composables/DisclaimerTopAppBar.kt create mode 100644 screen/disclaimer/src/test/java/com/ivy/disclaimer/DisclaimerScreenPaparazziTest.kt create mode 100644 screen/disclaimer/src/test/snapshots/images/com.ivy.disclaimer_DisclaimerScreenPaparazziTest_all checked[Dark].png create mode 100644 screen/disclaimer/src/test/snapshots/images/com.ivy.disclaimer_DisclaimerScreenPaparazziTest_all checked[Light].png create mode 100644 screen/disclaimer/src/test/snapshots/images/com.ivy.disclaimer_DisclaimerScreenPaparazziTest_none checked[Dark].png create mode 100644 screen/disclaimer/src/test/snapshots/images/com.ivy.disclaimer_DisclaimerScreenPaparazziTest_none checked[Light].png create mode 100644 shared/base/src/main/java/com/ivy/base/di/BaseModule.kt create mode 100644 shared/data/core/src/main/java/com/ivy/data/DataObserver.kt delete mode 100644 shared/data/core/src/main/java/com/ivy/data/DataWriteEventBus.kt delete mode 100644 shared/data/core/src/main/java/com/ivy/data/ExactDsl.kt delete mode 100644 shared/data/core/src/main/java/com/ivy/data/InMemoryDataStore.kt create mode 100644 shared/data/core/src/main/java/com/ivy/data/datasource/LocalLegalDataSource.kt create mode 100644 shared/data/core/src/main/java/com/ivy/data/repository/LegalRepository.kt create mode 100644 shared/data/core/src/main/java/com/ivy/data/repository/impl/LegalRepositoryImpl.kt delete mode 100644 shared/domain/src/main/java/com/ivy/domain/event/AccountUpdatedEvent.kt delete mode 100644 shared/domain/src/main/java/com/ivy/domain/event/EventBus.kt create mode 100644 shared/ui/core/src/main/java/com/ivy/ui/component/OpenSourceCard.kt create mode 100644 shared/ui/core/src/test/java/com/ivy/ui/PaparazziScreenshotTest.kt create mode 100644 shared/ui/core/src/test/java/com/ivy/ui/component/OpenSourceCardPaparazziTest.kt create mode 100644 shared/ui/core/src/test/snapshots/images/com.ivy.ui.component_OpenSourceCardPaparazziTest_default state[Dark].png create mode 100644 shared/ui/core/src/test/snapshots/images/com.ivy.ui.component_OpenSourceCardPaparazziTest_default state[Light].png diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 0fccee367b..cfd658823d 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -20,5 +20,5 @@ jobs: days-before-issue-close: 7 days-before-pr-stale: 2 days-before-pr-close: 1 - exempt-issue-labels: keep,P0 + exempt-issue-labels: keep,P0,bug exempt-pr-labels: keep,P0 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 58942f2066..b0365c93d1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -130,6 +130,7 @@ dependencies { implementation(projects.screen.budgets) implementation(projects.screen.categories) implementation(projects.screen.contributors) + implementation(projects.screen.disclaimer) implementation(projects.screen.editTransaction) implementation(projects.screen.exchangeRates) implementation(projects.screen.features) diff --git a/app/src/main/java/com/ivy/IvyNavGraph.kt b/app/src/main/java/com/ivy/IvyNavGraph.kt index 904c26fd7a..c28e9b988a 100644 --- a/app/src/main/java/com/ivy/IvyNavGraph.kt +++ b/app/src/main/java/com/ivy/IvyNavGraph.kt @@ -9,6 +9,7 @@ import com.ivy.balance.BalanceScreen import com.ivy.budgets.BudgetScreen import com.ivy.categories.CategoriesScreen import com.ivy.contributors.ContributorsScreenImpl +import com.ivy.disclaimer.DisclaimerScreenImpl import com.ivy.exchangerates.ExchangeRatesScreen import com.ivy.features.FeaturesScreenImpl import com.ivy.importdata.csv.CSVScreen @@ -22,6 +23,7 @@ import com.ivy.navigation.BudgetScreen import com.ivy.navigation.CSVScreen import com.ivy.navigation.CategoriesScreen import com.ivy.navigation.ContributorsScreen +import com.ivy.navigation.DisclaimerScreen import com.ivy.navigation.EditPlannedScreen import com.ivy.navigation.EditTransactionScreen import com.ivy.navigation.ExchangeRatesScreen @@ -81,5 +83,6 @@ fun BoxWithConstraintsScope.IvyNavGraph(screen: Screen?) { AttributionsScreen -> AttributionsScreenImpl() ContributorsScreen -> ContributorsScreenImpl() ReleasesScreen -> ReleasesScreenImpl() + DisclaimerScreen -> DisclaimerScreenImpl() } } diff --git a/app/src/main/java/com/ivy/wallet/RootViewModel.kt b/app/src/main/java/com/ivy/wallet/RootViewModel.kt index 04e9a82533..4eba48176c 100644 --- a/app/src/main/java/com/ivy/wallet/RootViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/RootViewModel.kt @@ -8,12 +8,13 @@ import com.ivy.base.legacy.SharedPrefs import com.ivy.base.legacy.Theme import com.ivy.base.legacy.stringRes import com.ivy.base.model.TransactionType -import com.ivy.data.InMemoryDataStore import com.ivy.data.db.dao.read.SettingsDao +import com.ivy.data.repository.LegalRepository import com.ivy.frp.test.TestIdlingResource import com.ivy.legacy.IvyWalletCtx import com.ivy.legacy.utils.ioThread import com.ivy.legacy.utils.readOnly +import com.ivy.navigation.DisclaimerScreen import com.ivy.navigation.EditTransactionScreen import com.ivy.navigation.MainScreen import com.ivy.navigation.Navigation @@ -40,7 +41,7 @@ class RootViewModel @Inject constructor( private val sharedPrefs: SharedPrefs, private val transactionReminderLogic: TransactionReminderLogic, private val migrationsManager: MigrationsManager, - private val inMemoryDataStore: InMemoryDataStore, + private val legalRepo: LegalRepository, ) : ViewModel() { companion object { @@ -54,11 +55,6 @@ class RootViewModel @Inject constructor( private val _appLocked = MutableStateFlow(null) val appLocked = _appLocked.readOnly() - init { - // TODO: Consider delaying this to improve cold start - inMemoryDataStore.init(viewModelScope) - } - fun start(systemDarkMode: Boolean, intent: Intent) { viewModelScope.launch { TestIdlingResource.increment() @@ -87,6 +83,9 @@ class RootViewModel @Inject constructor( } else { nav.navigateTo(OnboardingScreen) } + if (!legalRepo.isDisclaimerAccepted()) { + nav.navigateTo(DisclaimerScreen) + } } TestIdlingResource.decrement() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index daa77aff70..b9921a2c36 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,8 +19,8 @@ paparazzi = "1.3.3" # Android min-sdk = "28" compile-sdk = "34" -version-name = "4.6.1" -version-code = "161" +version-name = "4.6.2" +version-code = "162" jvm-target = "17" diff --git a/screen/accounts/src/main/java/com/ivy/accounts/AccountsViewModel.kt b/screen/accounts/src/main/java/com/ivy/accounts/AccountsViewModel.kt index 3c33cff127..d944e421e0 100644 --- a/screen/accounts/src/main/java/com/ivy/accounts/AccountsViewModel.kt +++ b/screen/accounts/src/main/java/com/ivy/accounts/AccountsViewModel.kt @@ -7,16 +7,16 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope -import com.ivy.ui.ComposeViewModel -import com.ivy.domain.event.AccountUpdatedEvent -import com.ivy.domain.event.EventBus import com.ivy.base.legacy.SharedPrefs +import com.ivy.data.DataObserver +import com.ivy.data.DataWriteEvent import com.ivy.data.repository.AccountRepository import com.ivy.legacy.IvyWalletCtx import com.ivy.legacy.data.model.AccountData import com.ivy.legacy.data.model.toCloseTimeRange import com.ivy.legacy.utils.format import com.ivy.legacy.utils.ioThread +import com.ivy.ui.ComposeViewModel import com.ivy.ui.R import com.ivy.wallet.domain.action.settings.BaseCurrencyAct import com.ivy.wallet.domain.action.viewmodel.account.AccountDataAct @@ -26,6 +26,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import javax.inject.Inject @@ -40,8 +41,8 @@ class AccountsViewModel @Inject constructor( private val calcWalletBalanceAct: CalcWalletBalanceAct, private val baseCurrencyAct: BaseCurrencyAct, private val accountDataAct: AccountDataAct, - private val eventBus: EventBus, - private val accountRepository: AccountRepository + private val accountRepository: AccountRepository, + private val dataObserver: DataObserver, ) : ComposeViewModel() { private val baseCurrency = mutableStateOf("") private val accountsData = mutableStateOf(listOf()) @@ -53,8 +54,16 @@ class AccountsViewModel @Inject constructor( init { viewModelScope.launch { - eventBus.subscribe(AccountUpdatedEvent) { - onStart() + dataObserver.writeEvents.collectLatest { event -> + when (event) { + is DataWriteEvent.AccountChange -> { + onStart() + } + + else -> { + // do nothing + } + } } } } diff --git a/screen/categories/src/main/java/com/ivy/categories/CategoriesViewModel.kt b/screen/categories/src/main/java/com/ivy/categories/CategoriesViewModel.kt index bb8e86274f..d1e26ab5a0 100644 --- a/screen/categories/src/main/java/com/ivy/categories/CategoriesViewModel.kt +++ b/screen/categories/src/main/java/com/ivy/categories/CategoriesViewModel.kt @@ -11,7 +11,9 @@ import com.ivy.ui.ComposeViewModel import com.ivy.data.repository.CategoryRepository import com.ivy.frp.action.thenMap import com.ivy.frp.thenInvokeAfter +import com.ivy.legacy.data.model.TimePeriod import com.ivy.legacy.datamodel.Account +import com.ivy.legacy.utils.ioThread import com.ivy.wallet.domain.action.account.AccountsAct import com.ivy.wallet.domain.action.category.LegacyCategoryIncomeWithAccountFiltersAct import com.ivy.wallet.domain.action.settings.BaseCurrencyAct @@ -109,8 +111,8 @@ class CategoriesViewModel @Inject constructor( } private suspend fun initialise() { - com.ivy.legacy.utils.ioThread { - val range = com.ivy.legacy.data.model.TimePeriod.currentMonth( + ioThread { + val range = TimePeriod.currentMonth( startDayOfMonth = ivyContext.startDayOfMonth ).toRange(ivyContext.startDayOfMonth) // this must be monthly @@ -169,14 +171,14 @@ class CategoriesViewModel @Inject constructor( val sortedList = sortList(newOrder, sortOrder).toImmutableList() if (sortOrder == SortOrder.DEFAULT) { - com.ivy.legacy.utils.ioThread { + ioThread { sortedList.forEachIndexed { index, categoryData -> - categoryRepository.save(categoryData.category) + categoryRepository.save(categoryData.category.copy(orderNum = index.toDouble())) } } } - com.ivy.legacy.utils.ioThread { + ioThread { sharedPrefs.putInt(SharedPrefs.CATEGORY_SORT_ORDER, sortOrder.orderNum) } diff --git a/screen/disclaimer/build.gradle.kts b/screen/disclaimer/build.gradle.kts new file mode 100644 index 0000000000..15e7f05889 --- /dev/null +++ b/screen/disclaimer/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("ivy.feature") +} + +android { + namespace = "com.ivy.disclaimer" +} + +dependencies { + implementation(projects.shared.data.core) + implementation(projects.shared.ui.core) + implementation(projects.shared.ui.navigation) + + testImplementation(projects.shared.ui.testing) +} diff --git a/screen/disclaimer/src/main/java/com/ivy/disclaimer/DisclaimerScreen.kt b/screen/disclaimer/src/main/java/com/ivy/disclaimer/DisclaimerScreen.kt new file mode 100644 index 0000000000..4e2cf9babb --- /dev/null +++ b/screen/disclaimer/src/main/java/com/ivy/disclaimer/DisclaimerScreen.kt @@ -0,0 +1,83 @@ +package com.ivy.disclaimer + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.ivy.disclaimer.composables.AcceptTermsText +import com.ivy.disclaimer.composables.AgreeButton +import com.ivy.disclaimer.composables.AgreementCheckBox +import com.ivy.disclaimer.composables.DisclaimerTopAppBar +import com.ivy.navigation.screenScopedViewModel +import com.ivy.ui.component.OpenSourceCard + +@Composable +fun DisclaimerScreenImpl() { + val viewModel: DisclaimerViewModel = screenScopedViewModel() + val viewState = viewModel.uiState() + DisclaimerScreenUi(viewState = viewState, onEvent = viewModel::onEvent) +} + +@Composable +fun DisclaimerScreenUi( + viewState: DisclaimerViewState, + onEvent: (DisclaimerViewEvent) -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + DisclaimerTopAppBar() + }, + content = { innerPadding -> + Content( + modifier = Modifier.padding(innerPadding), + viewState = viewState, + onEvent = onEvent, + ) + } + ) +} + +@Composable +private fun Content( + viewState: DisclaimerViewState, + onEvent: (DisclaimerViewEvent) -> Unit, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier.padding(horizontal = 16.dp) + ) { + item { + OpenSourceCard() + } + item { + Spacer(modifier = Modifier.height(12.dp)) + AcceptTermsText() + } + itemsIndexed(items = viewState.checkboxes) { index, item -> + Spacer(modifier = Modifier.height(8.dp)) + AgreementCheckBox( + viewState = item, + onClick = { + onEvent(DisclaimerViewEvent.OnCheckboxClick(index)) + } + ) + } + item { + Spacer(modifier = Modifier.height(12.dp)) + AgreeButton( + enabled = viewState.agreeButtonEnabled, + ) { onEvent(DisclaimerViewEvent.OnAgreeClick) } + } + item { + // To ensure proper scrolling + Spacer(modifier = Modifier.height(48.dp)) + } + } +} \ No newline at end of file diff --git a/screen/disclaimer/src/main/java/com/ivy/disclaimer/DisclaimerViewModel.kt b/screen/disclaimer/src/main/java/com/ivy/disclaimer/DisclaimerViewModel.kt new file mode 100644 index 0000000000..b7bb57f148 --- /dev/null +++ b/screen/disclaimer/src/main/java/com/ivy/disclaimer/DisclaimerViewModel.kt @@ -0,0 +1,92 @@ +package com.ivy.disclaimer + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.viewModelScope +import com.ivy.data.repository.LegalRepository +import com.ivy.navigation.Navigation +import com.ivy.ui.ComposeViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class DisclaimerViewModel @Inject constructor( + private val navigation: Navigation, + private val legalRepo: LegalRepository, +) : ComposeViewModel() { + + private var checkboxes by mutableStateOf(LegalCheckboxes) + + @Composable + override fun uiState(): DisclaimerViewState { + return DisclaimerViewState( + checkboxes = checkboxes, + agreeButtonEnabled = checkboxes.all(CheckboxViewState::checked), + ) + } + + override fun onEvent(event: DisclaimerViewEvent) { + when (event) { + DisclaimerViewEvent.OnAgreeClick -> handleAgreeClick() + is DisclaimerViewEvent.OnCheckboxClick -> handleCheckboxClick(event) + } + } + + private fun handleAgreeClick() { + viewModelScope.launch { + legalRepo.setDisclaimerAccepted(accepted = true) + navigation.back() + } + } + + private fun handleCheckboxClick(event: DisclaimerViewEvent.OnCheckboxClick) { + checkboxes = checkboxes.mapIndexed { index, item -> + if (index == event.index) { + item.copy( + checked = !item.checked + ) + } else { + item + } + }.toImmutableList() + } + + companion object { + // LEGAL text - do NOT extract or translate + val LegalCheckboxes = listOf( + CheckboxViewState( + text = "I recognize this app is open-source and provided 'as-is' " + + "with no warranties, explicit or implied. " + + "I fully accept all risks of errors, defects, or failures, " + + "using the app solely at my own risk.", + checked = false, + ), + CheckboxViewState( + text = "I understand there is no warranty for the accuracy, " + + "reliability, or completeness of my data. " + + "Manual data backup is my responsibility, and I agree to not hold " + + "the app liable for any data loss.", + checked = false, + ), + CheckboxViewState( + text = "I hereby release the app developers, contributors, " + + "and distributing company from any liability for " + + "claims, damages, legal fees, or losses, including those resulting " + + "from security breaches or data inaccuracies.", + checked = false, + ), + CheckboxViewState( + text = "I am aware and accept that the app may display misleading information " + + "or contain inaccuracies. " + + "I assume full responsibility for verifying the integrity " + + "of financial data and calculations before making " + + "any decisions based on app data.", + checked = false, + ), + ).toImmutableList() + } +} \ No newline at end of file diff --git a/screen/disclaimer/src/main/java/com/ivy/disclaimer/DisclaimerViewState.kt b/screen/disclaimer/src/main/java/com/ivy/disclaimer/DisclaimerViewState.kt new file mode 100644 index 0000000000..63a7fd5038 --- /dev/null +++ b/screen/disclaimer/src/main/java/com/ivy/disclaimer/DisclaimerViewState.kt @@ -0,0 +1,18 @@ +package com.ivy.disclaimer + +import kotlinx.collections.immutable.ImmutableList + +data class DisclaimerViewState( + val checkboxes: ImmutableList, + val agreeButtonEnabled: Boolean, +) + +data class CheckboxViewState( + val text: String, + val checked: Boolean +) + +sealed interface DisclaimerViewEvent { + data class OnCheckboxClick(val index: Int) : DisclaimerViewEvent + data object OnAgreeClick : DisclaimerViewEvent +} \ No newline at end of file diff --git a/screen/disclaimer/src/main/java/com/ivy/disclaimer/composables/AcceptTermsText.kt b/screen/disclaimer/src/main/java/com/ivy/disclaimer/composables/AcceptTermsText.kt new file mode 100644 index 0000000000..d982464990 --- /dev/null +++ b/screen/disclaimer/src/main/java/com/ivy/disclaimer/composables/AcceptTermsText.kt @@ -0,0 +1,19 @@ +package com.ivy.disclaimer.composables + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight + +@Composable +fun AcceptTermsText( + modifier: Modifier = Modifier +) { + Text( + modifier = modifier, + text = "Please read and agree to the following terms before using the app:", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + ) +} \ No newline at end of file diff --git a/screen/disclaimer/src/main/java/com/ivy/disclaimer/composables/AgreeButton.kt b/screen/disclaimer/src/main/java/com/ivy/disclaimer/composables/AgreeButton.kt new file mode 100644 index 0000000000..ec356c092c --- /dev/null +++ b/screen/disclaimer/src/main/java/com/ivy/disclaimer/composables/AgreeButton.kt @@ -0,0 +1,22 @@ +package com.ivy.disclaimer.composables + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun AgreeButton( + enabled: Boolean, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Button( + modifier = modifier.fillMaxWidth(), + enabled = enabled, + onClick = onClick, + ) { + Text("I accept and agree") + } +} \ No newline at end of file diff --git a/screen/disclaimer/src/main/java/com/ivy/disclaimer/composables/AgreementCheckBox.kt b/screen/disclaimer/src/main/java/com/ivy/disclaimer/composables/AgreementCheckBox.kt new file mode 100644 index 0000000000..6d2cdbc654 --- /dev/null +++ b/screen/disclaimer/src/main/java/com/ivy/disclaimer/composables/AgreementCheckBox.kt @@ -0,0 +1,29 @@ +package com.ivy.disclaimer.composables + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.ivy.disclaimer.CheckboxViewState + +@Composable +fun AgreementCheckBox( + viewState: CheckboxViewState, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Row( + modifier = modifier.clickable(onClick = onClick), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox(checked = viewState.checked, onCheckedChange = { onClick() }) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = viewState.text) + } +} \ No newline at end of file diff --git a/screen/disclaimer/src/main/java/com/ivy/disclaimer/composables/DisclaimerTopAppBar.kt b/screen/disclaimer/src/main/java/com/ivy/disclaimer/composables/DisclaimerTopAppBar.kt new file mode 100644 index 0000000000..bc880d67b1 --- /dev/null +++ b/screen/disclaimer/src/main/java/com/ivy/disclaimer/composables/DisclaimerTopAppBar.kt @@ -0,0 +1,20 @@ +package com.ivy.disclaimer.composables + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DisclaimerTopAppBar( + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier, + title = { + Text(text = "Important User Agreement") + } + ) +} \ No newline at end of file diff --git a/screen/disclaimer/src/test/java/com/ivy/disclaimer/DisclaimerScreenPaparazziTest.kt b/screen/disclaimer/src/test/java/com/ivy/disclaimer/DisclaimerScreenPaparazziTest.kt new file mode 100644 index 0000000000..18f8186d9b --- /dev/null +++ b/screen/disclaimer/src/test/java/com/ivy/disclaimer/DisclaimerScreenPaparazziTest.kt @@ -0,0 +1,44 @@ +package com.ivy.disclaimer + +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import com.ivy.ui.testing.PaparazziScreenshotTest +import com.ivy.ui.testing.PaparazziTheme +import kotlinx.collections.immutable.toImmutableList +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(TestParameterInjector::class) +class DisclaimerScreenPaparazziTest( + @TestParameter + private val theme: PaparazziTheme, +) : PaparazziScreenshotTest() { + + @Test + fun `none checked`() { + snapshot(theme) { + DisclaimerScreenUi( + viewState = DisclaimerViewState( + checkboxes = DisclaimerViewModel.LegalCheckboxes, + agreeButtonEnabled = false, + ), + onEvent = {} + ) + } + } + + @Test + fun `all checked`() { + snapshot(theme) { + DisclaimerScreenUi( + viewState = DisclaimerViewState( + checkboxes = DisclaimerViewModel.LegalCheckboxes.map { + it.copy(checked = true) + }.toImmutableList(), + agreeButtonEnabled = true, + ), + onEvent = {} + ) + } + } +} \ No newline at end of file diff --git a/screen/disclaimer/src/test/snapshots/images/com.ivy.disclaimer_DisclaimerScreenPaparazziTest_all checked[Dark].png b/screen/disclaimer/src/test/snapshots/images/com.ivy.disclaimer_DisclaimerScreenPaparazziTest_all checked[Dark].png new file mode 100644 index 0000000000..4052143c2b --- /dev/null +++ b/screen/disclaimer/src/test/snapshots/images/com.ivy.disclaimer_DisclaimerScreenPaparazziTest_all checked[Dark].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e389b7bad2a6743ed0303069e61c49cfe77026f1459098385f5d2b4e8d277317 +size 125741 diff --git a/screen/disclaimer/src/test/snapshots/images/com.ivy.disclaimer_DisclaimerScreenPaparazziTest_all checked[Light].png b/screen/disclaimer/src/test/snapshots/images/com.ivy.disclaimer_DisclaimerScreenPaparazziTest_all checked[Light].png new file mode 100644 index 0000000000..a41bda8364 --- /dev/null +++ b/screen/disclaimer/src/test/snapshots/images/com.ivy.disclaimer_DisclaimerScreenPaparazziTest_all checked[Light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12b925a1844a32520e4d5b4250d4f9a5f12463e42a536ba0dd10060ca538854a +size 124377 diff --git a/screen/disclaimer/src/test/snapshots/images/com.ivy.disclaimer_DisclaimerScreenPaparazziTest_none checked[Dark].png b/screen/disclaimer/src/test/snapshots/images/com.ivy.disclaimer_DisclaimerScreenPaparazziTest_none checked[Dark].png new file mode 100644 index 0000000000..1e5ac6f7f4 --- /dev/null +++ b/screen/disclaimer/src/test/snapshots/images/com.ivy.disclaimer_DisclaimerScreenPaparazziTest_none checked[Dark].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82c89d7a66fb8ca9aa1616326b31b4b274a7bb80ba083284cec06776f27173c0 +size 123750 diff --git a/screen/disclaimer/src/test/snapshots/images/com.ivy.disclaimer_DisclaimerScreenPaparazziTest_none checked[Light].png b/screen/disclaimer/src/test/snapshots/images/com.ivy.disclaimer_DisclaimerScreenPaparazziTest_none checked[Light].png new file mode 100644 index 0000000000..e1690dc318 --- /dev/null +++ b/screen/disclaimer/src/test/snapshots/images/com.ivy.disclaimer_DisclaimerScreenPaparazziTest_none checked[Light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac238d9b939ced07c3053430fb30073fd5a90b89ac67cc4990d23817c5df76e0 +size 122623 diff --git a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionViewModel.kt b/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionViewModel.kt index acbd783755..4df8a032fb 100644 --- a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionViewModel.kt +++ b/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionViewModel.kt @@ -8,17 +8,13 @@ import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope import com.ivy.base.Toaster +import com.ivy.base.legacy.SharedPrefs import com.ivy.base.legacy.Transaction import com.ivy.base.legacy.refreshWidget import com.ivy.base.model.TransactionType import com.ivy.data.db.dao.read.LoanDao import com.ivy.data.db.dao.read.SettingsDao import com.ivy.data.db.dao.write.WriteTransactionDao -import com.ivy.ui.ComposeViewModel -import com.ivy.domain.event.AccountUpdatedEvent -import com.ivy.domain.event.EventBus -import com.ivy.legacy.data.EditTransactionDisplayLoan -import com.ivy.base.legacy.SharedPrefs import com.ivy.data.model.Category import com.ivy.data.model.CategoryId import com.ivy.data.model.Tag @@ -27,6 +23,7 @@ import com.ivy.data.model.primitive.NotBlankTrimmedString import com.ivy.data.repository.CategoryRepository import com.ivy.data.repository.TagsRepository import com.ivy.data.repository.mapper.TagMapper +import com.ivy.legacy.data.EditTransactionDisplayLoan import com.ivy.legacy.datamodel.Account import com.ivy.legacy.datamodel.toEntity import com.ivy.legacy.domain.deprecated.logic.AccountCreator @@ -41,6 +38,7 @@ import com.ivy.legacy.utils.uiThread import com.ivy.navigation.EditTransactionScreen import com.ivy.navigation.MainScreen import com.ivy.navigation.Navigation +import com.ivy.ui.ComposeViewModel import com.ivy.ui.R import com.ivy.wallet.domain.action.account.AccountByIdAct import com.ivy.wallet.domain.action.account.AccountsAct @@ -98,7 +96,6 @@ class EditTransactionViewModel @Inject constructor( private val categoryRepository: CategoryRepository, private val trnByIdAct: TrnByIdAct, private val accountByIdAct: AccountByIdAct, - private val eventBus: EventBus, private val transactionWriter: WriteTransactionDao, private val tagsRepository: TagsRepository, private val tagMapper: TagMapper @@ -645,7 +642,6 @@ class EditTransactionViewModel @Inject constructor( private fun createAccount(data: CreateAccountData) { viewModelScope.launch { accountCreator.createAccount(data) { - eventBus.post(AccountUpdatedEvent) accounts.value = accountsAct(Unit) } } diff --git a/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt b/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt index 02f07afde5..4b5a4ae83f 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt @@ -11,9 +11,6 @@ import com.ivy.data.db.dao.read.LoanRecordDao import com.ivy.data.db.dao.read.SettingsDao import com.ivy.data.db.dao.write.WriteLoanDao import com.ivy.data.model.LoanType -import com.ivy.ui.ComposeViewModel -import com.ivy.domain.event.AccountUpdatedEvent -import com.ivy.domain.event.EventBus import com.ivy.frp.test.TestIdlingResource import com.ivy.legacy.datamodel.Account import com.ivy.legacy.datamodel.Loan @@ -22,6 +19,7 @@ import com.ivy.legacy.utils.format import com.ivy.legacy.utils.getDefaultFIATCurrency import com.ivy.legacy.utils.ioThread import com.ivy.loans.loan.data.DisplayLoan +import com.ivy.ui.ComposeViewModel import com.ivy.wallet.domain.action.account.AccountsAct import com.ivy.wallet.domain.action.loan.LoansAct import com.ivy.wallet.domain.deprecated.logic.LoanCreator @@ -49,7 +47,6 @@ class LoanViewModel @Inject constructor( private val loanTransactionsLogic: LoanTransactionsLogic, private val loansAct: LoansAct, private val accountsAct: AccountsAct, - private val eventBus: EventBus, private val loanWriter: WriteLoanDao, ) : ComposeViewModel() { @@ -278,7 +275,6 @@ class LoanViewModel @Inject constructor( TestIdlingResource.increment() accountCreator.createAccount(data) { - eventBus.post(AccountUpdatedEvent) accounts.value = accountsAct(Unit) } diff --git a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt index 57a5fe9716..933728d3c2 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt @@ -8,16 +8,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope import com.ivy.base.legacy.Transaction import com.ivy.base.model.LoanRecordType -import com.ivy.data.db.dao.read.AccountDao -import com.ivy.data.db.dao.read.LoanDao import com.ivy.data.db.dao.read.LoanRecordDao import com.ivy.data.db.dao.read.SettingsDao import com.ivy.data.db.dao.read.TransactionDao -import com.ivy.ui.ComposeViewModel -import com.ivy.domain.event.AccountUpdatedEvent -import com.ivy.domain.event.EventBus import com.ivy.frp.test.TestIdlingResource -import com.ivy.legacy.IvyWalletCtx import com.ivy.legacy.datamodel.Account import com.ivy.legacy.datamodel.Loan import com.ivy.legacy.datamodel.LoanRecord @@ -32,6 +26,7 @@ import com.ivy.loans.loandetails.events.LoanModalEvent import com.ivy.loans.loandetails.events.LoanRecordModalEvent import com.ivy.navigation.LoanDetailsScreen import com.ivy.navigation.Navigation +import com.ivy.ui.ComposeViewModel import com.ivy.wallet.domain.action.account.AccountsAct import com.ivy.wallet.domain.action.loan.LoanByIdAct import com.ivy.wallet.domain.deprecated.logic.LoanCreator @@ -53,20 +48,16 @@ import javax.inject.Inject @Stable @HiltViewModel class LoanDetailsViewModel @Inject constructor( - private val loanDao: LoanDao, private val loanRecordDao: LoanRecordDao, private val loanCreator: LoanCreator, private val loanRecordCreator: LoanRecordCreator, private val settingsDao: SettingsDao, - private val ivyContext: IvyWalletCtx, private val transactionDao: TransactionDao, - private val accountDao: AccountDao, private val accountCreator: AccountCreator, private val loanTransactionsLogic: LoanTransactionsLogic, private val nav: Navigation, private val accountsAct: AccountsAct, private val loanByIdAct: LoanByIdAct, - private val eventBus: EventBus, ) : ComposeViewModel() { private val baseCurrency = mutableStateOf("") @@ -462,7 +453,6 @@ class LoanDetailsViewModel @Inject constructor( TestIdlingResource.increment() accountCreator.createAccount(data) { - eventBus.post(AccountUpdatedEvent) accounts.value = accountsAct(Unit) } diff --git a/screen/main/src/main/java/com/ivy/main/MainViewModel.kt b/screen/main/src/main/java/com/ivy/main/MainViewModel.kt index 19ae1dd86d..0acdd60743 100644 --- a/screen/main/src/main/java/com/ivy/main/MainViewModel.kt +++ b/screen/main/src/main/java/com/ivy/main/MainViewModel.kt @@ -5,8 +5,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ivy.base.legacy.SharedPrefs import com.ivy.data.repository.CurrencyRepository -import com.ivy.domain.event.AccountUpdatedEvent -import com.ivy.domain.event.EventBus import com.ivy.domain.usecase.SyncExchangeRatesUseCase import com.ivy.frp.test.TestIdlingResource import com.ivy.legacy.IvyWalletCtx @@ -28,7 +26,6 @@ class MainViewModel @Inject constructor( private val syncExchangeRatesUseCase: SyncExchangeRatesUseCase, private val accountCreator: AccountCreator, private val sharedPrefs: SharedPrefs, - private val eventBus: EventBus, private val currencyRepository: CurrencyRepository, ) : ViewModel() { @@ -72,9 +69,7 @@ class MainViewModel @Inject constructor( viewModelScope.launch { TestIdlingResource.increment() - accountCreator.createAccount(data) { - eventBus.post(AccountUpdatedEvent) - } + accountCreator.createAccount(data) {} TestIdlingResource.decrement() } diff --git a/screen/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedViewModel.kt b/screen/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedViewModel.kt index d0182bbb79..7add3b5197 100644 --- a/screen/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedViewModel.kt +++ b/screen/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedViewModel.kt @@ -11,13 +11,10 @@ import com.ivy.data.db.dao.read.PlannedPaymentRuleDao import com.ivy.data.db.dao.read.SettingsDao import com.ivy.data.db.dao.write.WritePlannedPaymentRuleDao import com.ivy.data.db.dao.write.WriteTransactionDao -import com.ivy.data.model.IntervalType -import com.ivy.ui.ComposeViewModel import com.ivy.data.model.Category import com.ivy.data.model.CategoryId +import com.ivy.data.model.IntervalType import com.ivy.data.repository.CategoryRepository -import com.ivy.domain.event.AccountUpdatedEvent -import com.ivy.domain.event.EventBus import com.ivy.legacy.datamodel.Account import com.ivy.legacy.datamodel.PlannedPaymentRule import com.ivy.legacy.datamodel.temp.toDomain @@ -25,6 +22,7 @@ import com.ivy.legacy.domain.deprecated.logic.AccountCreator import com.ivy.legacy.utils.ioThread import com.ivy.navigation.EditPlannedScreen import com.ivy.navigation.Navigation +import com.ivy.ui.ComposeViewModel import com.ivy.wallet.domain.action.account.AccountsAct import com.ivy.wallet.domain.deprecated.logic.CategoryCreator import com.ivy.wallet.domain.deprecated.logic.PlannedPaymentsGenerator @@ -53,7 +51,6 @@ class EditPlannedViewModel @Inject constructor( private val categoryCreator: CategoryCreator, private val accountCreator: AccountCreator, private val accountsAct: AccountsAct, - private val eventBus: EventBus, private val plannedPaymentRuleWriter: WritePlannedPaymentRuleDao, private val transactionWriter: WriteTransactionDao, ) : ComposeViewModel() { @@ -222,8 +219,10 @@ class EditPlannedViewModel @Inject constructor( is EditPlannedScreenEvent.OnDelete -> delete() is EditPlannedScreenEvent.OnSetTransactionType -> updateTransactionType(event.newTransactionType) + is EditPlannedScreenEvent.OnDescriptionChanged -> updateDescription(event.newDescription) + is EditPlannedScreenEvent.OnCreateAccount -> createAccount(event.data) is EditPlannedScreenEvent.OnCreateCategory -> createCategory(event.data) is EditPlannedScreenEvent.OnAccountChanged -> updateAccount(event.newAccount) @@ -231,22 +230,30 @@ class EditPlannedViewModel @Inject constructor( is EditPlannedScreenEvent.OnTitleChanged -> updateTitle(event.newTitle) is EditPlannedScreenEvent.OnRuleChanged -> updateRule(event.startDate, event.oneTime, event.intervalN, event.intervalType) + is EditPlannedScreenEvent.OnCategoryChanged -> updateCategory(event.newCategory) is EditPlannedScreenEvent.OnEditCategory -> editCategory(event.updatedCategory) is EditPlannedScreenEvent.OnCategoryModalVisible -> categoryModalVisible.value = event.visible + is EditPlannedScreenEvent.OnCategoryModalDataChanged -> categoryModalData.value = event.categoryModalData + is EditPlannedScreenEvent.OnAccountModalDataChanged -> accountModalData.value = event.accountModalData + is EditPlannedScreenEvent.OnDescriptionModalVisible -> descriptionModalVisible.value = event.visible + is EditPlannedScreenEvent.OnTransactionTypeModalVisible -> transactionTypeModalVisible.value = event.visible + is EditPlannedScreenEvent.OnAmountModalVisible -> amountModalVisible.value = event.visible + is EditPlannedScreenEvent.OnDeleteTransactionModalVisible -> deleteTransactionModalVisible.value = event.visible + is EditPlannedScreenEvent.OnRecurringRuleModalDataChanged -> recurringRuleModalData.value = event.recurringRuleModalData } @@ -449,9 +456,9 @@ class EditPlannedViewModel @Inject constructor( private fun validateRecurring(): Boolean { return startDate.value != null && - intervalN.value != null && - intervalN.value!! > 0 && - intervalType.value != null + intervalN.value != null && + intervalN.value!! > 0 && + intervalType.value != null } private fun delete() { @@ -490,7 +497,6 @@ class EditPlannedViewModel @Inject constructor( private fun createAccount(data: CreateAccountData) { viewModelScope.launch { accountCreator.createAccount(data) { - eventBus.post(AccountUpdatedEvent) accounts.value = accountsAct(Unit) } } diff --git a/screen/settings/src/main/java/com/ivy/settings/SettingsScreen.kt b/screen/settings/src/main/java/com/ivy/settings/SettingsScreen.kt index 99c17d9eb1..d5194c058d 100644 --- a/screen/settings/src/main/java/com/ivy/settings/SettingsScreen.kt +++ b/screen/settings/src/main/java/com/ivy/settings/SettingsScreen.kt @@ -42,12 +42,11 @@ import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style import com.ivy.design.l1_buildingBlocks.IconScale import com.ivy.design.l1_buildingBlocks.IvyIconScaled +import com.ivy.design.utils.thenIf import com.ivy.legacy.Constants import com.ivy.legacy.IvyWalletPreview import com.ivy.legacy.rootScreen -import com.ivy.legacy.utils.OpResult import com.ivy.legacy.utils.drawColoredShadow -import com.ivy.design.utils.thenIf import com.ivy.navigation.AttributionsScreen import com.ivy.navigation.ContributorsScreen import com.ivy.navigation.ExchangeRatesScreen @@ -63,15 +62,12 @@ import com.ivy.wallet.ui.theme.Gradient import com.ivy.wallet.ui.theme.GradientGreen import com.ivy.wallet.ui.theme.GradientIvy import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.ui.theme.Green import com.ivy.wallet.ui.theme.MediumBlack -import com.ivy.wallet.ui.theme.Orange import com.ivy.wallet.ui.theme.Red import com.ivy.wallet.ui.theme.Red3 import com.ivy.wallet.ui.theme.White import com.ivy.wallet.ui.theme.components.IvySwitch import com.ivy.wallet.ui.theme.components.IvyToolbar -import com.ivy.wallet.ui.theme.findContrastTextColor import com.ivy.wallet.ui.theme.modal.ChooseStartDateOfMonthModal import com.ivy.wallet.ui.theme.modal.CurrencyModal import com.ivy.wallet.ui.theme.modal.DeleteModal @@ -431,13 +427,13 @@ private fun BoxWithConstraintsScope.UI( Spacer(Modifier.height(12.dp)) - Roadmap() + ReportBug() Spacer(Modifier.height(12.dp)) val rootActivity = rootScreen() RequestFeature { - rootActivity.openUrlInBrowser(Constants.URL_IVY_TELEGRAM_INVITE) + rootActivity.openUrlInBrowser(Constants.URL_GITHUB_NEW_ISSUE) } Spacer(Modifier.height(12.dp)) @@ -619,13 +615,14 @@ private fun HelpCenter() { } @Composable -private fun Roadmap() { +private fun ReportBug() { val uriHandler = LocalUriHandler.current SettingsDefaultButton( - icon = R.drawable.ic_custom_rocket_m, - text = stringResource(R.string.roadmap), + icon = R.drawable.ic_vue_dev_arrow, + text = stringResource(R.string.report_bug), + iconPadding = 10.dp, ) { - uriHandler.openUri(Constants.URL_ROADMAP) + uriHandler.openUri(Constants.URL_GITHUB_NEW_ISSUE) } } @@ -1095,35 +1092,6 @@ private fun SettingsSectionDivider( } } -@Composable -fun FetchMissingTransactionsButton( - opFetchTrns: OpResult?, - onFetchMissingTransactions: () -> Unit -) { - val background = Gradient.solid( - when (opFetchTrns) { - is OpResult.Failure -> Red - OpResult.Loading -> Orange - is OpResult.Success -> Green - null -> UI.colors.medium - } - ) - SettingsPrimaryButton( - icon = R.drawable.ic_sync, - text = when (opFetchTrns) { - is OpResult.Failure -> "Error: ${opFetchTrns.error()}" - OpResult.Loading -> "Full sync... wait!" - is OpResult.Success -> "Success. Check transactions." - else -> "Fetch missing transactions" - }, - backgroundGradient = background, - textColor = findContrastTextColor(background.startColor), - iconPadding = 0.dp - ) { - onFetchMissingTransactions() - } -} - @Composable private fun SettingsDefaultButton( @DrawableRes icon: Int, diff --git a/screen/transactions/src/main/java/com/ivy/transactions/TransactionsViewModel.kt b/screen/transactions/src/main/java/com/ivy/transactions/TransactionsViewModel.kt index f6d9c51316..f5942ed38d 100644 --- a/screen/transactions/src/main/java/com/ivy/transactions/TransactionsViewModel.kt +++ b/screen/transactions/src/main/java/com/ivy/transactions/TransactionsViewModel.kt @@ -7,7 +7,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.viewModelScope import arrow.core.toOption -import com.ivy.ui.ComposeViewModel import com.ivy.base.legacy.SharedPrefs import com.ivy.base.legacy.Transaction import com.ivy.base.legacy.TransactionHistoryItem @@ -37,6 +36,7 @@ import com.ivy.legacy.utils.isNotNullOrBlank import com.ivy.legacy.utils.selectEndTextFieldValue import com.ivy.navigation.Navigation import com.ivy.navigation.TransactionsScreen +import com.ivy.ui.ComposeViewModel import com.ivy.wallet.domain.action.account.AccTrnsAct import com.ivy.wallet.domain.action.account.AccountsAct import com.ivy.wallet.domain.action.account.CalcAccBalanceAct diff --git a/settings.gradle.kts b/settings.gradle.kts index 99bb7d4a19..182a7070d6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,6 +22,7 @@ include(":screen:balance") include(":screen:budgets") include(":screen:categories") include(":screen:contributors") +include(":screen:disclaimer") include(":screen:edit-transaction") include(":screen:exchange-rates") include(":screen:features") diff --git a/shared/base/src/main/java/com/ivy/base/TestDispatchersProvider.kt b/shared/base/src/main/java/com/ivy/base/TestDispatchersProvider.kt index fd105fb378..1e78418626 100644 --- a/shared/base/src/main/java/com/ivy/base/TestDispatchersProvider.kt +++ b/shared/base/src/main/java/com/ivy/base/TestDispatchersProvider.kt @@ -1,6 +1,8 @@ package com.ivy.base import com.ivy.base.threading.DispatchersProvider +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import org.jetbrains.annotations.VisibleForTesting import kotlin.coroutines.CoroutineContext @@ -10,4 +12,9 @@ object TestDispatchersProvider : DispatchersProvider { override val main: CoroutineContext = Dispatchers.Unconfined override val io: CoroutineContext = Dispatchers.Unconfined override val default: CoroutineContext = Dispatchers.Unconfined -} \ No newline at end of file +} + +@VisibleForTesting +val TestCoroutineScope = CoroutineScope( + Dispatchers.Unconfined + CoroutineName("test") +) \ No newline at end of file diff --git a/shared/base/src/main/java/com/ivy/base/di/BaseModule.kt b/shared/base/src/main/java/com/ivy/base/di/BaseModule.kt new file mode 100644 index 0000000000..30a8fe0d4d --- /dev/null +++ b/shared/base/src/main/java/com/ivy/base/di/BaseModule.kt @@ -0,0 +1,27 @@ +package com.ivy.base.di + +import com.ivy.base.threading.DispatchersProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import javax.inject.Qualifier + +@Module +@InstallIn(SingletonComponent::class) +object BaseModule { + @AppCoroutineScope + @Provides + fun provideApplicationCoroutineScope( + dispatchers: DispatchersProvider + ): CoroutineScope { + val applicationJob = SupervisorJob() + return CoroutineScope(dispatchers.main + applicationJob) + } +} + +@Retention(AnnotationRetention.BINARY) +@Qualifier +annotation class AppCoroutineScope \ No newline at end of file diff --git a/shared/data/core/src/androidTest/java/com/ivy/data/backup/BackupDataUseCaseAndroidTest.kt b/shared/data/core/src/androidTest/java/com/ivy/data/backup/BackupDataUseCaseAndroidTest.kt index 4811b6b3c3..a61bf7a0b9 100644 --- a/shared/data/core/src/androidTest/java/com/ivy/data/backup/BackupDataUseCaseAndroidTest.kt +++ b/shared/data/core/src/androidTest/java/com/ivy/data/backup/BackupDataUseCaseAndroidTest.kt @@ -15,6 +15,7 @@ import com.ivy.data.repository.fake.FakeAccountRepository import com.ivy.data.repository.fake.FakeCurrencyRepository import com.ivy.data.repository.mapper.AccountMapper import com.ivy.base.TestDispatchersProvider +import com.ivy.data.DataObserver import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.ints.shouldBeGreaterThan import kotlinx.coroutines.runBlocking @@ -67,7 +68,8 @@ class BackupDataUseCaseAndroidTest { context = appContext, json = KotlinxSerializationModule.provideJson(), dispatchersProvider = TestDispatchersProvider, - fileSystem = FileSystem(appContext) + fileSystem = FileSystem(appContext), + dataObserver = DataObserver(), ) } diff --git a/shared/data/core/src/main/java/com/ivy/data/DataObserver.kt b/shared/data/core/src/main/java/com/ivy/data/DataObserver.kt new file mode 100644 index 0000000000..eb466bb685 --- /dev/null +++ b/shared/data/core/src/main/java/com/ivy/data/DataObserver.kt @@ -0,0 +1,44 @@ +package com.ivy.data + +import com.ivy.data.model.Account +import com.ivy.data.model.AccountId +import com.ivy.data.model.Category +import com.ivy.data.model.CategoryId +import com.ivy.data.model.Tag +import com.ivy.data.model.primitive.TagId +import com.ivy.data.model.sync.UniqueId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DataObserver @Inject constructor() { + private val _writeEvents = MutableSharedFlow() + val writeEvents: Flow = _writeEvents + + suspend fun post(event: DataWriteEvent) { + _writeEvents.emit(event) + } +} + +sealed interface DataWriteEvent { + data object AllDataChange : AccountChange, CategoryChange + + sealed interface AccountChange : DataWriteEvent + data class SaveAccounts(val accounts: List) : AccountChange + data class DeleteAccounts(val operation: DeleteOperation) : AccountChange + + sealed interface CategoryChange : DataWriteEvent + data class SaveCategories(val categories: List) : CategoryChange + data class DeleteCategories(val operation: DeleteOperation) : CategoryChange + + sealed interface TagChange : DataWriteEvent + data class SaveTags(val tags: List) : TagChange + data class DeleteTags(val operation: DeleteOperation) : TagChange +} + +sealed interface DeleteOperation { + data object All : DeleteOperation + data class Just(val ids: List) : DeleteOperation +} diff --git a/shared/data/core/src/main/java/com/ivy/data/DataWriteEventBus.kt b/shared/data/core/src/main/java/com/ivy/data/DataWriteEventBus.kt deleted file mode 100644 index 358dab10a8..0000000000 --- a/shared/data/core/src/main/java/com/ivy/data/DataWriteEventBus.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.ivy.data - -import com.ivy.data.model.sync.UniqueId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class DataWriteEventBus @Inject constructor() { - private val internalEvents = MutableSharedFlow() - val events: Flow = internalEvents - - suspend fun post(event: DataWriteEvent) { - internalEvents.emit(event) - } -} - -sealed interface DataWriteEvent { - data class SaveAccounts(val accounts: List) : DataWriteEvent - data class DeleteAccounts(val operation: DeleteOperation) : DataWriteEvent - - data class SaveCategories(val categories: List) : DataWriteEvent - data class DeleteCategories(val operation: DeleteOperation) : DataWriteEvent -} - -sealed interface DeleteOperation { - data object All : DeleteOperation - data class Just(val ids: List) : DeleteOperation -} diff --git a/shared/data/core/src/main/java/com/ivy/data/ExactDsl.kt b/shared/data/core/src/main/java/com/ivy/data/ExactDsl.kt deleted file mode 100644 index 2e8b3a9603..0000000000 --- a/shared/data/core/src/main/java/com/ivy/data/ExactDsl.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.ivy.data - -import arrow.core.Either -import arrow.core.raise.Raise -import arrow.core.raise.either - -class ExactError(msg: String) : IllegalArgumentException(msg) - -interface Exact { - val exactName: String - - fun Raise.spec(raw: Value): ExactValue - - fun from(value: Value): Either = either { spec(value) } - .mapLeft { "$exactName error: $it" } - - /** - * Turns a [Value] into an [ExactError] if it matches the spec. - * Otherwise throws a runtime exception. - * @throws ExactError if the [value] doesn't match the spec. - */ - @Throws(ExactError::class) - fun unsafe(value: Value): ExactValue = from(value).fold( - ifLeft = { throw ExactError(it) }, - ifRight = { it }, - ) -} diff --git a/shared/data/core/src/main/java/com/ivy/data/InMemoryDataStore.kt b/shared/data/core/src/main/java/com/ivy/data/InMemoryDataStore.kt deleted file mode 100644 index 936c4e633c..0000000000 --- a/shared/data/core/src/main/java/com/ivy/data/InMemoryDataStore.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.ivy.data - -import com.ivy.data.model.Account -import com.ivy.data.model.Category -import com.ivy.data.repository.AccountRepository -import com.ivy.data.repository.CategoryRepository -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class InMemoryDataStore @Inject constructor( - private val dataWriteEventBus: DataWriteEventBus, - private val accountRepository: AccountRepository, - private val categoryRepository: CategoryRepository, -) { - private val _accounts = MutableStateFlow(emptyList()) - private val _categories = MutableStateFlow(emptyList()) - - val accounts: StateFlow> = _accounts - val categories: StateFlow> = _categories - - fun init(scope: CoroutineScope) { - scope.updateAccounts() - scope.updateCategories() - scope.launch { - // TODO: This can be optimized. But let's not do premature optimization. - dataWriteEventBus.events.collectLatest { event -> - when (event) { - is DataWriteEvent.SaveAccounts, - is DataWriteEvent.DeleteAccounts -> { - scope.updateAccounts() - } - - is DataWriteEvent.DeleteCategories, - is DataWriteEvent.SaveCategories -> { - scope.updateCategories() - } - } - } - } - } - - private fun CoroutineScope.updateAccounts() { - launch { - _accounts.value = accountRepository.findAll() - } - } - - private fun CoroutineScope.updateCategories() { - launch { - _categories.value = categoryRepository.findAll() - } - } -} \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/backup/BackupDataUseCase.kt b/shared/data/core/src/main/java/com/ivy/data/backup/BackupDataUseCase.kt index bb528622c5..d251db8ec6 100644 --- a/shared/data/core/src/main/java/com/ivy/data/backup/BackupDataUseCase.kt +++ b/shared/data/core/src/main/java/com/ivy/data/backup/BackupDataUseCase.kt @@ -7,6 +7,8 @@ import com.ivy.base.legacy.SharedPrefs import com.ivy.base.legacy.unzip import com.ivy.base.legacy.zip import com.ivy.base.threading.DispatchersProvider +import com.ivy.data.DataObserver +import com.ivy.data.DataWriteEvent import com.ivy.data.db.dao.read.AccountDao import com.ivy.data.db.dao.read.BudgetDao import com.ivy.data.db.dao.read.CategoryDao @@ -61,6 +63,7 @@ class BackupDataUseCase @Inject constructor( private val json: Json, private val dispatchersProvider: DispatchersProvider, private val fileSystem: FileSystem, + private val dataObserver: DataObserver, ) { suspend fun exportToFile( zipFileUri: Uri @@ -149,6 +152,8 @@ class BackupDataUseCase @Inject constructor( categoriesImported = 0, failedRows = persistentListOf() ) + } finally { + dataObserver.post(DataWriteEvent.AllDataChange) } } diff --git a/shared/data/core/src/main/java/com/ivy/data/datasource/LocalLegalDataSource.kt b/shared/data/core/src/main/java/com/ivy/data/datasource/LocalLegalDataSource.kt new file mode 100644 index 0000000000..59ae75445c --- /dev/null +++ b/shared/data/core/src/main/java/com/ivy/data/datasource/LocalLegalDataSource.kt @@ -0,0 +1,26 @@ +package com.ivy.data.datasource + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class LocalLegalDataSource @Inject constructor( + private val dataStore: DataStore +) { + suspend fun getIsDisclaimerAccepted(): Boolean? = dataStore.data + .map { it[DisclaimerAcceptedKey] }.firstOrNull() + + suspend fun setDisclaimerAccepted(accepted: Boolean) { + dataStore.edit { + it[DisclaimerAcceptedKey] = accepted + } + } + + companion object { + private val DisclaimerAcceptedKey = booleanPreferencesKey("disclaimer_accepted") + } +} \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/di/RepositoryBindingsModule.kt b/shared/data/core/src/main/java/com/ivy/data/di/RepositoryBindingsModule.kt index af6e87f6f2..21f3b3436a 100644 --- a/shared/data/core/src/main/java/com/ivy/data/di/RepositoryBindingsModule.kt +++ b/shared/data/core/src/main/java/com/ivy/data/di/RepositoryBindingsModule.kt @@ -4,12 +4,14 @@ import com.ivy.data.repository.AccountRepository import com.ivy.data.repository.CategoryRepository import com.ivy.data.repository.CurrencyRepository import com.ivy.data.repository.ExchangeRatesRepository +import com.ivy.data.repository.LegalRepository import com.ivy.data.repository.TagsRepository import com.ivy.data.repository.TransactionRepository import com.ivy.data.repository.impl.AccountRepositoryImpl import com.ivy.data.repository.impl.CategoryRepositoryImpl import com.ivy.data.repository.impl.CurrencyRepositoryImpl import com.ivy.data.repository.impl.ExchangeRatesRepositoryImpl +import com.ivy.data.repository.impl.LegalRepositoryImpl import com.ivy.data.repository.impl.TagsRepositoryImpl import com.ivy.data.repository.impl.TransactionRepositoryImpl import dagger.Binds @@ -37,4 +39,7 @@ abstract class RepositoryBindingsModule { @Binds abstract fun bindCurrencyRepo(repo: CurrencyRepositoryImpl): CurrencyRepository + + @Binds + abstract fun bindLegalRepo(repo: LegalRepositoryImpl): LegalRepository } diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/AccountRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/AccountRepository.kt index 0520e328b0..f0edf64986 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/AccountRepository.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/AccountRepository.kt @@ -4,12 +4,12 @@ import com.ivy.data.model.Account import com.ivy.data.model.AccountId interface AccountRepository { - suspend fun findById(id: com.ivy.data.model.AccountId): com.ivy.data.model.Account? - suspend fun findAll(deleted: Boolean = false): List + suspend fun findById(id: AccountId): Account? + suspend fun findAll(deleted: Boolean = false): List suspend fun findMaxOrderNum(): Double - suspend fun save(value: com.ivy.data.model.Account) - suspend fun saveMany(values: List) - suspend fun deleteById(id: com.ivy.data.model.AccountId) + suspend fun save(value: Account) + suspend fun saveMany(values: List) + suspend fun deleteById(id: AccountId) suspend fun deleteAll() } diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/CategoryRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/CategoryRepository.kt index 2117b03d9c..3f6b0e3091 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/CategoryRepository.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/CategoryRepository.kt @@ -4,12 +4,12 @@ import com.ivy.data.model.Category import com.ivy.data.model.CategoryId interface CategoryRepository { - suspend fun findAll(deleted: Boolean = false): List - suspend fun findById(id: com.ivy.data.model.CategoryId): com.ivy.data.model.Category? + suspend fun findAll(deleted: Boolean = false): List + suspend fun findById(id: CategoryId): Category? suspend fun findMaxOrderNum(): Double - suspend fun save(value: com.ivy.data.model.Category) - suspend fun saveMany(values: List) - suspend fun deleteById(id: com.ivy.data.model.CategoryId) + suspend fun save(value: Category) + suspend fun saveMany(values: List) + suspend fun deleteById(id: CategoryId) suspend fun deleteAll() } diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/LegalRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/LegalRepository.kt new file mode 100644 index 0000000000..1da7c198fd --- /dev/null +++ b/shared/data/core/src/main/java/com/ivy/data/repository/LegalRepository.kt @@ -0,0 +1,6 @@ +package com.ivy.data.repository + +interface LegalRepository { + suspend fun isDisclaimerAccepted(): Boolean + suspend fun setDisclaimerAccepted(accepted: Boolean) +} \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/TagsRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/TagsRepository.kt index 1c0a1e4b6c..cef2e4f18e 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/TagsRepository.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/TagsRepository.kt @@ -6,18 +6,18 @@ import com.ivy.data.model.primitive.AssociationId import com.ivy.data.model.primitive.TagId interface TagsRepository { - suspend fun findByIds(id: TagId): com.ivy.data.model.Tag? - suspend fun findByIds(ids: List): List - suspend fun findByAssociatedId(id: AssociationId): List - suspend fun findByAssociatedId(ids: List): Map> - suspend fun findAll(deleted: Boolean = false): List - suspend fun findByText(text: String): List - suspend fun findByAllAssociatedIdForTagId(tagIds: List): Map> - suspend fun findByAllTagsForAssociations(): Map> + suspend fun findByIds(id: TagId): Tag? + suspend fun findByIds(ids: List): List + suspend fun findByAssociatedId(id: AssociationId): List + suspend fun findByAssociatedId(ids: List): Map> + suspend fun findAll(deleted: Boolean = false): List + suspend fun findByText(text: String): List + suspend fun findByAllAssociatedIdForTagId(tagIds: List): Map> + suspend fun findByAllTagsForAssociations(): Map> suspend fun associateTagToEntity(associationId: AssociationId, tagId: TagId) suspend fun removeTagAssociation(associationId: AssociationId, tagId: TagId) - suspend fun save(value: com.ivy.data.model.Tag) - suspend fun updateTag(tagId: TagId, value: com.ivy.data.model.Tag) + suspend fun save(value: Tag) + suspend fun updateTag(tagId: TagId, value: Tag) suspend fun deleteById(id: TagId) suspend fun deleteAll() } diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/TransactionRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/TransactionRepository.kt index 4c1e028917..9c53b35b5c 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/TransactionRepository.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/TransactionRepository.kt @@ -12,216 +12,216 @@ import java.util.UUID interface TransactionRepository { - suspend fun findAll(): List + suspend fun findAll(): List @Suppress("FunctionNaming") - suspend fun findAll_LIMIT_1(): List + suspend fun findAll_LIMIT_1(): List - suspend fun findAllIncome(): List + suspend fun findAllIncome(): List - suspend fun findAllExpense(): List + suspend fun findAllExpense(): List - suspend fun findAllTransfer(): List + suspend fun findAllTransfer(): List - suspend fun findAllIncomeByAccount(accountId: com.ivy.data.model.AccountId): List + suspend fun findAllIncomeByAccount(accountId: AccountId): List - suspend fun findAllExpenseByAccount(accountId: com.ivy.data.model.AccountId): List + suspend fun findAllExpenseByAccount(accountId: AccountId): List - suspend fun findAllTransferByAccount(accountId: com.ivy.data.model.AccountId): List + suspend fun findAllTransferByAccount(accountId: AccountId): List suspend fun findAllIncomeByAccountBetween( - accountId: com.ivy.data.model.AccountId, + accountId: AccountId, startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List suspend fun findAllExpenseByAccountBetween( - accountId: com.ivy.data.model.AccountId, + accountId: AccountId, startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List suspend fun findAllTransferByAccountBetween( - accountId: com.ivy.data.model.AccountId, + accountId: AccountId, startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List suspend fun findAllTransfersToAccount( - toAccountId: com.ivy.data.model.AccountId, - ): List + toAccountId: AccountId, + ): List suspend fun findAllTransfersToAccountBetween( - toAccountId: com.ivy.data.model.AccountId, + toAccountId: AccountId, startDate: LocalDateTime, endDate: LocalDateTime, - ): List + ): List suspend fun findAllBetween( startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List suspend fun findAllByAccountAndBetween( - accountId: com.ivy.data.model.AccountId, + accountId: AccountId, startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List suspend fun findAllByCategoryAndBetween( - categoryId: com.ivy.data.model.CategoryId, + categoryId: CategoryId, startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List suspend fun findAllUnspecifiedAndBetween( startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List suspend fun findAllIncomeByCategoryAndBetween( - categoryId: com.ivy.data.model.CategoryId, + categoryId: CategoryId, startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List suspend fun findAllExpenseByCategoryAndBetween( - categoryId: com.ivy.data.model.CategoryId, + categoryId: CategoryId, startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List suspend fun findAllTransferByCategoryAndBetween( - categoryId: com.ivy.data.model.CategoryId, + categoryId: CategoryId, startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List suspend fun findAllUnspecifiedIncomeAndBetween( startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List suspend fun findAllUnspecifiedExpenseAndBetween( startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List suspend fun findAllUnspecifiedTransferAndBetween( startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List suspend fun findAllToAccountAndBetween( - toAccountId: com.ivy.data.model.AccountId, + toAccountId: AccountId, startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List suspend fun findAllDueToBetween( startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List suspend fun findAllDueToBetweenByCategory( startDate: LocalDateTime, endDate: LocalDateTime, - categoryId: com.ivy.data.model.CategoryId - ): List + categoryId: CategoryId + ): List suspend fun findAllDueToBetweenByCategoryUnspecified( startDate: LocalDateTime, endDate: LocalDateTime, - ): List + ): List suspend fun findAllDueToBetweenByAccount( startDate: LocalDateTime, endDate: LocalDateTime, - accountId: com.ivy.data.model.AccountId - ): List + accountId: AccountId + ): List - suspend fun findAllByRecurringRuleId(recurringRuleId: UUID): List + suspend fun findAllByRecurringRuleId(recurringRuleId: UUID): List suspend fun findAllIncomeBetween( startDate: LocalDateTime, endDate: LocalDateTime, - ): List + ): List suspend fun findAllExpenseBetween( startDate: LocalDateTime, endDate: LocalDateTime, - ): List + ): List suspend fun findAllTransferBetween( startDate: LocalDateTime, endDate: LocalDateTime, - ): List + ): List suspend fun findAllBetweenAndRecurringRuleId( startDate: LocalDateTime, endDate: LocalDateTime, recurringRuleId: UUID - ): List + ): List - suspend fun findById(id: com.ivy.data.model.TransactionId): com.ivy.data.model.Transaction? - suspend fun findByIds(ids: List): List + suspend fun findById(id: TransactionId): Transaction? + suspend fun findByIds(ids: List): List suspend fun findByIsSyncedAndIsDeleted( synced: Boolean, deleted: Boolean = false - ): List + ): List suspend fun countHappenedTransactions(): Long - suspend fun findAllByTitleMatchingPattern(pattern: String): List + suspend fun findAllByTitleMatchingPattern(pattern: String): List suspend fun countByTitleMatchingPattern( pattern: String, ): Long suspend fun findAllByCategory( - categoryId: com.ivy.data.model.CategoryId, - ): List + categoryId: CategoryId, + ): List suspend fun countByTitleMatchingPatternAndCategoryId( pattern: String, - categoryId: com.ivy.data.model.CategoryId + categoryId: CategoryId ): Long suspend fun findAllByAccount( - accountId: com.ivy.data.model.AccountId - ): List + accountId: AccountId + ): List suspend fun countByTitleMatchingPatternAndAccountId( pattern: String, - accountId: com.ivy.data.model.AccountId + accountId: AccountId ): Long suspend fun findLoanTransaction( loanId: UUID - ): com.ivy.data.model.Transaction? + ): Transaction? suspend fun findLoanRecordTransaction( loanRecordId: UUID - ): com.ivy.data.model.Transaction? + ): Transaction? suspend fun findAllByLoanId( loanId: UUID - ): List + ): List - suspend fun save(accountId: com.ivy.data.model.AccountId, value: com.ivy.data.model.Transaction) + suspend fun save(accountId: AccountId, value: Transaction) - suspend fun saveMany(accountId: com.ivy.data.model.AccountId, value: List) + suspend fun saveMany(accountId: AccountId, value: List) - suspend fun flagDeleted(id: com.ivy.data.model.TransactionId) + suspend fun flagDeleted(id: TransactionId) suspend fun flagDeletedByRecurringRuleIdAndNoDateTime(recurringRuleId: UUID) - suspend fun flagDeletedByAccountId(accountId: com.ivy.data.model.AccountId) + suspend fun flagDeletedByAccountId(accountId: AccountId) - suspend fun deleteById(id: com.ivy.data.model.TransactionId) + suspend fun deleteById(id: TransactionId) - suspend fun deleteAllByAccountId(accountId: com.ivy.data.model.AccountId) + suspend fun deleteAllByAccountId(accountId: AccountId) suspend fun deleteAll() } \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeAccountRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeAccountRepository.kt index 1bc7729b60..68445dde01 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeAccountRepository.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeAccountRepository.kt @@ -1,6 +1,7 @@ package com.ivy.data.repository.fake -import com.ivy.data.DataWriteEventBus +import com.ivy.base.TestCoroutineScope +import com.ivy.data.DataObserver import com.ivy.data.db.dao.read.AccountDao import com.ivy.data.db.dao.read.SettingsDao import com.ivy.data.db.dao.write.WriteAccountDao @@ -27,6 +28,7 @@ class FakeAccountRepository( accountDao = accountDao, writeAccountDao = writeAccountDao, dispatchersProvider = TestDispatchersProvider, - writeEventBus = DataWriteEventBus(), + dataObserver = DataObserver(), + appCoroutineScope = TestCoroutineScope ) ) : AccountRepository by accountRepository \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeTagRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeTagRepository.kt index a5f50963a3..9f35e70850 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeTagRepository.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeTagRepository.kt @@ -1,6 +1,8 @@ package com.ivy.data.repository.fake +import com.ivy.base.TestCoroutineScope import com.ivy.base.TestDispatchersProvider +import com.ivy.data.DataObserver import com.ivy.data.db.dao.read.TagAssociationDao import com.ivy.data.db.dao.read.TagDao import com.ivy.data.db.dao.write.WriteTagAssociationDao @@ -22,7 +24,8 @@ class FakeTagRepository( tagAssociationDao = tagAssociationDao, writeTagDao = writeTagDao, writeTagAssociationDao = writeTagAssociationDao, - dispatchersProvider = TestDispatchersProvider - + dispatchersProvider = TestDispatchersProvider, + dataObserver = DataObserver(), + appCoroutineScope = TestCoroutineScope, ) ) : TagsRepository by tagsRepository \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/impl/AccountRepositoryImpl.kt b/shared/data/core/src/main/java/com/ivy/data/repository/impl/AccountRepositoryImpl.kt index 056efcaad7..6d016ed942 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/impl/AccountRepositoryImpl.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/impl/AccountRepositoryImpl.kt @@ -1,8 +1,9 @@ package com.ivy.data.repository.impl +import com.ivy.base.di.AppCoroutineScope import com.ivy.base.threading.DispatchersProvider +import com.ivy.data.DataObserver import com.ivy.data.DataWriteEvent -import com.ivy.data.DataWriteEventBus import com.ivy.data.DeleteOperation import com.ivy.data.db.dao.read.AccountDao import com.ivy.data.db.dao.write.WriteAccountDao @@ -10,6 +11,9 @@ import com.ivy.data.model.Account import com.ivy.data.model.AccountId import com.ivy.data.repository.AccountRepository import com.ivy.data.repository.mapper.AccountMapper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton @@ -20,12 +24,31 @@ class AccountRepositoryImpl @Inject constructor( private val accountDao: AccountDao, private val writeAccountDao: WriteAccountDao, private val dispatchersProvider: DispatchersProvider, - private val writeEventBus: DataWriteEventBus, + private val dataObserver: DataObserver, + @AppCoroutineScope + private val appCoroutineScope: CoroutineScope, ) : AccountRepository { - private val accountsMemo = mutableMapOf() + init { + appCoroutineScope.launch { + dataObserver.writeEvents.collectLatest { event -> + when (event) { + DataWriteEvent.AllDataChange -> { + findAllMemoized = false + accountsMemo.clear() + } + else -> { + // do nothing + } + } + } + } + } - override suspend fun findById(id: com.ivy.data.model.AccountId): com.ivy.data.model.Account? { + private val accountsMemo = mutableMapOf() + private var findAllMemoized = false + + override suspend fun findById(id: AccountId): Account? { return accountsMemo[id] ?: withContext(dispatchersProvider.io) { accountDao.findById(id.value)?.let { with(mapper) { it.toDomain() }.getOrNull() @@ -37,14 +60,17 @@ class AccountRepositoryImpl @Inject constructor( } } - override suspend fun findAll(deleted: Boolean): List { - return if (accountsMemo.isNotEmpty()) { + override suspend fun findAll(deleted: Boolean): List { + return if (findAllMemoized) { accountsMemo.values.sortedBy { it.orderNum } } else { withContext(dispatchersProvider.io) { accountDao.findAll(deleted).mapNotNull { with(mapper) { it.toDomain() }.getOrNull() - }.also(::memoize) + }.also { + memoize(it) + findAllMemoized = true + } } } } @@ -59,38 +85,38 @@ class AccountRepositoryImpl @Inject constructor( } } - override suspend fun save(value: com.ivy.data.model.Account) { + override suspend fun save(value: Account) { withContext(dispatchersProvider.io) { writeAccountDao.save( with(mapper) { value.toEntity() } ) // Memoize accountsMemo[value.id] = value - writeEventBus.post(DataWriteEvent.SaveAccounts(listOf(value))) + dataObserver.post(DataWriteEvent.SaveAccounts(listOf(value))) } } - override suspend fun saveMany(values: List) { + override suspend fun saveMany(values: List) { withContext(dispatchersProvider.io) { writeAccountDao.saveMany( values.map { with(mapper) { it.toEntity() } } ) memoize(values) - writeEventBus.post(DataWriteEvent.SaveAccounts(values)) + dataObserver.post(DataWriteEvent.SaveAccounts(values)) } } - private fun memoize(accounts: List) { + private fun memoize(accounts: List) { accounts.forEach { accountsMemo[it.id] = it } } - override suspend fun deleteById(id: com.ivy.data.model.AccountId) { + override suspend fun deleteById(id: AccountId) { withContext(dispatchersProvider.io) { accountsMemo.remove(id) writeAccountDao.deleteById(id.value) - writeEventBus.post( + dataObserver.post( DataWriteEvent.DeleteAccounts(DeleteOperation.Just(listOf(id))) ) } @@ -100,7 +126,7 @@ class AccountRepositoryImpl @Inject constructor( withContext(dispatchersProvider.io) { accountsMemo.clear() writeAccountDao.deleteAll() - writeEventBus.post(DataWriteEvent.DeleteAccounts(DeleteOperation.All)) + dataObserver.post(DataWriteEvent.DeleteAccounts(DeleteOperation.All)) } } } diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/impl/CategoryRepositoryImpl.kt b/shared/data/core/src/main/java/com/ivy/data/repository/impl/CategoryRepositoryImpl.kt index 7aafce80b0..06f2d7d5f4 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/impl/CategoryRepositoryImpl.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/impl/CategoryRepositoryImpl.kt @@ -1,8 +1,9 @@ package com.ivy.data.repository.impl +import com.ivy.base.di.AppCoroutineScope import com.ivy.base.threading.DispatchersProvider +import com.ivy.data.DataObserver import com.ivy.data.DataWriteEvent -import com.ivy.data.DataWriteEventBus import com.ivy.data.DeleteOperation import com.ivy.data.db.dao.read.CategoryDao import com.ivy.data.db.dao.write.WriteCategoryDao @@ -10,6 +11,9 @@ import com.ivy.data.model.Category import com.ivy.data.model.CategoryId import com.ivy.data.repository.CategoryRepository import com.ivy.data.repository.mapper.CategoryMapper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton @@ -20,27 +24,47 @@ class CategoryRepositoryImpl @Inject constructor( private val writeCategoryDao: WriteCategoryDao, private val categoryDao: CategoryDao, private val dispatchersProvider: DispatchersProvider, - private val writeEventBus: DataWriteEventBus, + private val dataObserver: DataObserver, + @AppCoroutineScope + private val appCoroutineScope: CoroutineScope ) : CategoryRepository { - private val categoriesMemo = mutableMapOf() + init { + appCoroutineScope.launch { + dataObserver.writeEvents.collectLatest { event -> + when (event) { + DataWriteEvent.AllDataChange -> { + findAllMemoized = false + categoriesMemo.clear() + } + + else -> { + // do nothing + } + } + } + } + } + + private val categoriesMemo = mutableMapOf() private var findAllMemoized: Boolean = false - override suspend fun findAll(deleted: Boolean): List { + override suspend fun findAll(deleted: Boolean): List { return if (findAllMemoized) { categoriesMemo.values.sortedBy { it.orderNum } } else { withContext(dispatchersProvider.io) { categoryDao.findAll(deleted).mapNotNull { with(mapper) { it.toDomain() }.getOrNull() - }.also(::memoize).also { + }.also { + memoize(it) findAllMemoized = true } } } } - override suspend fun findById(id: com.ivy.data.model.CategoryId): com.ivy.data.model.Category? { + override suspend fun findById(id: CategoryId): Category? { return categoriesMemo[id] ?: withContext(dispatchersProvider.io) { categoryDao.findById(id.value)?.let { with(mapper) { it.toDomain() }.getOrNull() @@ -62,31 +86,31 @@ class CategoryRepositoryImpl @Inject constructor( } } - override suspend fun save(value: com.ivy.data.model.Category) { + override suspend fun save(value: Category) { return withContext(dispatchersProvider.io) { writeCategoryDao.save( with(mapper) { value.toEntity() } ) + dataObserver.post(DataWriteEvent.SaveCategories(listOf(value))) categoriesMemo[value.id] = value - writeEventBus.post(DataWriteEvent.SaveCategories(listOf(value))) } } - override suspend fun saveMany(values: List) { + override suspend fun saveMany(values: List) { withContext(dispatchersProvider.io) { writeCategoryDao.saveMany( values.map { with(mapper) { it.toEntity() } } ) memoize(values) - writeEventBus.post(DataWriteEvent.SaveCategories(values)) + dataObserver.post(DataWriteEvent.SaveCategories(values)) } } - override suspend fun deleteById(id: com.ivy.data.model.CategoryId) { + override suspend fun deleteById(id: CategoryId) { withContext(dispatchersProvider.io) { categoriesMemo.remove(id) writeCategoryDao.deleteById(id.value) - writeEventBus.post( + dataObserver.post( DataWriteEvent.DeleteCategories( DeleteOperation.Just(listOf(id)) ) @@ -98,11 +122,11 @@ class CategoryRepositoryImpl @Inject constructor( withContext(dispatchersProvider.io) { categoriesMemo.clear() writeCategoryDao.deleteAll() - writeEventBus.post(DataWriteEvent.DeleteCategories(DeleteOperation.All)) + dataObserver.post(DataWriteEvent.DeleteCategories(DeleteOperation.All)) } } - private fun memoize(accounts: List) { + private fun memoize(accounts: List) { accounts.forEach { categoriesMemo[it.id] = it } diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/impl/LegalRepositoryImpl.kt b/shared/data/core/src/main/java/com/ivy/data/repository/impl/LegalRepositoryImpl.kt new file mode 100644 index 0000000000..3d01ea17ad --- /dev/null +++ b/shared/data/core/src/main/java/com/ivy/data/repository/impl/LegalRepositoryImpl.kt @@ -0,0 +1,22 @@ +package com.ivy.data.repository.impl + +import com.ivy.base.threading.DispatchersProvider +import com.ivy.data.datasource.LocalLegalDataSource +import com.ivy.data.repository.LegalRepository +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class LegalRepositoryImpl @Inject constructor( + private val localLegalDataSource: LocalLegalDataSource, + private val dispatchers: DispatchersProvider +) : LegalRepository { + override suspend fun isDisclaimerAccepted(): Boolean = withContext(dispatchers.io) { + localLegalDataSource.getIsDisclaimerAccepted() ?: false + } + + override suspend fun setDisclaimerAccepted( + accepted: Boolean + ): Unit = withContext(dispatchers.io) { + localLegalDataSource.setDisclaimerAccepted(accepted) + } +} \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/impl/TagsRepositoryImpl.kt b/shared/data/core/src/main/java/com/ivy/data/repository/impl/TagsRepositoryImpl.kt index b7e42bf080..1cded27013 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/impl/TagsRepositoryImpl.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/impl/TagsRepositoryImpl.kt @@ -1,6 +1,10 @@ package com.ivy.data.repository.impl +import com.ivy.base.di.AppCoroutineScope import com.ivy.base.threading.DispatchersProvider +import com.ivy.data.DataObserver +import com.ivy.data.DataWriteEvent +import com.ivy.data.DeleteOperation import com.ivy.data.db.dao.read.TagAssociationDao import com.ivy.data.db.dao.read.TagDao import com.ivy.data.db.dao.write.WriteTagAssociationDao @@ -11,8 +15,11 @@ import com.ivy.data.model.primitive.AssociationId import com.ivy.data.model.primitive.TagId import com.ivy.data.repository.TagsRepository import com.ivy.data.repository.mapper.TagMapper +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.UUID import javax.inject.Inject @@ -25,10 +32,27 @@ class TagsRepositoryImpl @Inject constructor( private val tagAssociationDao: TagAssociationDao, private val writeTagDao: WriteTagDao, private val writeTagAssociationDao: WriteTagAssociationDao, - private val dispatchersProvider: DispatchersProvider + private val dispatchersProvider: DispatchersProvider, + private val dataObserver: DataObserver, + @AppCoroutineScope + private val appCoroutineScope: CoroutineScope ) : TagsRepository { - companion object { - private const val MAX_SQL_LITE_QUERY_SIZE = 999 + + init { + appCoroutineScope.launch { + dataObserver.writeEvents.collectLatest { event -> + when (event) { + DataWriteEvent.AllDataChange -> { + findAllMemoized = false + tagsMemo.clear() + } + + else -> { + // do nothing + } + } + } + } } private val tagsMemo = mutableMapOf() @@ -153,6 +177,7 @@ class TagsRepositoryImpl @Inject constructor( writeTagDao.save(with(mapper) { value.toEntity() }) }.also { memoize(value) + dataObserver.post(DataWriteEvent.SaveTags(listOf(value))) } } @@ -160,6 +185,7 @@ class TagsRepositoryImpl @Inject constructor( withContext(dispatchersProvider.io) { writeTagDao.update(with(mapper) { value.toEntity() }) memoize(value) + dataObserver.post(DataWriteEvent.SaveTags(listOf(value))) } } @@ -168,6 +194,7 @@ class TagsRepositoryImpl @Inject constructor( writeTagAssociationDao.deleteAssociationsByTagId(id.value) writeTagDao.deleteById(id.value) tagsMemo.remove(id) + dataObserver.post(DataWriteEvent.DeleteTags(DeleteOperation.Just(listOf(id)))) } } @@ -176,6 +203,7 @@ class TagsRepositoryImpl @Inject constructor( tagsMemo.clear() writeTagAssociationDao.deleteAll() writeTagDao.deleteAll() + dataObserver.post(DataWriteEvent.DeleteTags(DeleteOperation.All)) } } @@ -188,4 +216,8 @@ class TagsRepositoryImpl @Inject constructor( } private fun List.toRawValues(): List = this.map { it.value } + + companion object { + private const val MAX_SQL_LITE_QUERY_SIZE = 999 + } } diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/mapper/TagMapper.kt b/shared/data/core/src/main/java/com/ivy/data/repository/mapper/TagMapper.kt index a74d131720..135111decd 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/mapper/TagMapper.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/mapper/TagMapper.kt @@ -21,8 +21,8 @@ class TagMapper @Inject constructor() { fun createNewTagId(): TagId = TagId(UUID.randomUUID()) } - fun TagEntity.toDomain(): Either = either { - com.ivy.data.model.Tag( + fun TagEntity.toDomain(): Either = either { + Tag( id = TagId(id), name = NotBlankTrimmedString.from(name).bind(), description = description, @@ -35,7 +35,7 @@ class TagMapper @Inject constructor() { ) } - fun com.ivy.data.model.Tag.toEntity(): TagEntity { + fun Tag.toEntity(): TagEntity { return TagEntity( id = id.value, name = name.value, @@ -49,7 +49,7 @@ class TagMapper @Inject constructor() { ) } - fun com.ivy.data.model.TagAssociation.toEntity(): TagAssociationEntity { + fun TagAssociation.toEntity(): TagAssociationEntity { return TagAssociationEntity( tagId = id.value, associatedId = associatedId.value, @@ -58,8 +58,8 @@ class TagMapper @Inject constructor() { ) } - fun TagAssociationEntity.toDomain(): com.ivy.data.model.TagAssociation { - return com.ivy.data.model.TagAssociation( + fun TagAssociationEntity.toDomain(): TagAssociation { + return TagAssociation( id = TagId(tagId), associatedId = AssociationId(associatedId), lastUpdated = lastSyncedTime, @@ -67,8 +67,8 @@ class TagMapper @Inject constructor() { ) } - fun createNewTag(tagId: TagId = createNewTagId(), name: NotBlankTrimmedString): com.ivy.data.model.Tag { - return com.ivy.data.model.Tag( + fun createNewTag(tagId: TagId = createNewTagId(), name: NotBlankTrimmedString): Tag { + return Tag( id = tagId, name = name, description = null, @@ -81,8 +81,8 @@ class TagMapper @Inject constructor() { ) } - fun createNewTagAssociation(tagId: TagId, associationId: AssociationId): com.ivy.data.model.TagAssociation { - return com.ivy.data.model.TagAssociation( + fun createNewTagAssociation(tagId: TagId, associationId: AssociationId): TagAssociation { + return TagAssociation( id = tagId, associatedId = associationId, lastUpdated = Instant.EPOCH, diff --git a/shared/data/core/src/main/java/com/ivy/data/temp/migration/TempMigrationUtils.kt b/shared/data/core/src/main/java/com/ivy/data/temp/migration/TempMigrationUtils.kt index 9c26fda68c..833b85b83e 100644 --- a/shared/data/core/src/main/java/com/ivy/data/temp/migration/TempMigrationUtils.kt +++ b/shared/data/core/src/main/java/com/ivy/data/temp/migration/TempMigrationUtils.kt @@ -10,48 +10,48 @@ import java.math.BigDecimal import java.time.Instant import java.util.UUID -fun com.ivy.data.model.Transaction.getValue(): BigDecimal = +fun Transaction.getValue(): BigDecimal = when (this) { - is com.ivy.data.model.Expense -> value.amount.value.toBigDecimal() - is com.ivy.data.model.Income -> value.amount.value.toBigDecimal() - is com.ivy.data.model.Transfer -> fromValue.amount.value.toBigDecimal() + is Expense -> value.amount.value.toBigDecimal() + is Income -> value.amount.value.toBigDecimal() + is Transfer -> fromValue.amount.value.toBigDecimal() } -fun com.ivy.data.model.Transaction.getAccountId(): UUID = +fun Transaction.getAccountId(): UUID = when (this) { - is com.ivy.data.model.Expense -> account.value - is com.ivy.data.model.Income -> account.value - is com.ivy.data.model.Transfer -> fromAccount.value + is Expense -> account.value + is Income -> account.value + is Transfer -> fromAccount.value } -fun com.ivy.data.model.Transaction.getAccount(): com.ivy.data.model.AccountId = when (this) { - is com.ivy.data.model.Expense -> account - is com.ivy.data.model.Income -> account - is com.ivy.data.model.Transfer -> fromAccount +fun Transaction.getAccount(): AccountId = when (this) { + is Expense -> account + is Income -> account + is Transfer -> fromAccount } -fun com.ivy.data.model.Transaction.getTransactionType(): TransactionType { +fun Transaction.getTransactionType(): TransactionType { return when (this) { - is com.ivy.data.model.Expense -> TransactionType.EXPENSE - is com.ivy.data.model.Income -> TransactionType.INCOME - is com.ivy.data.model.Transfer -> TransactionType.TRANSFER + is Expense -> TransactionType.EXPENSE + is Income -> TransactionType.INCOME + is Transfer -> TransactionType.TRANSFER } } -fun com.ivy.data.model.Transaction.settleNow(): com.ivy.data.model.Transaction { +fun Transaction.settleNow(): Transaction { val timeNow = Instant.now() return when (this) { - is com.ivy.data.model.Income -> this.copy( + is Income -> this.copy( settled = true, time = timeNow ) - is com.ivy.data.model.Expense -> this.copy( + is Expense -> this.copy( settled = true, time = timeNow ) - is com.ivy.data.model.Transfer -> this.copy( + is Transfer -> this.copy( settled = true, time = timeNow ) diff --git a/shared/data/core/src/test/java/com/ivy/data/backup/BackupDataUseCaseTest.kt b/shared/data/core/src/test/java/com/ivy/data/backup/BackupDataUseCaseTest.kt index 357d60ef5d..5cb73dba15 100644 --- a/shared/data/core/src/test/java/com/ivy/data/backup/BackupDataUseCaseTest.kt +++ b/shared/data/core/src/test/java/com/ivy/data/backup/BackupDataUseCaseTest.kt @@ -2,6 +2,7 @@ package com.ivy.data.backup import com.ivy.base.TestDispatchersProvider import com.ivy.base.di.KotlinxSerializationModule +import com.ivy.data.DataObserver import com.ivy.data.db.dao.fake.FakeAccountDao import com.ivy.data.db.dao.fake.FakeBudgetDao import com.ivy.data.db.dao.fake.FakeCategoryDao @@ -63,7 +64,8 @@ class BackupDataUseCaseTest { sharedPrefs = mockk(relaxed = true), json = KotlinxSerializationModule.provideJson(), dispatchersProvider = TestDispatchersProvider, - fileSystem = mockk(relaxed = true) + fileSystem = mockk(relaxed = true), + dataObserver = DataObserver(), ) private suspend fun backupTestCase(backupVersion: String) { diff --git a/shared/data/core/src/test/java/com/ivy/data/repository/impl/AccountRepositoryImplTest.kt b/shared/data/core/src/test/java/com/ivy/data/repository/impl/AccountRepositoryImplTest.kt index 44da45751a..71a888316a 100644 --- a/shared/data/core/src/test/java/com/ivy/data/repository/impl/AccountRepositoryImplTest.kt +++ b/shared/data/core/src/test/java/com/ivy/data/repository/impl/AccountRepositoryImplTest.kt @@ -1,15 +1,14 @@ package com.ivy.data.repository.impl +import com.ivy.base.TestCoroutineScope import com.ivy.base.TestDispatchersProvider import com.ivy.data.DataWriteEvent -import com.ivy.data.DataWriteEventBus +import com.ivy.data.DataObserver import com.ivy.data.DeleteOperation import com.ivy.data.db.dao.fake.FakeSettingsDao import com.ivy.data.db.dao.read.AccountDao import com.ivy.data.db.dao.write.WriteAccountDao import com.ivy.data.db.entity.AccountEntity -import com.ivy.data.model.Account -import com.ivy.data.model.AccountId import com.ivy.data.model.primitive.AssetCode import com.ivy.data.model.primitive.ColorInt import com.ivy.data.model.primitive.NotBlankTrimmedString @@ -31,7 +30,7 @@ import java.util.UUID class AccountRepositoryImplTest { val accountDao = mockk() val writeAccountDao = mockk() - val writeEventBus = mockk(relaxed = true) + val writeEventBus = mockk(relaxed = true) private lateinit var repository: AccountRepository @@ -43,7 +42,8 @@ class AccountRepositoryImplTest { accountDao = accountDao, writeAccountDao = writeAccountDao, dispatchersProvider = TestDispatchersProvider, - writeEventBus = writeEventBus, + dataObserver = writeEventBus, + appCoroutineScope = TestCoroutineScope, ) } diff --git a/shared/data/core/src/test/java/com/ivy/data/repository/impl/CategoryRepositoryImplTest.kt b/shared/data/core/src/test/java/com/ivy/data/repository/impl/CategoryRepositoryImplTest.kt index f14463c7eb..e033bd7146 100644 --- a/shared/data/core/src/test/java/com/ivy/data/repository/impl/CategoryRepositoryImplTest.kt +++ b/shared/data/core/src/test/java/com/ivy/data/repository/impl/CategoryRepositoryImplTest.kt @@ -1,14 +1,13 @@ package com.ivy.data.repository.impl +import com.ivy.base.TestCoroutineScope import com.ivy.base.TestDispatchersProvider import com.ivy.data.DataWriteEvent -import com.ivy.data.DataWriteEventBus +import com.ivy.data.DataObserver import com.ivy.data.DeleteOperation import com.ivy.data.db.dao.read.CategoryDao import com.ivy.data.db.dao.write.WriteCategoryDao import com.ivy.data.db.entity.CategoryEntity -import com.ivy.data.model.Category -import com.ivy.data.model.CategoryId import com.ivy.data.model.primitive.ColorInt import com.ivy.data.model.primitive.NotBlankTrimmedString import com.ivy.data.repository.CategoryRepository @@ -28,7 +27,7 @@ import java.util.UUID class CategoryRepositoryImplTest { private val categoryDao = mockk() private val writeCategoryDao = mockk() - private val writeEventBus = mockk(relaxed = true) + private val writeEventBus = mockk(relaxed = true) private lateinit var repository: CategoryRepository @@ -39,7 +38,8 @@ class CategoryRepositoryImplTest { categoryDao = categoryDao, writeCategoryDao = writeCategoryDao, dispatchersProvider = TestDispatchersProvider, - writeEventBus = writeEventBus + dataObserver = writeEventBus, + appCoroutineScope = TestCoroutineScope, ) } diff --git a/shared/domain/src/main/java/com/ivy/domain/event/AccountUpdatedEvent.kt b/shared/domain/src/main/java/com/ivy/domain/event/AccountUpdatedEvent.kt deleted file mode 100644 index 2d374b8340..0000000000 --- a/shared/domain/src/main/java/com/ivy/domain/event/AccountUpdatedEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.ivy.domain.event - -data object AccountUpdatedEvent : EventBus.Event diff --git a/shared/domain/src/main/java/com/ivy/domain/event/EventBus.kt b/shared/domain/src/main/java/com/ivy/domain/event/EventBus.kt deleted file mode 100644 index 8be1707c0e..0000000000 --- a/shared/domain/src/main/java/com/ivy/domain/event/EventBus.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.ivy.domain.event - -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.filter -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class EventBus @Inject constructor() { - private val eventFlow = MutableSharedFlow( - replay = 0, - extraBufferCapacity = 1 - ) - - sealed interface Event - - /** - * @param events the events that you want to be notified about - * or empty to listen for all - */ - suspend fun subscribe( - vararg events: Event, - onEvent: suspend (Event) -> Unit - ) { - eventFlow.filter { events.isEmpty() || it in events } - .collectLatest { - onEvent(it) - } - } - - fun post(event: Event) { - eventFlow.tryEmit(event) - } -} diff --git a/shared/ui/core/src/main/java/com/ivy/ui/component/OpenSourceCard.kt b/shared/ui/core/src/main/java/com/ivy/ui/component/OpenSourceCard.kt new file mode 100644 index 0000000000..13177665ec --- /dev/null +++ b/shared/ui/core/src/main/java/com/ivy/ui/component/OpenSourceCard.kt @@ -0,0 +1,76 @@ +package com.ivy.ui.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.ivy.design.system.colors.IvyColors +import com.ivy.ui.R + +const val IvyWalletGitHubRepoUrl = "https://github.com/Ivy-Apps/ivy-wallet" + +@Composable +fun OpenSourceCard( + modifier: Modifier = Modifier, +) { + val uriHandler = LocalUriHandler.current + ElevatedCard( + modifier = modifier.fillMaxWidth(), + onClick = { + uriHandler.openUri(IvyWalletGitHubRepoUrl) + } + ) { + Row( + modifier = Modifier.padding( + horizontal = 16.dp, + vertical = 12.dp, + ), + verticalAlignment = Alignment.CenterVertically + ) { + GitHubLogo() + Spacer(modifier = Modifier.width(12.dp)) + OpenSourceTexts() + } + } +} + +@Composable +private fun GitHubLogo(modifier: Modifier = Modifier) { + Icon( + modifier = modifier, + painter = painterResource(R.drawable.github_logo), + contentDescription = null, + ) +} + +@Composable +private fun OpenSourceTexts(modifier: Modifier = Modifier) { + Column( + modifier = modifier, + ) { + Text( + text = stringResource(R.string.ivy_wallet_open_source), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold + ) + Text( + text = IvyWalletGitHubRepoUrl, + style = MaterialTheme.typography.labelSmall, + color = IvyColors.Blue.primary + ) + } +} \ No newline at end of file diff --git a/shared/ui/core/src/main/res/values/strings.xml b/shared/ui/core/src/main/res/values/strings.xml index 50189c2c7e..4782bc46f8 100644 --- a/shared/ui/core/src/main/res/values/strings.xml +++ b/shared/ui/core/src/main/res/values/strings.xml @@ -261,8 +261,9 @@ Ivy Telegram group Help Center Roadmap + Report a bug Request a feature - Contact support + Community support Contributors ACCOUNT Logout diff --git a/shared/ui/core/src/test/java/com/ivy/ui/PaparazziScreenshotTest.kt b/shared/ui/core/src/test/java/com/ivy/ui/PaparazziScreenshotTest.kt new file mode 100644 index 0000000000..38d20f2a22 --- /dev/null +++ b/shared/ui/core/src/test/java/com/ivy/ui/PaparazziScreenshotTest.kt @@ -0,0 +1,33 @@ +package com.ivy.ui + +import androidx.compose.runtime.Composable +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import com.ivy.design.system.IvyMaterial3Theme +import org.junit.Rule + +open class PaparazziScreenshotTest { + @get:Rule + val paparazzi = Paparazzi( + deviceConfig = DeviceConfig.PIXEL_6_PRO, + showSystemUi = true, + maxPercentDifference = 0.001 + ) + + protected fun snapshot(theme: PaparazziTheme, content: @Composable () -> Unit) { + paparazzi.snapshot { + IvyMaterial3Theme( + dark = when (theme) { + PaparazziTheme.Light -> false + PaparazziTheme.Dark -> true + } + ) { + content() + } + } + } +} + +enum class PaparazziTheme { + Light, Dark +} \ No newline at end of file diff --git a/shared/ui/core/src/test/java/com/ivy/ui/component/OpenSourceCardPaparazziTest.kt b/shared/ui/core/src/test/java/com/ivy/ui/component/OpenSourceCardPaparazziTest.kt new file mode 100644 index 0000000000..6c2d77d7b6 --- /dev/null +++ b/shared/ui/core/src/test/java/com/ivy/ui/component/OpenSourceCardPaparazziTest.kt @@ -0,0 +1,28 @@ +package com.ivy.ui.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import com.ivy.ui.PaparazziScreenshotTest +import com.ivy.ui.PaparazziTheme +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(TestParameterInjector::class) +class OpenSourceCardPaparazziTest( + @TestParameter + private val theme: PaparazziTheme, +) : PaparazziScreenshotTest() { + + @Test + fun `default state`() { + snapshot(theme) { + Box(modifier = Modifier.padding(16.dp)) { + OpenSourceCard() + } + } + } +} \ No newline at end of file diff --git a/shared/ui/core/src/test/snapshots/images/com.ivy.ui.component_OpenSourceCardPaparazziTest_default state[Dark].png b/shared/ui/core/src/test/snapshots/images/com.ivy.ui.component_OpenSourceCardPaparazziTest_default state[Dark].png new file mode 100644 index 0000000000..37c3b01af2 --- /dev/null +++ b/shared/ui/core/src/test/snapshots/images/com.ivy.ui.component_OpenSourceCardPaparazziTest_default state[Dark].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc3444dee2e3ee10c8a3ad44306e432ab5dd11225f92be527773278f5e6fe8af +size 14313 diff --git a/shared/ui/core/src/test/snapshots/images/com.ivy.ui.component_OpenSourceCardPaparazziTest_default state[Light].png b/shared/ui/core/src/test/snapshots/images/com.ivy.ui.component_OpenSourceCardPaparazziTest_default state[Light].png new file mode 100644 index 0000000000..687f45510d --- /dev/null +++ b/shared/ui/core/src/test/snapshots/images/com.ivy.ui.component_OpenSourceCardPaparazziTest_default state[Light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9916ec1631d0cb63c89d26f622f47905b225622092d0a434c0d17fb087222840 +size 14074 diff --git a/shared/ui/navigation/src/main/java/com/ivy/navigation/Screens.kt b/shared/ui/navigation/src/main/java/com/ivy/navigation/Screens.kt index cee578f041..13c67e6199 100644 --- a/shared/ui/navigation/src/main/java/com/ivy/navigation/Screens.kt +++ b/shared/ui/navigation/src/main/java/com/ivy/navigation/Screens.kt @@ -138,3 +138,5 @@ data object AttributionsScreen : Screen data object ContributorsScreen : Screen data object ReleasesScreen : Screen + +data object DisclaimerScreen : Screen diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/Constants.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/Constants.kt index 6292971013..d71ecfc6ba 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/Constants.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/Constants.kt @@ -8,7 +8,7 @@ object Constants { const val URL_IVY_WALLET_REPO = "https://github.com/Ivy-Apps/ivy-wallet" - const val URL_ROADMAP = "https://github.com/orgs/Ivy-Apps/projects/1/views/1" + const val URL_GITHUB_NEW_ISSUE = "https://github.com/Ivy-Apps/ivy-wallet/issues/new/choose" const val URL_HELP_CENTER = "https://t.me/+ETavgioAvWg4NThk" diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/LogoutLogic.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/LogoutLogic.kt index 0c012573b8..9f00a1bc93 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/LogoutLogic.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/LogoutLogic.kt @@ -1,7 +1,9 @@ package com.ivy.legacy -import com.ivy.data.db.IvyRoomDatabase import com.ivy.base.legacy.SharedPrefs +import com.ivy.data.DataObserver +import com.ivy.data.DataWriteEvent +import com.ivy.data.db.IvyRoomDatabase import com.ivy.legacy.utils.ioThread import com.ivy.navigation.MainScreen import com.ivy.navigation.Navigation @@ -12,7 +14,8 @@ import javax.inject.Inject class LogoutLogic @Inject constructor( private val ivyDb: IvyRoomDatabase, private val sharedPrefs: SharedPrefs, - private val navigation: Navigation + private val navigation: Navigation, + private val dataObserver: DataObserver, ) { suspend fun logout() { ioThread { @@ -20,6 +23,7 @@ class LogoutLogic @Inject constructor( sharedPrefs.removeAll() } + dataObserver.post(DataWriteEvent.AllDataChange) navigation.resetBackStack() navigation.navigateTo(OnboardingScreen) }