Add ability to turn off and delete backups.

This commit is contained in:
Alex Hart
2024-06-18 12:43:47 -03:00
committed by Greyson Parrelli
parent 6659700a1c
commit 5ecf60a306
20 changed files with 239 additions and 152 deletions

View File

@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.backup.v2
import androidx.annotation.WorkerThread
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -41,6 +42,7 @@ import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeature
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.database.DistributionListTables
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
@@ -88,6 +90,7 @@ object BackupRepository {
Log.i(TAG, "Resetting initialized state due to 401.")
SignalStore.backup().backupsInitialized = false
}
403 -> {
Log.i(TAG, "Bad auth credential. Clearing stored credentials.")
SignalStore.backup().clearAllCredentials()
@@ -95,6 +98,13 @@ object BackupRepository {
}
}
@WorkerThread
fun turnOffAndDeleteBackup() {
RecurringInAppPaymentRepository.cancelActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP)
SignalStore.backup().areBackupsEnabled = false
SignalStore.backup().backupTier = null
}
fun export(outputStream: OutputStream, append: (ByteArray) -> Unit, plaintext: Boolean = false) {
val eventTimer = EventTimer()
val writer: BackupExportWriter = if (plaintext) {

View File

@@ -12,18 +12,12 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.util.BottomSheetUtil
class BecomeASustainerFragment : DSLSettingsBottomSheetFragment() {
private val viewModel: BecomeASustainerViewModel by viewModels(
factoryProducer = {
BecomeASustainerViewModel.Factory(RecurringInAppPaymentRepository(AppDependencies.donationsService))
}
)
private val viewModel: BecomeASustainerViewModel by viewModels()
override fun bindAdapter(adapter: DSLSettingsAdapter) {
BadgePreview.register(adapter)

View File

@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.badges.self.none
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.kotlin.subscribeBy
@@ -10,7 +9,7 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.util.livedata.Store
class BecomeASustainerViewModel(subscriptionsRepository: RecurringInAppPaymentRepository) : ViewModel() {
class BecomeASustainerViewModel : ViewModel() {
private val store = Store(BecomeASustainerState())
@@ -19,7 +18,7 @@ class BecomeASustainerViewModel(subscriptionsRepository: RecurringInAppPaymentRe
private val disposables = CompositeDisposable()
init {
disposables += subscriptionsRepository.getSubscriptions().subscribeBy(
disposables += RecurringInAppPaymentRepository.getSubscriptions().subscribeBy(
onError = { Log.w(TAG, "Could not load subscriptions.") },
onSuccess = { subscriptions ->
store.update {
@@ -36,10 +35,4 @@ class BecomeASustainerViewModel(subscriptionsRepository: RecurringInAppPaymentRe
companion object {
private val TAG = Log.tag(BecomeASustainerViewModel::class.java)
}
class Factory(private val subscriptionsRepository: RecurringInAppPaymentRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(BecomeASustainerViewModel(subscriptionsRepository))!!
}
}
}

View File

@@ -13,9 +13,7 @@ import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -31,7 +29,7 @@ class BadgesOverviewFragment : DSLSettingsFragment(
private val lifecycleDisposable = LifecycleDisposable()
private val viewModel: BadgesOverviewViewModel by viewModels(
factoryProducer = {
BadgesOverviewViewModel.Factory(BadgeRepository(requireContext()), RecurringInAppPaymentRepository(AppDependencies.donationsService))
BadgesOverviewViewModel.Factory(BadgeRepository(requireContext()))
}
)

View File

@@ -23,8 +23,7 @@ import java.util.Optional
private val TAG = Log.tag(BadgesOverviewViewModel::class.java)
class BadgesOverviewViewModel(
private val badgeRepository: BadgeRepository,
private val subscriptionsRepository: RecurringInAppPaymentRepository
private val badgeRepository: BadgeRepository
) : ViewModel() {
private val store = Store(BadgesOverviewState())
private val eventSubject = PublishSubject.create<BadgesOverviewEvent>()
@@ -51,8 +50,8 @@ class BadgesOverviewViewModel(
}
disposables += Single.zip(
subscriptionsRepository.getActiveSubscription(InAppPaymentSubscriberRecord.Type.DONATION),
subscriptionsRepository.getSubscriptions()
RecurringInAppPaymentRepository.getActiveSubscription(InAppPaymentSubscriberRecord.Type.DONATION),
RecurringInAppPaymentRepository.getSubscriptions()
) { active, all ->
if (!active.isActive && active.activeSubscription?.willCancelAtPeriodEnd() == true) {
Optional.ofNullable<String>(all.firstOrNull { it.level == active.activeSubscription?.level }?.badge?.id)
@@ -89,11 +88,10 @@ class BadgesOverviewViewModel(
}
class Factory(
private val badgeRepository: BadgeRepository,
private val subscriptionsRepository: RecurringInAppPaymentRepository
private val badgeRepository: BadgeRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(BadgesOverviewViewModel(badgeRepository, subscriptionsRepository)))
return requireNotNull(modelClass.cast(BadgesOverviewViewModel(badgeRepository)))
}
}

View File

@@ -15,9 +15,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.livedata.Store
class AppSettingsViewModel(
recurringInAppPaymentRepository: RecurringInAppPaymentRepository = RecurringInAppPaymentRepository(AppDependencies.donationsService)
) : ViewModel() {
class AppSettingsViewModel : ViewModel() {
private val store = Store(
AppSettingsState(
@@ -40,7 +38,7 @@ class AppSettingsViewModel(
store.update(unreadPaymentsLiveData) { payments, state -> state.copy(unreadPaymentsCount = payments.map { it.unreadCount }.orElse(0)) }
store.update(selfLiveData) { self, state -> state.copy(self = self) }
disposables += recurringInAppPaymentRepository.getActiveSubscription(InAppPaymentSubscriberRecord.Type.DONATION).subscribeBy(
disposables += RecurringInAppPaymentRepository.getActiveSubscription(InAppPaymentSubscriberRecord.Type.DONATION).subscribeBy(
onSuccess = { activeSubscription ->
store.update { state ->
state.copy(allowUserToGoToDonationManagementScreen = activeSubscription.isActive || InAppDonations.hasAtLeastOnePaymentMethodAvailable())

View File

@@ -12,12 +12,17 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
@@ -27,17 +32,19 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import kotlinx.collections.immutable.persistentListOf
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
@@ -83,7 +90,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
@Composable
override fun FragmentContent() {
val state by viewModel.state
val state by viewModel.state.collectAsState()
val callbacks = remember { Callbacks() }
RemoteBackupsSettingsContent(
@@ -142,7 +149,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
}
override fun onTurnOffAndDeleteBackupsConfirm() {
// TODO [alex] CheckoutFlowStartFragment.launchForBackupsCancellation(childFragmentManager)
viewModel.turnOffAndDeleteBackups()
}
override fun onBackupsTypeClick() {
@@ -164,12 +171,6 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
super.onResume()
viewModel.refresh()
}
// override fun onCheckoutFlowResult(result: CheckoutFlowStartFragment.Result) {
// if (result is CheckoutFlowStartFragment.Result.CancelationSuccess) {
// Snackbar.make(requireView(), R.string.SubscribeFragment__your_subscription_has_been_cancelled, Snackbar.LENGTH_LONG).show()
// }
// }
}
/**
@@ -331,6 +332,13 @@ private fun RemoteBackupsSettingsContent(
onDismiss = contentCallbacks::onDialogDismissed
)
}
RemoteBackupsSettingsState.Dialog.DELETING_BACKUP, RemoteBackupsSettingsState.Dialog.BACKUP_DELETED -> {
DeletingBackupDialog(
backupDeleted = requestedDialog == RemoteBackupsSettingsState.Dialog.BACKUP_DELETED,
onDismiss = contentCallbacks::onDialogDismissed
)
}
}
LaunchedEffect(requestedSnackbar) {
@@ -509,6 +517,60 @@ private fun TurnOffAndDeleteBackupsDialog(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DeletingBackupDialog(
backupDeleted: Boolean,
onDismiss: () -> Unit
) {
BasicAlertDialog(
onDismissRequest = onDismiss,
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false
)
) {
Surface(
shape = AlertDialogDefaults.shape,
color = AlertDialogDefaults.containerColor
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.defaultMinSize(minWidth = 232.dp)
.padding(bottom = 60.dp)
) {
if (backupDeleted) {
Icon(
painter = painterResource(id = R.drawable.symbol_check_light_24),
contentDescription = null,
tint = Color(0xFF09B37B),
modifier = Modifier
.padding(top = 58.dp, bottom = 9.dp)
.size(48.dp)
)
Text(
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_deleted),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} else {
CircularProgressIndicator(
modifier = Modifier
.padding(top = 64.dp, bottom = 20.dp)
.size(48.dp)
)
Text(
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__deleting_backup),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun BackupFrequencyDialog(
@@ -642,6 +704,17 @@ private fun TurnOffAndDeleteBackupsDialogPreview() {
}
}
@SignalPreview
@Composable
private fun DeleteBackupDialogPreview() {
Previews.Preview {
DeletingBackupDialog(
backupDeleted = true,
onDismiss = {}
)
}
}
@SignalPreview
@Composable
private fun BackupFrequencyDialogPreview() {

View File

@@ -22,7 +22,9 @@ data class RemoteBackupsSettingsState(
enum class Dialog {
NONE,
TURN_OFF_AND_DELETE_BACKUPS,
BACKUP_FREQUENCY
BACKUP_FREQUENCY,
DELETING_BACKUP,
BACKUP_DELETED
}
enum class Snackbar {

View File

@@ -5,11 +5,16 @@
package org.thoughtcrime.securesms.components.settings.app.chats.backups
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.BackupV2Event
@@ -17,12 +22,13 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.service.MessageBackupListener
import kotlin.time.Duration.Companion.milliseconds
/**
* ViewModel for state management of RemoteBackupsSettingsFragment
*/
class RemoteBackupsSettingsViewModel : ViewModel() {
private val internalState = mutableStateOf(
private val internalState = MutableStateFlow(
RemoteBackupsSettingsState(
messageBackupsType = null,
lastBackupTimestamp = SignalStore.backup().lastBackupTime,
@@ -31,7 +37,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
)
)
val state: State<RemoteBackupsSettingsState> = internalState
val state: StateFlow<RemoteBackupsSettingsState> = internalState
init {
refresh()
@@ -39,22 +45,22 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
fun setCanBackUpUsingCellular(canBackUpUsingCellular: Boolean) {
SignalStore.backup().backupWithCellular = canBackUpUsingCellular
internalState.value = state.value.copy(canBackUpUsingCellular = canBackUpUsingCellular)
internalState.update { it.copy(canBackUpUsingCellular = canBackUpUsingCellular) }
}
fun setBackupsFrequency(backupsFrequency: BackupFrequency) {
SignalStore.backup().backupFrequency = backupsFrequency
internalState.value = state.value.copy(backupsFrequency = backupsFrequency)
internalState.update { it.copy(backupsFrequency = backupsFrequency) }
MessageBackupListener.setNextBackupTimeToIntervalFromNow()
MessageBackupListener.schedule(AppDependencies.application)
}
fun requestDialog(dialog: RemoteBackupsSettingsState.Dialog) {
internalState.value = state.value.copy(dialog = dialog)
internalState.update { it.copy(dialog = dialog) }
}
fun requestSnackbar(snackbar: RemoteBackupsSettingsState.Snackbar) {
internalState.value = state.value.copy(snackbar = snackbar)
internalState.update { it.copy(snackbar = snackbar) }
}
fun refresh() {
@@ -62,35 +68,49 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
val tier = SignalStore.backup().backupTier
val backupType = if (tier != null) BackupRepository.getBackupsType(tier) else null
internalState.value = state.value.copy(
messageBackupsType = backupType,
lastBackupTimestamp = SignalStore.backup().lastBackupTime,
backupSize = SignalStore.backup().totalBackupSize,
backupsFrequency = SignalStore.backup().backupFrequency
)
internalState.update {
it.copy(
messageBackupsType = backupType,
lastBackupTimestamp = SignalStore.backup().lastBackupTime,
backupSize = SignalStore.backup().totalBackupSize,
backupsFrequency = SignalStore.backup().backupFrequency
)
}
}
}
fun turnOffAndDeleteBackups() {
// TODO [message-backups] -- Delete.
SignalStore.backup().areBackupsEnabled = false
internalState.value = state.value.copy(snackbar = RemoteBackupsSettingsState.Snackbar.BACKUP_DELETED_AND_TURNED_OFF)
viewModelScope.launch {
requestDialog(RemoteBackupsSettingsState.Dialog.DELETING_BACKUP)
withContext(Dispatchers.IO) {
BackupRepository.turnOffAndDeleteBackup()
}
if (isActive) {
requestDialog(RemoteBackupsSettingsState.Dialog.BACKUP_DELETED)
delay(2000.milliseconds)
requestDialog(RemoteBackupsSettingsState.Dialog.NONE)
}
}
}
fun updateBackupProgress(backupEvent: BackupV2Event?) {
internalState.value = state.value.copy(backupProgress = backupEvent)
internalState.update { it.copy(backupProgress = backupEvent) }
refreshBackupState()
}
private fun refreshBackupState() {
internalState.value = state.value.copy(
lastBackupTimestamp = SignalStore.backup().lastBackupTime,
backupSize = SignalStore.backup().totalBackupSize
)
internalState.update {
it.copy(
lastBackupTimestamp = SignalStore.backup().lastBackupTime,
backupSize = SignalStore.backup().totalBackupSize
)
}
}
fun onBackupNowClick() {
if (state.value.backupProgress == null || state.value.backupProgress?.type == BackupV2Event.Type.FINISHED) {
if (internalState.value.backupProgress == null || internalState.value.backupProgress?.type == BackupV2Event.Type.FINISHED) {
BackupMessagesJob.enqueue()
}
}

View File

@@ -31,8 +31,6 @@ class PayPalRepository(private val donationsService: DonationsService) {
private val TAG = Log.tag(PayPalRepository::class.java)
}
private val recurringInAppPaymentRepository = RecurringInAppPaymentRepository(donationsService)
fun createOneTimePaymentIntent(
amount: FiatMoney,
badgeRecipient: RecipientId,
@@ -88,7 +86,7 @@ class PayPalRepository(private val donationsService: DonationsService) {
)
}.flatMap { serviceResponse ->
if (retryOn409 && serviceResponse.status == 409) {
recurringInAppPaymentRepository.rotateSubscriberId(subscriberType).andThen(createPaymentMethod(subscriberType, retryOn409 = false))
RecurringInAppPaymentRepository.rotateSubscriberId(subscriberType).andThen(createPaymentMethod(subscriberType, retryOn409 = false))
} else {
serviceResponse.flattenResult()
}

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components.settings.app.subscription
import androidx.annotation.CheckResult
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
@@ -25,7 +26,6 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.subscription.LevelUpdate
import org.thoughtcrime.securesms.subscription.LevelUpdateOperation
import org.thoughtcrime.securesms.subscription.Subscription
import org.whispersystems.signalservice.api.services.DonationsService
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.api.subscriptions.IdempotencyKey
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
@@ -39,7 +39,11 @@ import kotlin.time.Duration.Companion.milliseconds
* Repository which can query for the user's active subscription as well as a list of available subscriptions,
* in the currency indicated.
*/
class RecurringInAppPaymentRepository(private val donationsService: DonationsService) {
object RecurringInAppPaymentRepository {
private val TAG = Log.tag(RecurringInAppPaymentRepository::class.java)
private val donationsService = AppDependencies.donationsService
fun getActiveSubscription(type: InAppPaymentSubscriberRecord.Type): Single<ActiveSubscription> {
val localSubscription = InAppPaymentsRepository.getSubscriber(type)
@@ -129,29 +133,29 @@ class RecurringInAppPaymentRepository(private val donationsService: DonationsSer
}
}
fun cancelActiveSubscription(subscriberType: InAppPaymentSubscriberRecord.Type): Completable {
fun cancelActiveSubscriptionSync(subscriberType: InAppPaymentSubscriberRecord.Type) {
Log.d(TAG, "Canceling active subscription...", true)
return Single
.fromCallable {
val localSubscriber = InAppPaymentsRepository.requireSubscriber(subscriberType)
val localSubscriber = InAppPaymentsRepository.requireSubscriber(subscriberType)
donationsService.cancelSubscription(localSubscriber.subscriberId)
}
val serviceResponse: ServiceResponse<EmptyResponse> = donationsService.cancelSubscription(localSubscriber.subscriberId)
serviceResponse.resultOrThrow
Log.d(TAG, "Cancelled active subscription.", true)
SignalStore.donationsValues().updateLocalStateForManualCancellation(subscriberType)
MultiDeviceSubscriptionSyncRequestJob.enqueue()
InAppPaymentsRepository.scheduleSyncForAccountRecordChange()
}
@CheckResult
fun cancelActiveSubscription(subscriberType: InAppPaymentSubscriberRecord.Type): Completable {
return Completable
.fromAction { cancelActiveSubscriptionSync(subscriberType) }
.subscribeOn(Schedulers.io())
.flatMap(ServiceResponse<EmptyResponse>::flattenResult)
.ignoreElement()
.doOnComplete {
Log.d(TAG, "Cancelled active subscription.", true)
SignalStore.donationsValues().updateLocalStateForManualCancellation(subscriberType)
MultiDeviceSubscriptionSyncRequestJob.enqueue()
InAppPaymentsRepository.scheduleSyncForAccountRecordChange()
}
}
fun cancelActiveSubscriptionIfNecessary(subscriberType: InAppPaymentSubscriberRecord.Type): Completable {
return Single.fromCallable { InAppPaymentsRepository.getShouldCancelSubscriptionBeforeNextSubscribeAttempt(subscriberType) }.flatMapCompletable {
if (it) {
Log.d(TAG, "Cancelling active subscription...", true)
cancelActiveSubscription(subscriberType).doOnComplete {
SignalStore.donationsValues().updateLocalStateForManualCancellation(subscriberType)
MultiDeviceSubscriptionSyncRequestJob.enqueue()
@@ -250,27 +254,23 @@ class RecurringInAppPaymentRepository(private val donationsService: DonationsSer
getOrCreateLevelUpdateOperation(TAG, subscriptionLevel)
}
companion object {
private val TAG = Log.tag(RecurringInAppPaymentRepository::class.java)
fun getOrCreateLevelUpdateOperation(tag: String, subscriptionLevel: String): LevelUpdateOperation {
Log.d(tag, "Retrieving level update operation for $subscriptionLevel")
val levelUpdateOperation = SignalStore.donationsValues().getLevelOperation(subscriptionLevel)
return if (levelUpdateOperation == null) {
val newOperation = LevelUpdateOperation(
idempotencyKey = IdempotencyKey.generate(),
level = subscriptionLevel
)
fun getOrCreateLevelUpdateOperation(tag: String, subscriptionLevel: String): LevelUpdateOperation {
Log.d(tag, "Retrieving level update operation for $subscriptionLevel")
val levelUpdateOperation = SignalStore.donationsValues().getLevelOperation(subscriptionLevel)
return if (levelUpdateOperation == null) {
val newOperation = LevelUpdateOperation(
idempotencyKey = IdempotencyKey.generate(),
level = subscriptionLevel
)
SignalStore.donationsValues().setLevelOperation(newOperation)
LevelUpdate.updateProcessingState(true)
Log.d(tag, "Created a new operation for $subscriptionLevel")
newOperation
} else {
LevelUpdate.updateProcessingState(true)
Log.d(tag, "Reusing operation for $subscriptionLevel")
levelUpdateOperation
}
SignalStore.donationsValues().setLevelOperation(newOperation)
LevelUpdate.updateProcessingState(true)
Log.d(tag, "Created a new operation for $subscriptionLevel")
newOperation
} else {
LevelUpdate.updateProcessingState(true)
Log.d(tag, "Reusing operation for $subscriptionLevel")
levelUpdateOperation
}
}

View File

@@ -51,7 +51,6 @@ class StripeRepository(
private val googlePayApi = GooglePayApi(activity, StripeApi.Gateway(Environment.Donations.STRIPE_CONFIGURATION), Environment.Donations.GOOGLE_PAY_CONFIGURATION)
private val stripeApi = StripeApi(Environment.Donations.STRIPE_CONFIGURATION, this, this, AppDependencies.okHttpClient)
private val recurringInAppPaymentRepository = RecurringInAppPaymentRepository(AppDependencies.donationsService)
fun isGooglePayAvailable(): Completable {
return googlePayApi.queryIsReadyToPay()
@@ -169,7 +168,7 @@ class StripeRepository(
}
.flatMap { serviceResponse ->
if (retryOn409 && serviceResponse.status == 409) {
recurringInAppPaymentRepository.rotateSubscriberId(subscriberType).andThen(createPaymentMethod(subscriberType, paymentSourceType, retryOn409 = false))
RecurringInAppPaymentRepository.rotateSubscriberId(subscriberType).andThen(createPaymentMethod(subscriberType, paymentSourceType, retryOn409 = false))
} else {
serviceResponse.flattenResult()
}

View File

@@ -52,7 +52,6 @@ import java.util.Optional
*/
class DonateToSignalViewModel(
startType: InAppPaymentType,
private val subscriptionsRepository: RecurringInAppPaymentRepository,
private val oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository
) : ViewModel() {
@@ -75,7 +74,7 @@ class DonateToSignalViewModel(
init {
initializeOneTimeDonationState(oneTimeInAppPaymentRepository)
initializeMonthlyDonationState(subscriptionsRepository)
initializeMonthlyDonationState(RecurringInAppPaymentRepository)
networkDisposable += InternetConnectionObserver
.observe()
@@ -91,7 +90,7 @@ class DonateToSignalViewModel(
fun retryMonthlyDonationState() {
if (!monthlyDonationDisposables.isDisposed && store.state.monthlyDonationState.donationStage == DonateToSignalState.DonationStage.FAILURE) {
store.update { it.copy(monthlyDonationState = it.monthlyDonationState.copy(donationStage = DonateToSignalState.DonationStage.INIT)) }
initializeMonthlyDonationState(subscriptionsRepository)
initializeMonthlyDonationState(RecurringInAppPaymentRepository)
}
}
@@ -181,7 +180,7 @@ class DonateToSignalViewModel(
}
fun refreshActiveSubscription() {
subscriptionsRepository
RecurringInAppPaymentRepository
.getActiveSubscription(InAppPaymentSubscriberRecord.Type.DONATION)
.subscribeBy(
onSuccess = {
@@ -395,7 +394,7 @@ class DonateToSignalViewModel(
val usd = PlatformCurrencyUtil.USD
val newSubscriber = InAppPaymentsRepository.getSubscriber(usd, InAppPaymentSubscriberRecord.Type.DONATION) ?: InAppPaymentSubscriberRecord(SubscriberId.generate(), usd, InAppPaymentSubscriberRecord.Type.DONATION, false, InAppPaymentData.PaymentMethodType.UNKNOWN)
InAppPaymentsRepository.setSubscriber(newSubscriber)
subscriptionsRepository.syncAccountRecord().subscribe()
RecurringInAppPaymentRepository.syncAccountRecord().subscribe()
}
}
},
@@ -422,11 +421,10 @@ class DonateToSignalViewModel(
class Factory(
private val startType: InAppPaymentType,
private val subscriptionsRepository: RecurringInAppPaymentRepository = RecurringInAppPaymentRepository(AppDependencies.donationsService),
private val oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository = OneTimeInAppPaymentRepository(AppDependencies.donationsService)
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(DonateToSignalViewModel(startType, subscriptionsRepository, oneTimeInAppPaymentRepository)) as T
return modelClass.cast(DonateToSignalViewModel(startType, oneTimeInAppPaymentRepository)) as T
}
}
}

View File

@@ -36,7 +36,6 @@ import org.whispersystems.signalservice.api.util.Preconditions
class PayPalPaymentInProgressViewModel(
private val payPalRepository: PayPalRepository,
private val recurringInAppPaymentRepository: RecurringInAppPaymentRepository,
private val oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository
) : ViewModel() {
@@ -86,7 +85,7 @@ class PayPalPaymentInProgressViewModel(
Log.d(TAG, "Beginning subscription update...", true)
store.update { DonationProcessorStage.PAYMENT_PIPELINE }
disposables += recurringInAppPaymentRepository.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType()).andThen(recurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, PaymentSourceType.PayPal))
disposables += RecurringInAppPaymentRepository.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType()).andThen(RecurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, PaymentSourceType.PayPal))
.subscribeBy(
onComplete = {
Log.w(TAG, "Completed subscription update", true)
@@ -104,12 +103,12 @@ class PayPalPaymentInProgressViewModel(
Log.d(TAG, "Beginning cancellation...", true)
store.update { DonationProcessorStage.CANCELLING }
disposables += recurringInAppPaymentRepository.cancelActiveSubscription(subscriberType).subscribeBy(
disposables += RecurringInAppPaymentRepository.cancelActiveSubscription(subscriberType).subscribeBy(
onComplete = {
Log.d(TAG, "Cancellation succeeded", true)
SignalStore.donationsValues().updateLocalStateForManualCancellation(subscriberType)
MultiDeviceSubscriptionSyncRequestJob.enqueue()
recurringInAppPaymentRepository.syncAccountRecord().subscribe()
RecurringInAppPaymentRepository.syncAccountRecord().subscribe()
store.update { DonationProcessorStage.COMPLETE }
},
onError = { throwable ->
@@ -172,14 +171,14 @@ class PayPalPaymentInProgressViewModel(
private fun proceedMonthly(inAppPayment: InAppPaymentTable.InAppPayment, routeToPaypalConfirmation: (PayPalCreatePaymentMethodResponse) -> Single<PayPalPaymentMethodId>) {
Log.d(TAG, "Proceeding with monthly payment pipeline for InAppPayment::${inAppPayment.id} of type ${inAppPayment.type}...", true)
val setup = recurringInAppPaymentRepository.ensureSubscriberId(inAppPayment.type.requireSubscriberType())
.andThen(recurringInAppPaymentRepository.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType()))
val setup = RecurringInAppPaymentRepository.ensureSubscriberId(inAppPayment.type.requireSubscriberType())
.andThen(RecurringInAppPaymentRepository.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType()))
.andThen(payPalRepository.createPaymentMethod(inAppPayment.type.requireSubscriberType()))
.flatMap(routeToPaypalConfirmation)
.flatMapCompletable { payPalRepository.setDefaultPaymentMethod(inAppPayment.type.requireSubscriberType(), it.paymentId) }
.onErrorResumeNext { Completable.error(DonationError.getPaymentSetupError(DonationErrorSource.MONTHLY, it, PaymentSourceType.PayPal)) }
disposables += setup.andThen(recurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, PaymentSourceType.PayPal))
disposables += setup.andThen(RecurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, PaymentSourceType.PayPal))
.subscribeBy(
onError = { throwable ->
Log.w(TAG, "Failure in monthly payment pipeline...", throwable, true)
@@ -195,11 +194,10 @@ class PayPalPaymentInProgressViewModel(
class Factory(
private val payPalRepository: PayPalRepository = PayPalRepository(AppDependencies.donationsService),
private val recurringInAppPaymentRepository: RecurringInAppPaymentRepository = RecurringInAppPaymentRepository(AppDependencies.donationsService),
private val oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository = OneTimeInAppPaymentRepository(AppDependencies.donationsService)
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(PayPalPaymentInProgressViewModel(payPalRepository, recurringInAppPaymentRepository, oneTimeInAppPaymentRepository)) as T
return modelClass.cast(PayPalPaymentInProgressViewModel(payPalRepository, oneTimeInAppPaymentRepository)) as T
}
}
}

View File

@@ -41,7 +41,6 @@ import org.whispersystems.signalservice.internal.push.exceptions.DonationProcess
class StripePaymentInProgressViewModel(
private val stripeRepository: StripeRepository,
private val recurringInAppPaymentRepository: RecurringInAppPaymentRepository,
private val oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository
) : ViewModel() {
@@ -144,18 +143,18 @@ class StripePaymentInProgressViewModel(
}
private fun proceedMonthly(inAppPayment: InAppPaymentTable.InAppPayment, paymentSourceProvider: PaymentSourceProvider, nextActionHandler: StripeNextActionHandler) {
val ensureSubscriberId: Completable = recurringInAppPaymentRepository.ensureSubscriberId(inAppPayment.type.requireSubscriberType())
val ensureSubscriberId: Completable = RecurringInAppPaymentRepository.ensureSubscriberId(inAppPayment.type.requireSubscriberType())
val createAndConfirmSetupIntent: Single<StripeApi.Secure3DSAction> = paymentSourceProvider.paymentSource.flatMap {
stripeRepository.createAndConfirmSetupIntent(inAppPayment.type, it, paymentSourceProvider.paymentSourceType as PaymentSourceType.Stripe)
}
val setLevel: Completable = recurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, paymentSourceProvider.paymentSourceType)
val setLevel: Completable = RecurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, paymentSourceProvider.paymentSourceType)
Log.d(TAG, "Starting subscription payment pipeline...", true)
store.update { DonationProcessorStage.PAYMENT_PIPELINE }
val setup: Completable = ensureSubscriberId
.andThen(recurringInAppPaymentRepository.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType()))
.andThen(RecurringInAppPaymentRepository.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType()))
.andThen(createAndConfirmSetupIntent)
.flatMap { secure3DSAction ->
nextActionHandler.handle(
@@ -255,7 +254,7 @@ class StripePaymentInProgressViewModel(
Log.d(TAG, "Beginning cancellation...", true)
store.update { DonationProcessorStage.CANCELLING }
disposables += recurringInAppPaymentRepository.cancelActiveSubscription(subscriberType).subscribeBy(
disposables += RecurringInAppPaymentRepository.cancelActiveSubscription(subscriberType).subscribeBy(
onComplete = {
Log.d(TAG, "Cancellation succeeded", true)
store.update { DonationProcessorStage.COMPLETE }
@@ -270,10 +269,10 @@ class StripePaymentInProgressViewModel(
fun updateSubscription(inAppPayment: InAppPaymentTable.InAppPayment) {
Log.d(TAG, "Beginning subscription update...", true)
store.update { DonationProcessorStage.PAYMENT_PIPELINE }
disposables += recurringInAppPaymentRepository
disposables += RecurringInAppPaymentRepository
.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType())
.andThen(recurringInAppPaymentRepository.getPaymentSourceTypeOfLatestSubscription(inAppPayment.type.requireSubscriberType()))
.flatMapCompletable { paymentSourceType -> recurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, paymentSourceType) }
.andThen(RecurringInAppPaymentRepository.getPaymentSourceTypeOfLatestSubscription(inAppPayment.type.requireSubscriberType()))
.flatMapCompletable { paymentSourceType -> RecurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, paymentSourceType) }
.subscribeBy(
onComplete = {
Log.w(TAG, "Completed subscription update", true)
@@ -304,11 +303,10 @@ class StripePaymentInProgressViewModel(
class Factory(
private val stripeRepository: StripeRepository,
private val recurringInAppPaymentRepository: RecurringInAppPaymentRepository = RecurringInAppPaymentRepository(AppDependencies.donationsService),
private val oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository = OneTimeInAppPaymentRepository(AppDependencies.donationsService)
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(StripePaymentInProgressViewModel(stripeRepository, recurringInAppPaymentRepository, oneTimeInAppPaymentRepository)) as T
return modelClass.cast(StripePaymentInProgressViewModel(stripeRepository, oneTimeInAppPaymentRepository)) as T
}
}
}

View File

@@ -20,14 +20,12 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.TerminalDonationDelegate
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.help.HelpFragment
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
@@ -66,11 +64,7 @@ class ManageDonationsFragment :
)
}
private val viewModel: ManageDonationsViewModel by viewModels(
factoryProducer = {
ManageDonationsViewModel.Factory(RecurringInAppPaymentRepository(AppDependencies.donationsService))
}
)
private val viewModel: ManageDonationsViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycle.addObserver(TerminalDonationDelegate(childFragmentManager, viewLifecycleOwner))

View File

@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.manage
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
@@ -25,9 +24,7 @@ import org.thoughtcrime.securesms.util.livedata.Store
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import java.util.Optional
class ManageDonationsViewModel(
private val subscriptionsRepository: RecurringInAppPaymentRepository
) : ViewModel() {
class ManageDonationsViewModel : ViewModel() {
private val store = Store(ManageDonationsState())
private val disposables = CompositeDisposable()
@@ -65,7 +62,7 @@ class ManageDonationsViewModel(
disposables.clear()
val levelUpdateOperationEdges: Observable<Boolean> = LevelUpdate.isProcessing.distinctUntilChanged()
val activeSubscription: Single<ActiveSubscription> = subscriptionsRepository.getActiveSubscription(InAppPaymentSubscriberRecord.Type.DONATION)
val activeSubscription: Single<ActiveSubscription> = RecurringInAppPaymentRepository.getActiveSubscription(InAppPaymentSubscriberRecord.Type.DONATION)
disposables += Single.fromCallable {
InAppPaymentsRepository.getShouldCancelSubscriptionBeforeNextSubscribeAttempt(InAppPaymentSubscriberRecord.Type.DONATION)
@@ -134,7 +131,7 @@ class ManageDonationsViewModel(
}
)
disposables += subscriptionsRepository.getSubscriptions().subscribeBy(
disposables += RecurringInAppPaymentRepository.getSubscriptions().subscribeBy(
onSuccess = { subs ->
store.update { it.copy(availableSubscriptions = subs) }
},
@@ -155,14 +152,6 @@ class ManageDonationsViewModel(
}
}
class Factory(
private val subscriptionsRepository: RecurringInAppPaymentRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(ManageDonationsViewModel(subscriptionsRepository))!!
}
}
companion object {
private val TAG = Log.tag(ManageDonationsViewModel::class.java)
}

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M18.9 5.37c0.35 0.22 0.46 0.69 0.23 1.03l-8.32 13c-0.13 0.2-0.35 0.34-0.6 0.35-0.24 0.01-0.47-0.1-0.62-0.29L4.9 13.48c-0.26-0.32-0.2-0.8 0.13-1.05 0.32-0.26 0.8-0.2 1.05 0.13l4.03 5.14 7.75-12.1c0.22-0.35 0.69-0.45 1.03-0.23Z"/>
</vector>

View File

@@ -7031,5 +7031,11 @@
<!-- Educational bottom sheet confirm/dismiss button text shown to notify about delete syncs causing deletes to happen across all devices -->
<string name="DeleteSyncEducation_acknowledge_button">OK</string>
<!-- RemoteBackupsSettingsFragment -->
<!-- Text on dialog while user backup is being deleted -->
<string name="RemoteBackupsSettingsFragment__deleting_backup">Deleting backup…</string>
<!-- Text on dialog when user backup is deleted -->
<string name="RemoteBackupsSettingsFragment__backup_deleted">Backup deleted</string>
<!-- EOF -->
</resources>

View File

@@ -84,6 +84,18 @@ public final class ServiceResponse<Result> {
}
}
public Result getResultOrThrow() throws Throwable {
if (result.isPresent()) {
return result.get();
} else if (applicationError.isPresent()) {
throw applicationError.get();
} else if (executionError.isPresent()) {
throw executionError.get();
} else {
throw new AssertionError("Should never get here");
}
}
public static <T> ServiceResponse<T> forResult(T result, WebsocketResponse response) {
return new ServiceResponse<>(result, response);
}