diff --git a/app/src/androidTest/java/com/ivy/wallet/compose/scenario/OperationsCoreTest.kt b/app/src/androidTest/java/com/ivy/wallet/compose/scenario/OperationsCoreTest.kt index b37919d4fd..9110a3f926 100644 --- a/app/src/androidTest/java/com/ivy/wallet/compose/scenario/OperationsCoreTest.kt +++ b/app/src/androidTest/java/com/ivy/wallet/compose/scenario/OperationsCoreTest.kt @@ -1,6 +1,9 @@ package com.ivy.wallet.compose.scenario -import androidx.compose.ui.test.* +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick import com.ivy.wallet.compose.IvyComposeTest import com.ivy.wallet.compose.helpers.* import dagger.hilt.android.testing.HiltAndroidTest @@ -67,8 +70,13 @@ class OperationsCoreTest : IvyComposeTest() { category = "Investments" ) - composeTestRule.onNodeWithTag("transaction_card") - .assertIsDisplayed() + homeTab.dismissPrompt() + + homeTab.clickTransaction( + amount = "5,000.00", + title = "Salary", + category = "Investments" + ) } @Test diff --git a/app/src/main/java/com/ivy/wallet/base/DateExt.kt b/app/src/main/java/com/ivy/wallet/base/DateExt.kt index 63905fd93a..287f246c5c 100644 --- a/app/src/main/java/com/ivy/wallet/base/DateExt.kt +++ b/app/src/main/java/com/ivy/wallet/base/DateExt.kt @@ -173,11 +173,32 @@ fun LocalTime.convertLocalToUTC(): LocalTime { return this.minusSeconds(offset) } +fun LocalTime.convertUTCToLocal(): LocalTime { + val offset = timeNowLocal().atZone(ZoneOffset.systemDefault()).offset.totalSeconds.toLong() + return this.plusSeconds(offset) +} + fun LocalDateTime.convertLocalToUTC(): LocalDateTime { val offset = timeNowLocal().atZone(ZoneOffset.systemDefault()).offset.totalSeconds.toLong() return this.minusSeconds(offset) } +// The timepicker returns time in UTC, but the date picker returns date in LocalTimeZone +// hence use this method to get both date & time in UTC +fun getTrueDate(date: LocalDate, time: LocalTime, convert: Boolean = true): LocalDateTime { + val timeLocal = if (convert) time.convertUTCToLocal() else time + + return timeNowUTC() + .withYear(date.year) + .withMonth(date.monthValue) + .withDayOfMonth(date.dayOfMonth) + .withHour(timeLocal.hour) + .withMinute(timeLocal.minute) + .withSecond(0) + .withNano(0) + .convertLocalToUTC() +} + fun LocalDate.formatLocal( pattern: String = "dd MMM yyyy", @@ -230,10 +251,10 @@ fun LocalDateTime.timeLeft( } fun startOfMonth(date: LocalDate): LocalDateTime = - date.withDayOfMonth(1).atStartOfDay() + date.withDayOfMonth(1).atStartOfDay().convertLocalToUTC() fun endOfMonth(date: LocalDate): LocalDateTime = - date.withDayOfMonth(date.lengthOfMonth()).atEndOfDay() + date.withDayOfMonth(date.lengthOfMonth()).atEndOfDay().convertLocalToUTC() fun LocalDate.atEndOfDay(): LocalDateTime = this.atTime(23, 59, 59) diff --git a/app/src/main/java/com/ivy/wallet/ui/balance/BalanceViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/balance/BalanceViewModel.kt index 155ff7996e..8a5b8ee07d 100644 --- a/app/src/main/java/com/ivy/wallet/ui/balance/BalanceViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/balance/BalanceViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ivy.wallet.base.asLiveData +import com.ivy.wallet.base.dateNowUTC import com.ivy.wallet.base.ioThread import com.ivy.wallet.logic.PlannedPaymentsLogic import com.ivy.wallet.logic.WalletLogic @@ -62,18 +63,20 @@ class BalanceViewModel @Inject constructor( fun nextMonth() { val month = period.value?.month + val year = period.value?.year ?: dateNowUTC().year if (month != null) { start( - period = month.incrementMonthPeriod(ivyContext, 1L), + period = month.incrementMonthPeriod(ivyContext, 1L, year = year), ) } } fun previousMonth() { val month = period.value?.month + val year = period.value?.year ?: dateNowUTC().year if (month != null) { start( - period = month.incrementMonthPeriod(ivyContext, -1L), + period = month.incrementMonthPeriod(ivyContext, -1L, year = year), ) } } diff --git a/app/src/main/java/com/ivy/wallet/ui/edit/EditTransactionScreen.kt b/app/src/main/java/com/ivy/wallet/ui/edit/EditTransactionScreen.kt index 0c8b367050..ec760c87ec 100644 --- a/app/src/main/java/com/ivy/wallet/ui/edit/EditTransactionScreen.kt +++ b/app/src/main/java/com/ivy/wallet/ui/edit/EditTransactionScreen.kt @@ -15,6 +15,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.google.accompanist.insets.navigationBarsPadding import com.google.accompanist.insets.statusBarsPadding import com.ivy.wallet.R +import com.ivy.wallet.base.convertUTCtoLocal +import com.ivy.wallet.base.getTrueDate import com.ivy.wallet.base.onScreenStart import com.ivy.wallet.base.timeNowLocal import com.ivy.wallet.logic.model.CreateAccountData @@ -242,10 +244,10 @@ private fun BoxWithConstraintsScope.UI( dueDateTime = dueDate, ) { ivyContext.datePicker( - initialDate = dateTime?.toLocalDate(), + initialDate = dateTime?.convertUTCtoLocal()?.toLocalDate(), ) { date -> ivyContext.timePicker { time -> - onSetDateTime(date.atTime(time.hour, time.minute, time.second)) + onSetDateTime(getTrueDate(date, time)) } } } diff --git a/app/src/main/java/com/ivy/wallet/ui/home/HomeViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/home/HomeViewModel.kt index 18df055e54..10abe69fa5 100644 --- a/app/src/main/java/com/ivy/wallet/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/home/HomeViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ivy.wallet.base.TestIdlingResource import com.ivy.wallet.base.asLiveData +import com.ivy.wallet.base.dateNowUTC import com.ivy.wallet.base.ioThread import com.ivy.wallet.logic.CustomerJourneyLogic import com.ivy.wallet.logic.PlannedPaymentsLogic @@ -275,18 +276,20 @@ class HomeViewModel @Inject constructor( fun nextMonth() { val month = period.value?.month + val year = period.value?.year ?: dateNowUTC().year if (month != null) { load( - period = month.incrementMonthPeriod(ivyContext, 1L), + period = month.incrementMonthPeriod(ivyContext, 1L, year = year), ) } } fun previousMonth() { val month = period.value?.month + val year = period.value?.year ?: dateNowUTC().year if (month != null) { load( - period = month.incrementMonthPeriod(ivyContext, -1L), + period = month.incrementMonthPeriod(ivyContext, -1L, year = year), ) } } diff --git a/app/src/main/java/com/ivy/wallet/ui/onboarding/model/TimePeriod.kt b/app/src/main/java/com/ivy/wallet/ui/onboarding/model/TimePeriod.kt index a21c906594..e302aa77fd 100644 --- a/app/src/main/java/com/ivy/wallet/ui/onboarding/model/TimePeriod.kt +++ b/app/src/main/java/com/ivy/wallet/ui/onboarding/model/TimePeriod.kt @@ -7,6 +7,7 @@ import java.time.LocalDateTime data class TimePeriod( val month: Month? = null, + val year: Int? = null, val fromToRange: FromToTimeRange? = null, val lastNRange: LastNTimeRange? = null, ) { @@ -41,7 +42,8 @@ data class TimePeriod( return TimePeriod( month = Month.fromMonthValue( periodDate.monthValue - ) + ), + year = periodDate.year ) } } @@ -54,7 +56,7 @@ data class TimePeriod( ): FromToTimeRange { return when { month != null -> { - val date = month.toDate() + val date = if (year != null) month.toDate().withYear(year) else month.toDate() val (from, to) = if (startDateOfMonth != 1) { customStartDayOfMonthPeriodRange( date = date, @@ -95,14 +97,16 @@ data class TimePeriod( val from = date .withDayOfMonthSafe(startDateOfMonth) .atStartOfDay() + .convertLocalToUTC() val to = date - .withDayOfMonthSafe(startDateOfMonth) //startDayOfMonth != 1 just shift N day the month forward so to should +1 month .plusMonths(1) + .withDayOfMonthSafe(startDateOfMonth) //e.g. Correct: 14.10-13.11 (Incorrect: 14.10-14.11) .minusDays(1) .atEndOfDay() + .convertLocalToUTC() return Pair(from, to) } @@ -113,13 +117,11 @@ data class TimePeriod( return when { month != null -> { if (startDateOfMonth == 1) { - month.name + displayMonthStartingOn1st(month = month) } else { val range = toRange(startDateOfMonth) val pattern = "MMM dd" - //Don't use formatLocal() because .to is at 23:59:59 => - // it may appear as +1 day in some timeZones when converted - "${range.from?.format(pattern)} - ${range.to?.format(pattern)}" + "${range.from?.formatLocal(pattern)} - ${range.to?.formatLocal(pattern)}" } } fromToRange != null -> { @@ -138,7 +140,7 @@ data class TimePeriod( return when { month != null -> { if (startDateOfMonth == 1) { - month.name + displayMonthStartingOn1st(month = month) } else { toRange(startDateOfMonth).toDisplay() } @@ -153,6 +155,17 @@ data class TimePeriod( toRange(startDateOfMonth).toDisplay() } } + } + private fun displayMonthStartingOn1st(month: Month): String { + val year = year + return if (year != null && dateNowUTC().year != year) { + //not this year + "${month.name}, $year" + } else { + //this year + month.name + } } + } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/statistic/level1/PieChartStatisticViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/statistic/level1/PieChartStatisticViewModel.kt index b958682354..3a0d6c8f12 100644 --- a/app/src/main/java/com/ivy/wallet/ui/statistic/level1/PieChartStatisticViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/statistic/level1/PieChartStatisticViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ivy.wallet.base.asLiveData +import com.ivy.wallet.base.dateNowUTC import com.ivy.wallet.base.ioThread import com.ivy.wallet.logic.WalletCategoryLogic import com.ivy.wallet.logic.WalletLogic @@ -142,9 +143,10 @@ class PieChartStatisticViewModel @Inject constructor( fun nextMonth() { val month = period.value?.month + val year = period.value?.year ?: dateNowUTC().year if (month != null) { load( - period = month.incrementMonthPeriod(ivyContext, 1L), + period = month.incrementMonthPeriod(ivyContext, 1L,year), type = type.value!! ) } @@ -152,9 +154,10 @@ class PieChartStatisticViewModel @Inject constructor( fun previousMonth() { val month = period.value?.month + val year = period.value?.year ?: dateNowUTC().year if (month != null) { load( - period = month.incrementMonthPeriod(ivyContext, -1L), + period = month.incrementMonthPeriod(ivyContext, -1L,year), type = type.value!! ) } diff --git a/app/src/main/java/com/ivy/wallet/ui/statistic/level2/ItemStatisticViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/statistic/level2/ItemStatisticViewModel.kt index 8d5c1ce7f5..f4ee5fe15e 100644 --- a/app/src/main/java/com/ivy/wallet/ui/statistic/level2/ItemStatisticViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/statistic/level2/ItemStatisticViewModel.kt @@ -3,10 +3,7 @@ package com.ivy.wallet.ui.statistic.level2 import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.ivy.wallet.base.TestIdlingResource -import com.ivy.wallet.base.asLiveData -import com.ivy.wallet.base.ioThread -import com.ivy.wallet.base.isNotNullOrBlank +import com.ivy.wallet.base.* import com.ivy.wallet.logic.* import com.ivy.wallet.logic.currency.ExchangeRatesLogic import com.ivy.wallet.model.TransactionHistoryItem @@ -316,10 +313,11 @@ class ItemStatisticViewModel @Inject constructor( fun nextMonth(screen: Screen.ItemStatistic) { val month = period.value?.month + val year = period.value?.year ?: dateNowUTC().year if (month != null) { start( screen = screen, - period = month.incrementMonthPeriod(ivyContext, 1L), + period = month.incrementMonthPeriod(ivyContext, 1L,year), reset = false ) } @@ -327,10 +325,11 @@ class ItemStatisticViewModel @Inject constructor( fun previousMonth(screen: Screen.ItemStatistic) { val month = period.value?.month + val year = period.value?.year ?: dateNowUTC().year if (month != null) { start( screen = screen, - period = month.incrementMonthPeriod(ivyContext, -1L), + period = month.incrementMonthPeriod(ivyContext, -1L,year), reset = false ) } diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/ChoosePeriodModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/ChoosePeriodModal.kt index 4a75b275f4..40cc0b6e9f 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/modal/ChoosePeriodModal.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/modal/ChoosePeriodModal.kt @@ -73,10 +73,13 @@ fun BoxWithConstraintsScope.ChoosePeriodModal( Spacer(Modifier.height(32.dp)) ChooseMonth( - selectedMonth = period?.month + selectedMonthYear = period?.month?.let { + MonthYear(month = it, year = period?.year ?: dateNowUTC().year) + } ) { period = TimePeriod( - month = it + month = it.month, + year = it.year ) } @@ -125,36 +128,52 @@ fun BoxWithConstraintsScope.ChoosePeriodModal( @Composable private fun ChooseMonth( - selectedMonth: Month?, - onSelected: (Month) -> Unit, + selectedMonthYear: MonthYear?, + onSelected: (MonthYear) -> Unit, ) { Text( modifier = Modifier .padding(start = 32.dp), text = "Choose month", style = Typo.body1.style( - color = if (selectedMonth != null) IvyTheme.colors.pureInverse else Gray, + color = if (selectedMonthYear != null) IvyTheme.colors.pureInverse else Gray, fontWeight = FontWeight.ExtraBold ) ) Spacer(Modifier.height(24.dp)) - val months = monthsList() + val currentYear = dateNowUTC().year + val months = remember(currentYear) { + monthsList() + .map { + MonthYear(month = it, year = currentYear - 1) + } + .plus( + monthsList().map { MonthYear(month = it, year = currentYear) } + ) + .plus( + monthsList().map { MonthYear(month = it, year = currentYear + 1) } + ) + } val state = rememberLazyListState() val coroutineScope = rememberCoroutineScope() onScreenStart { - if (selectedMonth != null) { - val selectedMonthIndex = months.indexOf(selectedMonth) + if (selectedMonthYear != null) { + val selectedMonthIndex = months.indexOf(selectedMonthYear) if (selectedMonthIndex != -1) { coroutineScope.launch { state.scrollToItem(selectedMonthIndex) } } } else { - val currentMonthIndex = months.indexOf(fromMonthValue(dateNowUTC().monthValue)) + val currentMonthYear = MonthYear( + month = fromMonthValue(dateNowUTC().monthValue), + year = currentYear + ) + val currentMonthIndex = months.indexOf(currentMonthYear) if (currentMonthIndex != -1) { coroutineScope.launch { state.scrollToItem(currentMonthIndex) @@ -171,12 +190,12 @@ private fun ChooseMonth( Spacer(Modifier.width(12.dp)) } - items(items = months) { month -> + items(items = months) { monthYear -> MonthButton( - selected = month == selectedMonth, - text = month.name + selected = monthYear == selectedMonthYear, + text = monthYear.forDisplay(currentYear = currentYear) ) { - onSelected(month) + onSelected(monthYear) } Spacer(Modifier.width(12.dp)) @@ -184,6 +203,23 @@ private fun ChooseMonth( } } +data class MonthYear( + val month: Month, + val year: Int +) { + fun forDisplay( + currentYear: Int + ): String { + return if (year != currentYear) { + //not current year + "${month.name}, $year" + } else { + //current year + month.name + } + } +} + @Composable private fun MonthButton( modifier: Modifier = Modifier, @@ -417,7 +453,7 @@ private fun AllTime( onSelected: (FromToTimeRange?) -> Unit, ) { val active = timeRange != null && timeRange.from == null && - timeRange.to != null && timeRange.to.isAfter(timeNowUTC()) + timeRange.to != null && timeRange.to.isAfter(timeNowUTC()) Text( modifier = Modifier diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/LoanRecordModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/LoanRecordModal.kt index 1d77bc2c3a..f63a333c69 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/modal/LoanRecordModal.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/modal/LoanRecordModal.kt @@ -179,9 +179,9 @@ private fun DateTimeRow( iconStart = R.drawable.ic_date ) { ivyContext.datePicker( - initialDate = dateTime.toLocalDate() + initialDate = dateTime.convertUTCtoLocal().toLocalDate() ) { - onSetDateTime(it.atTime(dateTime.toLocalTime())) + onSetDateTime(getTrueDate(it, dateTime.toLocalTime())) } } @@ -192,7 +192,7 @@ private fun DateTimeRow( iconStart = R.drawable.ic_date ) { ivyContext.timePicker { - onSetDateTime(dateTime.toLocalDate().atTime(it)) + onSetDateTime(getTrueDate(dateTime.convertUTCtoLocal().toLocalDate(), it)) } } diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/model/Month.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/model/Month.kt index 1ed1f3f487..62167d7cab 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/modal/model/Month.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/modal/model/Month.kt @@ -34,10 +34,15 @@ data class Month( .withMonth(monthValue) - fun incrementMonthPeriod(ivyContext: IvyContext, increment: Long): TimePeriod { - val incrementedMonth = toDate().plusMonths(increment) + fun incrementMonthPeriod( + ivyContext: IvyContext, + increment: Long, + year: Int + ): TimePeriod { + val incrementedMonth = toDate().withYear(year).plusMonths(increment) val incrementedPeriod = TimePeriod( - month = fromMonthValue(incrementedMonth.monthValue) + month = fromMonthValue(incrementedMonth.monthValue), + year = incrementedMonth.year ) ivyContext.updateSelectedPeriodInMemory(incrementedPeriod) return incrementedPeriod diff --git a/buildSrc/src/main/java/com/ivy/wallet/buildsrc/dependencies.kt b/buildSrc/src/main/java/com/ivy/wallet/buildsrc/dependencies.kt index 26cc667dd5..2826f2d6c5 100644 --- a/buildSrc/src/main/java/com/ivy/wallet/buildsrc/dependencies.kt +++ b/buildSrc/src/main/java/com/ivy/wallet/buildsrc/dependencies.kt @@ -21,8 +21,8 @@ object Libs { object Project { //Version - const val versionName = "2.3.3-halley" - const val versionCode = 93 + const val versionName = "2.3.4-halley" + const val versionCode = 94 //Compile SDK & Build Tools const val compileSdkVersion = 31