Skip to content

Commit

Permalink
Merge branch 'release/3.3.8'
Browse files Browse the repository at this point in the history
  • Loading branch information
benoitletondor committed Nov 15, 2024
2 parents 4739d17 + 124fde9 commit bdb6fa0
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 45 deletions.
1 change: 1 addition & 0 deletions Android/EasyBudget/.idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions Android/EasyBudget/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ android {
compileSdk = 35
minSdk = 23
targetSdk = 35
versionCode = 155
versionName = "3.3.7"
versionCode = 156
versionName = "3.3.8"
vectorDrawables.useSupportLibrary = true
}

Expand Down Expand Up @@ -90,8 +90,8 @@ android {

compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}

buildFeatures {
Expand All @@ -114,7 +114,7 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")

coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.2")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.3")

implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.core:core-ktx:1.15.0")
Expand All @@ -130,7 +130,7 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")

implementation(platform("com.google.firebase:firebase-bom:33.5.1"))
implementation(platform("com.google.firebase:firebase-bom:33.6.0"))
implementation("com.google.firebase:firebase-messaging-ktx")
implementation("com.google.firebase:firebase-storage")
implementation("com.google.firebase:firebase-crashlytics")
Expand All @@ -139,7 +139,7 @@ dependencies {
implementation("com.google.firebase:firebase-config")
implementation("com.firebaseui:firebase-ui-auth:8.0.2")

val composeBom = platform("androidx.compose:compose-bom:2024.10.01")
val composeBom = platform("androidx.compose:compose-bom:2024.11.00")
implementation(composeBom)
androidTestImplementation(composeBom)
debugImplementation("androidx.compose.ui:ui-tooling")
Expand All @@ -148,7 +148,7 @@ dependencies {
implementation("androidx.compose.ui:ui")
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
implementation("androidx.navigation:navigation-compose:2.8.3")
implementation("androidx.navigation:navigation-compose:2.8.4")

implementation("com.google.accompanist:accompanist-themeadapter-material3:0.36.0")
implementation("com.google.accompanist:accompanist-permissions:0.36.0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ private const val BACKUP_DB_FILENAME = "db_backup"

class BackupException(message: String) : Exception("Backup: $message")

suspend fun backupDB(context: Context,
cloudStorage: CloudStorage,
auth: Auth,
parameters: Parameters,
iab: Iab): ListenableWorker.Result {
suspend fun backupDB(
context: Context,
cloudStorage: CloudStorage,
auth: Auth,
parameters: Parameters,
iab: Iab,
): ListenableWorker.Result {
Logger.debug("BackupJob", "Starting backup")

val currentUser = (auth.state.value as? AuthState.Authenticated)?.currentUser
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,4 @@ enum class PurchaseType {
LEGACY_PREMIUM,
PREMIUM_SUBSCRIPTION,
PRO_SUBSCRIPTION,
}

/**
* Intent action broadcast when the status of iab changed
*/
const val INTENT_IAB_STATUS_CHANGED = "iabStatusChanged"
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ package com.benoitletondor.easybudgetapp.iab

import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.android.billingclient.api.*
import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams
import com.benoitletondor.easybudgetapp.R
Expand Down Expand Up @@ -74,16 +72,12 @@ class IabImpl(
}

/**
* Set the new iab status and notify the app by sending an [.INTENT_IAB_STATUS_CHANGED] intent
* Set the new iab status and notify the app via the [iabStatusFlow]
*
* @param status the new status
*/
private fun setIabStatusAndNotify(status: PremiumCheckStatus) {
iabStatusMutableFlow.value = status

val intent = Intent(INTENT_IAB_STATUS_CHANGED)

LocalBroadcastManager.getInstance(appContext).sendBroadcast(intent)
}

override fun isIabReady(): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ import com.benoitletondor.easybudgetapp.model.RecurringExpenseDeleteType
import com.benoitletondor.easybudgetapp.parameters.ONBOARDING_STEP_COMPLETED
import com.benoitletondor.easybudgetapp.parameters.Parameters
import com.benoitletondor.easybudgetapp.parameters.getInitDate
import com.benoitletondor.easybudgetapp.parameters.getLastBackupDate
import com.benoitletondor.easybudgetapp.parameters.getLatestSelectedOnlineAccountId
import com.benoitletondor.easybudgetapp.parameters.getOnboardingStep
import com.benoitletondor.easybudgetapp.parameters.isBackupEnabled
import com.benoitletondor.easybudgetapp.parameters.setLatestSelectedOnlineAccountId
import com.benoitletondor.easybudgetapp.parameters.setUserSawMonthlyReportHint
import com.benoitletondor.easybudgetapp.parameters.watchFirstDayOfWeek
Expand All @@ -63,6 +65,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.time.LocalDate
import java.time.YearMonth
import java.util.Date
import java.util.concurrent.TimeUnit
import javax.inject.Inject

@HiltViewModel
Expand Down Expand Up @@ -383,6 +387,57 @@ class MainViewModel @Inject constructor(

val shouldNavigateToOnboarding get() = parameters.getOnboardingStep() != ONBOARDING_STEP_COMPLETED

init {
monitorLastBackupState()
}

// TODO remove this whole block once we have enough data
private fun monitorLastBackupState() {
viewModelScope.launch {
try {
iab.iabStatusFlow.collectLatest { iabStatusFlow ->
when(iabStatusFlow) {
PremiumCheckStatus.INITIALIZING,
PremiumCheckStatus.CHECKING,
PremiumCheckStatus.ERROR,
PremiumCheckStatus.NOT_PREMIUM -> Unit
PremiumCheckStatus.LEGACY_PREMIUM,
PremiumCheckStatus.PREMIUM_SUBSCRIBED,
PremiumCheckStatus.PRO_SUBSCRIBED -> {
if (parameters.isBackupEnabled()) {
fun backupDiffDays(lastBackupDate: Date?): Long? {
if (lastBackupDate == null) {
return null
}

val now = Date()
val diff = now.time - lastBackupDate.time
val diffInDays = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS)

return diffInDays
}

val lastBackupDate = parameters.getLastBackupDate()
val backupDiffDaysValue = backupDiffDays(lastBackupDate)
if (backupDiffDaysValue != null) {
Logger.warning("Backup is late, last backup was $backupDiffDaysValue days ago", Exception("Late backup exception"))
} else {
Logger.warning("Backup is active but never happened")
}
} else {
Logger.debug("Backup is inactive")
}
}
}
}
} catch (e: Exception) {
if (e is CancellationException) throw e

Logger.warning("Error while monitoring late offline account backup", e)
}
}
}

fun onOnboardingResult(onboardingResult: OnboardingResult) {
viewModelScope.launch {
if (onboardingResult.onboardingCompleted) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,11 @@ private fun AccountsView(
is AccountSelectorViewModel.State.NotPro,
is AccountSelectorViewModel.State.Error -> true
}
val shouldDisplayOfflineBackupEnabled = state is AccountSelectorViewModel.OfflineBackStateAvailable && state.isOfflineBackupEnabled

val offlineBackupState = if (state is AccountSelectorViewModel.OfflineBackupStateAvailable) { state.backupState } else { null }
val shouldDisplayOfflineBackupEnabled = offlineBackupState != null && offlineBackupState is AccountSelectorViewModel.OfflineBackupState.Enabled
val shouldDisplayOfflineBackupIsLate = offlineBackupState != null && offlineBackupState is AccountSelectorViewModel.OfflineBackupState.Enabled && offlineBackupState.showLateBackupAlert
// TODO: implement this warning

val context = LocalContext.current

Expand Down Expand Up @@ -639,7 +643,7 @@ fun AccountsNotProViewPreview() {
AppTheme {
AccountsView(
state = AccountSelectorViewModel.State.NotPro(
isOfflineBackupEnabled = false,
backupState = AccountSelectorViewModel.OfflineBackupState.Disabled,
),
eventFlow = MutableSharedFlow(),
onIabErrorRetryButtonClicked = {},
Expand All @@ -661,7 +665,7 @@ fun AccountsNotAuthenticatedViewPreview() {
AppTheme {
AccountsView(
state = AccountSelectorViewModel.State.NotAuthenticated(
isOfflineBackupEnabled = false,
backupState = AccountSelectorViewModel.OfflineBackupState.Disabled,
),
eventFlow = MutableSharedFlow(),
onIabErrorRetryButtonClicked = {},
Expand Down Expand Up @@ -719,7 +723,7 @@ fun AccountsAvailableViewPreview() {
)
),
pendingInvitations = listOf(),
isOfflineBackupEnabled = true,
backupState = AccountSelectorViewModel.OfflineBackupState.Enabled(showLateBackupAlert = false),
),
eventFlow = MutableSharedFlow(),
onIabErrorRetryButtonClicked = {},
Expand Down Expand Up @@ -797,7 +801,7 @@ fun AccountsAvailableFullViewPreview() {
user = CurrentUser("", "", ""),
)
),
isOfflineBackupEnabled = false,
backupState = AccountSelectorViewModel.OfflineBackupState.Enabled(showLateBackupAlert = true),
),
eventFlow = MutableSharedFlow(),
onIabErrorRetryButtonClicked = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ import com.benoitletondor.easybudgetapp.helper.combine
import com.benoitletondor.easybudgetapp.iab.Iab
import com.benoitletondor.easybudgetapp.iab.PremiumCheckStatus
import com.benoitletondor.easybudgetapp.parameters.Parameters
import com.benoitletondor.easybudgetapp.parameters.getLastBackupDate
import com.benoitletondor.easybudgetapp.parameters.watchIsBackupEnabled
import com.benoitletondor.easybudgetapp.parameters.watchLastBackupDate
import com.benoitletondor.easybudgetapp.parameters.watchLatestSelectedOnlineAccountId
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CancellationException
Expand All @@ -44,6 +46,8 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.retryWhen
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.util.Date
import java.util.concurrent.TimeUnit
import javax.inject.Inject

@HiltViewModel
Expand Down Expand Up @@ -94,14 +98,21 @@ class AccountSelectorViewModel @Inject constructor(
loadingInvitationMutableFlow,
parameters.watchLatestSelectedOnlineAccountId(),
parameters.watchIsBackupEnabled(),
) { iabStatus, authStatus, onlineAccounts, pendingAccountsInvitation, maybeLoadingInvitation, maybeSelectedOnlineAccountId, isBackupEnabled ->
parameters.watchLastBackupDate(),
) { iabStatus, authStatus, onlineAccounts, pendingAccountsInvitation, maybeLoadingInvitation, maybeSelectedOnlineAccountId, isBackupEnabled, lastBackupDate ->
return@combine when(iabStatus) {
PremiumCheckStatus.INITIALIZING,
PremiumCheckStatus.CHECKING -> State.Loading
PremiumCheckStatus.ERROR -> State.IabError
PremiumCheckStatus.NOT_PREMIUM -> State.NotPro(isOfflineBackupEnabled = false)
PremiumCheckStatus.NOT_PREMIUM -> State.NotPro(backupState = OfflineBackupState.Disabled)
PremiumCheckStatus.LEGACY_PREMIUM,
PremiumCheckStatus.PREMIUM_SUBSCRIBED -> State.NotPro(isOfflineBackupEnabled = isBackupEnabled)
PremiumCheckStatus.PREMIUM_SUBSCRIBED -> State.NotPro(
backupState = if (isBackupEnabled) {
OfflineBackupState.Enabled(showLateBackupAlert = isBackupLate(lastBackupDate))
} else {
OfflineBackupState.Disabled
}
)
PremiumCheckStatus.PRO_SUBSCRIBED -> when(authStatus) {
is AuthState.Authenticated -> {
val ownAccounts = onlineAccounts
Expand All @@ -126,12 +137,20 @@ class AccountSelectorViewModel @Inject constructor(
isLoading = maybeLoadingInvitation?.account?.id == account.id,
)
},
isOfflineBackupEnabled = isBackupEnabled,
backupState = if (isBackupEnabled) {
OfflineBackupState.Enabled(showLateBackupAlert = isBackupLate(lastBackupDate))
} else {
OfflineBackupState.Disabled
}
)
}
AuthState.Authenticating -> State.Loading
AuthState.NotAuthenticated -> State.NotAuthenticated(
isOfflineBackupEnabled = isBackupEnabled,
backupState = if (isBackupEnabled) {
OfflineBackupState.Enabled(showLateBackupAlert = isBackupLate(lastBackupDate))
} else {
OfflineBackupState.Disabled
}
)
}
}
Expand Down Expand Up @@ -231,31 +250,48 @@ class AccountSelectorViewModel @Inject constructor(
selected = maybeSelectedOnlineAccountId == id,
)

private fun isBackupLate(lastBackupDate: Date?): Boolean {
if (lastBackupDate == null) {
return false
}

val now = Date()
val diff = now.time - lastBackupDate.time
val diffInDays = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS)

return diffInDays >= 14
}

data class Invitation(
val account: Account,
val user: CurrentUser,
val isLoading: Boolean,
)

sealed interface OfflineBackStateAvailable {
val isOfflineBackupEnabled: Boolean
sealed interface OfflineBackupStateAvailable {
val backupState: OfflineBackupState
}

sealed class OfflineBackupState {
data object Disabled: OfflineBackupState()
data class Enabled(val showLateBackupAlert: Boolean): OfflineBackupState()
}

sealed class State {
data object Loading : State()
data object IabError : State()
data class Error(val cause: Throwable) : State()
data class NotPro(override val isOfflineBackupEnabled: Boolean) : State(), OfflineBackStateAvailable
data class NotAuthenticated(override val isOfflineBackupEnabled: Boolean) : State(), OfflineBackStateAvailable
data class NotPro(override val backupState: OfflineBackupState) : State(), OfflineBackupStateAvailable
data class NotAuthenticated(override val backupState: OfflineBackupState) : State(), OfflineBackupStateAvailable
data class AccountsAvailable(
val userEmail: String,
val isOfflineSelected: Boolean,
val ownAccounts: List<Account>,
val showCreateOnlineAccountButton: Boolean,
val invitedAccounts: List<Account>,
val pendingInvitations: List<Invitation>,
override val isOfflineBackupEnabled: Boolean,
) : State(), OfflineBackStateAvailable
override val backupState: OfflineBackupState,
) : State(), OfflineBackupStateAvailable
}

sealed class Event {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ class BackupSettingsViewModel @Inject constructor(
fun onBackupActivated() {
if (!parameters.isBackupEnabled()) {
parameters.setBackupEnabled(true)
Logger.debug("Backup activated")

viewModelScope.launch {
val maybeBackupActivatedState = withTimeoutOrNull(5.seconds) {
Expand All @@ -178,6 +179,7 @@ class BackupSettingsViewModel @Inject constructor(
fun onBackupDeactivated() {
if (parameters.isBackupEnabled()) {
parameters.setBackupEnabled(false)
Logger.debug("Backup deactivated")
unscheduleBackup(appContext)
}
}
Expand Down
Loading

0 comments on commit bdb6fa0

Please sign in to comment.