Rewrite in-app-payment flows to prepare for backups support.

This commit is contained in:
Alex Hart
2024-04-19 17:04:15 -03:00
committed by Cody Henthorne
parent b36b00a11c
commit d719edf104
123 changed files with 5429 additions and 1586 deletions

View File

@@ -23,16 +23,13 @@ import org.thoughtcrime.securesms.components.emoji.MediaKeyboard
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.donate.DonateToSignalType
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.conversation.preferences.RecipientPreference
import org.thoughtcrime.securesms.components.settings.models.TextInput
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.keyboard.KeyboardPage
import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment
@@ -41,6 +38,7 @@ import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.Debouncer
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import java.util.Optional
/**
* Allows the user to confirm details about a gift, add a message, and finally make a payment.
@@ -85,7 +83,11 @@ class GiftFlowConfirmationFragment :
keyboardPagerViewModel.setOnlyPage(KeyboardPage.EMOJI)
donationCheckoutDelegate = DonationCheckoutDelegate(this, this, viewModel.uiSessionKey, DonationErrorSource.GIFT)
donationCheckoutDelegate = DonationCheckoutDelegate(
this,
this,
viewModel.state.mapOptional { Optional.ofNullable(it.inAppPaymentId) }.distinctUntilChanged()
)
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
.setView(R.layout.processing_payment_dialog)
@@ -104,23 +106,13 @@ class GiftFlowConfirmationFragment :
val continueButton = requireView().findViewById<MaterialButton>(R.id.continue_button)
continueButton.setOnClickListener {
findNavController().safeNavigate(
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToGatewaySelectorBottomSheet(
with(viewModel.snapshot) {
GatewayRequest(
uiSessionKey = viewModel.uiSessionKey,
donateToSignalType = DonateToSignalType.GIFT,
badge = giftBadge!!,
label = getString(R.string.preferences__one_time),
price = giftPrices[currency]!!.amount,
currencyCode = currency.currencyCode,
level = giftLevel!!,
recipientId = recipient!!.id,
additionalMessage = additionalMessage?.toString()
)
}
lifecycleDisposable += viewModel.insertInAppPayment(requireContext()).subscribe { inAppPayment ->
findNavController().safeNavigate(
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToGatewaySelectorBottomSheet(
inAppPayment
)
)
)
}
}
val textInput = requireView().findViewById<FrameLayout>(R.id.text_input)
@@ -253,27 +245,27 @@ class GiftFlowConfirmationFragment :
}
}
override fun navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest) {
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, gatewayRequest))
override fun navigateToStripePaymentInProgress(inAppPayment: InAppPaymentTable.InAppPayment) {
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, inAppPayment, inAppPayment.type))
}
override fun navigateToPayPalPaymentInProgress(gatewayRequest: GatewayRequest) {
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToPaypalPaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, gatewayRequest))
override fun navigateToPayPalPaymentInProgress(inAppPayment: InAppPaymentTable.InAppPayment) {
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToPaypalPaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, inAppPayment, inAppPayment.type))
}
override fun navigateToCreditCardForm(gatewayRequest: GatewayRequest) {
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToCreditCardFragment(gatewayRequest))
override fun navigateToCreditCardForm(inAppPayment: InAppPaymentTable.InAppPayment) {
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToCreditCardFragment(inAppPayment))
}
override fun navigateToIdealDetailsFragment(gatewayRequest: GatewayRequest) {
override fun navigateToIdealDetailsFragment(inAppPayment: InAppPaymentTable.InAppPayment) {
error("Unsupported operation")
}
override fun navigateToBankTransferMandate(gatewayResponse: GatewayResponse) {
override fun navigateToBankTransferMandate(inAppPayment: InAppPaymentTable.InAppPayment) {
error("Unsupported operation")
}
override fun onPaymentComplete(gatewayRequest: GatewayRequest) {
override fun onPaymentComplete(inAppPayment: InAppPaymentTable.InAppPayment) {
val mainActivityIntent = MainActivity.clearTop(requireContext())
lifecycleDisposable += ConversationIntents
@@ -291,5 +283,5 @@ class GiftFlowConfirmationFragment :
override fun onUserLaunchedAnExternalApplication() = Unit
override fun navigateToDonationPending(gatewayRequest: GatewayRequest) = error("Unsupported operation")
override fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment) = error("Unsupported operation")
}

View File

@@ -1,12 +1,20 @@
package org.thoughtcrime.securesms.badges.gifts.flow
import android.content.Context
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatValue
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.getGiftBadgeAmounts
import org.thoughtcrime.securesms.components.settings.app.subscription.getGiftBadges
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import java.util.Currency
@@ -21,6 +29,25 @@ class GiftFlowRepository {
private val TAG = Log.tag(GiftFlowRepository::class.java)
}
fun insertInAppPayment(context: Context, giftSnapshot: GiftFlowState): Single<InAppPaymentTable.InAppPayment> {
return Single.fromCallable {
SignalDatabase.inAppPayments.insert(
type = InAppPaymentTable.Type.ONE_TIME_GIFT,
state = InAppPaymentTable.State.CREATED,
subscriberId = null,
endOfPeriod = null,
inAppPaymentData = InAppPaymentData(
badge = Badges.toDatabaseBadge(giftSnapshot.giftBadge!!),
label = context.getString(R.string.preferences__one_time),
amount = giftSnapshot.giftPrices[giftSnapshot.currency]!!.toFiatValue(),
level = giftSnapshot.giftLevel!!,
recipientId = giftSnapshot.recipient!!.id.serialize(),
additionalMessage = giftSnapshot.additionalMessage?.toString()
)
)
}.flatMap { InAppPaymentsRepository.requireInAppPayment(it) }.subscribeOn(Schedulers.io())
}
fun getGiftBadge(): Single<Pair<Int, Badge>> {
return Single
.fromCallable {

View File

@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.models.Ne
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
import org.thoughtcrime.securesms.components.settings.models.SplashImage
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -91,7 +92,7 @@ class GiftFlowStartFragment : DSLSettingsFragment(
selectedCurrency = state.currency,
isEnabled = state.stage == GiftFlowState.Stage.READY,
onClick = {
val action = GiftFlowStartFragmentDirections.actionGiftFlowStartFragmentToSetCurrencyFragment(true, viewModel.getSupportedCurrencyCodes().toTypedArray())
val action = GiftFlowStartFragmentDirections.actionGiftFlowStartFragmentToSetCurrencyFragment(InAppPaymentTable.Type.ONE_TIME_GIFT, viewModel.getSupportedCurrencyCodes().toTypedArray())
findNavController().safeNavigate(action)
}
)

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.badges.gifts.flow
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.recipients.Recipient
import java.util.Currency
@@ -9,6 +10,7 @@ import java.util.Currency
* State maintained by the GiftFlowViewModel
*/
data class GiftFlowState(
val inAppPaymentId: InAppPaymentTable.InAppPaymentId? = null,
val currency: Currency,
val giftLevel: Long? = null,
val giftBadge: Badge? = null,

View File

@@ -1,9 +1,12 @@
package org.thoughtcrime.securesms.badges.gifts.flow
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.kotlin.plusAssign
@@ -14,6 +17,7 @@ import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.InternetConnectionObserver
@@ -39,7 +43,6 @@ class GiftFlowViewModel(
val state: Flowable<GiftFlowState> = store.stateFlowable
val events: Observable<DonationEvent> = eventPublisher
val snapshot: GiftFlowState get() = store.state
val uiSessionKey: Long = System.currentTimeMillis()
init {
refresh()
@@ -101,6 +104,15 @@ class GiftFlowViewModel(
)
}
fun insertInAppPayment(context: Context): Single<InAppPaymentTable.InAppPayment> {
val giftSnapshot = snapshot
return giftFlowRepository.insertInAppPayment(context, giftSnapshot)
.doOnSuccess { inAppPayment ->
store.update { it.copy(inAppPaymentId = inAppPayment.id) }
}
.observeOn(AndroidSchedulers.mainThread())
}
override fun onCleared() {
disposables.clear()
}

View File

@@ -10,14 +10,19 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.badges.BadgeRepository
import org.thoughtcrime.securesms.badges.gifts.viewgift.ViewGiftRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError.BadgeRedemptionError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.jobmanager.JobTracker
import org.thoughtcrime.securesms.jobs.DonationReceiptRedemptionJob
import org.thoughtcrime.securesms.database.DatabaseObserver.MessageObserver
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.InAppPaymentRedemptionJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.requireGiftBadge
import org.thoughtcrime.securesms.util.rx.RxStore
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class ViewReceivedGiftViewModel(
@@ -98,44 +103,22 @@ class ViewReceivedGiftViewModel(
}
private fun awaitRedemptionCompletion(setAsPrimary: Boolean): Completable {
return Completable.create {
Log.i(TAG, "Enqueuing gift redemption and awaiting result...", true)
var finalJobState: JobTracker.JobState? = null
val countDownLatch = CountDownLatch(1)
DonationReceiptRedemptionJob.createJobChainForGift(messageId, setAsPrimary).enqueue { _, state ->
if (state.isComplete) {
finalJobState = state
countDownLatch.countDown()
return Completable.create { emitter ->
val messageObserver = MessageObserver { messageId ->
val message = SignalDatabase.messages.getMessageRecord(messageId.id)
when (message.requireGiftBadge().redemptionState) {
GiftBadge.RedemptionState.REDEEMED -> emitter.onComplete()
GiftBadge.RedemptionState.FAILED -> emitter.onError(DonationError.genericBadgeRedemptionFailure(DonationErrorSource.GIFT_REDEMPTION))
else -> Unit
}
}
try {
if (countDownLatch.await(10, TimeUnit.SECONDS)) {
when (finalJobState) {
JobTracker.JobState.SUCCESS -> {
Log.d(TAG, "Gift redemption job chain succeeded.", true)
it.onComplete()
}
JobTracker.JobState.FAILURE -> {
Log.d(TAG, "Gift redemption job chain failed permanently.", true)
it.onError(DonationError.genericBadgeRedemptionFailure(DonationErrorSource.GIFT_REDEMPTION))
}
else -> {
Log.w(TAG, "Gift redemption job chain ignored due to in-progress jobs.", true)
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION))
}
}
} else {
Log.w(TAG, "Timeout awaiting for gift token redemption and profile refresh", true)
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION))
}
} catch (e: InterruptedException) {
Log.w(TAG, "Interrupted awaiting for gift token redemption and profile refresh", true)
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION))
ApplicationDependencies.getJobManager().add(InAppPaymentRedemptionJob.create(MessageId(messageId), setAsPrimary))
ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(messageObserver)
emitter.setCancellable {
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageObserver)
}
}
}.timeout(10, TimeUnit.SECONDS, Completable.error(BadgeRedemptionError.TimeoutWaitingForTokenError(DonationErrorSource.GIFT_REDEMPTION)))
}
class Factory(

View File

@@ -16,6 +16,8 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
@@ -27,47 +29,68 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Texts
import org.signal.core.ui.theme.SignalTheme
import org.signal.donations.StripeDeclineCode
import org.signal.donations.StripeFailureCode
import org.signal.core.util.getParcelableCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImage112
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.mapToErrorStringResource
import org.thoughtcrime.securesms.components.settings.app.subscription.manage.ManageDonationsFragment
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.SpanUtil
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.thoughtcrime.securesms.util.viewModel
class MonthlyDonationCanceledBottomSheetDialogFragment : ComposeBottomSheetDialogFragment() {
companion object {
private const val ARG_IN_APP_PAYMENT_ID = "arg.inAppPaymentId"
@JvmStatic
@JvmOverloads
fun show(fragmentManager: FragmentManager, inAppPaymentId: InAppPaymentTable.InAppPaymentId? = null) {
val fragment = MonthlyDonationCanceledBottomSheetDialogFragment()
fragment.arguments = bundleOf(
ARG_IN_APP_PAYMENT_ID to inAppPaymentId
)
fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
override val peekHeightPercentage: Float = 1f
private val viewModel by viewModel {
MonthlyDonationCanceledViewModel(arguments?.getParcelableCompat(ARG_IN_APP_PAYMENT_ID, InAppPaymentTable.InAppPaymentId::class.java))
}
@Composable
override fun SheetContent() {
val chargeFailure: ActiveSubscription.ChargeFailure? = SignalStore.donationsValues().getUnexpectedSubscriptionCancelationChargeFailure()
val declineCode: StripeDeclineCode = StripeDeclineCode.getFromCode(chargeFailure?.outcomeNetworkReason)
val failureCode: StripeFailureCode = StripeFailureCode.getFromCode(chargeFailure?.code)
val state by viewModel.state
val errorMessage = if (declineCode.isKnown()) {
declineCode.mapToErrorStringResource()
} else if (failureCode.isKnown) {
failureCode.mapToErrorStringResource()
} else {
declineCode.mapToErrorStringResource()
if (state.loadState == MonthlyDonationCanceledState.LoadState.LOADING) {
return
}
if (state.loadState == MonthlyDonationCanceledState.LoadState.FAILED) {
LaunchedEffect(Unit) {
dismissAllowingStateLoss()
}
return
}
MonthlyDonationCanceled(
badge = SignalStore.donationsValues().getExpiredBadge(),
errorMessageRes = errorMessage,
badge = state.badge,
errorMessageRes = state.errorMessage,
onRenewClicked = {
startActivity(AppSettingsActivity.subscriptions(requireContext()))
dismissAllowingStateLoss()
@@ -78,15 +101,6 @@ class MonthlyDonationCanceledBottomSheetDialogFragment : ComposeBottomSheetDialo
}
)
}
companion object {
@JvmStatic
fun show(fragmentManager: FragmentManager) {
val fragment = MonthlyDonationCanceledBottomSheetDialogFragment()
fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
}
@Preview(name = "Light Theme", group = "ShortName", uiMode = Configuration.UI_MODE_NIGHT_NO)

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.badges.self.expired
import androidx.annotation.StringRes
import org.thoughtcrime.securesms.badges.models.Badge
data class MonthlyDonationCanceledState(
val loadState: LoadState = LoadState.LOADING,
val badge: Badge? = null,
@StringRes val errorMessage: Int = -1
) {
enum class LoadState {
LOADING,
READY,
FAILED
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.badges.self.expired
import androidx.annotation.StringRes
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.reactivex.rxjava3.kotlin.plusAssign
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.donations.StripeDeclineCode
import org.signal.donations.StripeFailureCode
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toActiveSubscriptionChargeFailure
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.mapToErrorStringResource
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription.ChargeFailure
class MonthlyDonationCanceledViewModel(
inAppPaymentId: InAppPaymentTable.InAppPaymentId?
) : ViewModel() {
private val internalState = mutableStateOf(MonthlyDonationCanceledState())
val state: State<MonthlyDonationCanceledState> = internalState
init {
if (inAppPaymentId != null) {
initializeFromInAppPaymentId(inAppPaymentId)
} else {
initializeFromSignalStore()
}
}
private fun initializeFromInAppPaymentId(inAppPaymentId: InAppPaymentTable.InAppPaymentId) {
viewModelScope.launch {
val inAppPayment = withContext(Dispatchers.IO) {
SignalDatabase.inAppPayments.getById(inAppPaymentId)
}
if (inAppPayment != null) {
internalState.value = MonthlyDonationCanceledState(
loadState = MonthlyDonationCanceledState.LoadState.READY,
badge = Badges.fromDatabaseBadge(inAppPayment.data.badge!!),
errorMessage = getErrorMessage(inAppPayment.data.cancellation?.chargeFailure?.toActiveSubscriptionChargeFailure())
)
} else {
internalState.value = internalState.value.copy(loadState = MonthlyDonationCanceledState.LoadState.FAILED)
}
}
}
private fun initializeFromSignalStore() {
internalState.value = MonthlyDonationCanceledState(
loadState = MonthlyDonationCanceledState.LoadState.READY,
badge = SignalStore.donationsValues().getExpiredBadge(),
errorMessage = getErrorMessage(SignalStore.donationsValues().getUnexpectedSubscriptionCancelationChargeFailure())
)
}
@StringRes
private fun getErrorMessage(chargeFailure: ChargeFailure?): Int {
val declineCode: StripeDeclineCode = StripeDeclineCode.getFromCode(chargeFailure?.outcomeNetworkReason)
val failureCode: StripeFailureCode = StripeFailureCode.getFromCode(chargeFailure?.code)
return if (declineCode.isKnown()) {
declineCode.mapToErrorStringResource()
} else if (failureCode.isKnown) {
failureCode.mapToErrorStringResource()
} else {
declineCode.mapToErrorStringResource()
}
}
}

View File

@@ -12,7 +12,7 @@ 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.MonthlyDonationRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.util.BottomSheetUtil
@@ -21,7 +21,7 @@ class BecomeASustainerFragment : DSLSettingsBottomSheetFragment() {
private val viewModel: BecomeASustainerViewModel by viewModels(
factoryProducer = {
BecomeASustainerViewModel.Factory(MonthlyDonationRepository(ApplicationDependencies.getDonationsService()))
BecomeASustainerViewModel.Factory(RecurringInAppPaymentRepository(ApplicationDependencies.getDonationsService()))
}
)

View File

@@ -7,10 +7,10 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.util.livedata.Store
class BecomeASustainerViewModel(subscriptionsRepository: MonthlyDonationRepository) : ViewModel() {
class BecomeASustainerViewModel(subscriptionsRepository: RecurringInAppPaymentRepository) : ViewModel() {
private val store = Store(BecomeASustainerState())
@@ -37,7 +37,7 @@ class BecomeASustainerViewModel(subscriptionsRepository: MonthlyDonationReposito
private val TAG = Log.tag(BecomeASustainerViewModel::class.java)
}
class Factory(private val subscriptionsRepository: MonthlyDonationRepository) : ViewModelProvider.Factory {
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,7 +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.MonthlyDonationRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient
@@ -31,7 +31,7 @@ class BadgesOverviewFragment : DSLSettingsFragment(
private val lifecycleDisposable = LifecycleDisposable()
private val viewModel: BadgesOverviewViewModel by viewModels(
factoryProducer = {
BadgesOverviewViewModel.Factory(BadgeRepository(requireContext()), MonthlyDonationRepository(ApplicationDependencies.getDonationsService()))
BadgesOverviewViewModel.Factory(BadgeRepository(requireContext()), RecurringInAppPaymentRepository(ApplicationDependencies.getDonationsService()))
}
)

View File

@@ -12,7 +12,8 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.subjects.PublishSubject
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.badges.BadgeRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.InternetConnectionObserver
@@ -23,7 +24,7 @@ private val TAG = Log.tag(BadgesOverviewViewModel::class.java)
class BadgesOverviewViewModel(
private val badgeRepository: BadgeRepository,
private val subscriptionsRepository: MonthlyDonationRepository
private val subscriptionsRepository: RecurringInAppPaymentRepository
) : ViewModel() {
private val store = Store(BadgesOverviewState())
private val eventSubject = PublishSubject.create<BadgesOverviewEvent>()
@@ -50,7 +51,7 @@ class BadgesOverviewViewModel(
}
disposables += Single.zip(
subscriptionsRepository.getActiveSubscription(),
subscriptionsRepository.getActiveSubscription(InAppPaymentSubscriberRecord.Type.DONATION),
subscriptionsRepository.getSubscriptions()
) { active, all ->
if (!active.isActive && active.activeSubscription?.willCancelAtPeriodEnd() == true) {
@@ -89,7 +90,7 @@ class BadgesOverviewViewModel(
class Factory(
private val badgeRepository: BadgeRepository,
private val subscriptionsRepository: MonthlyDonationRepository
private val subscriptionsRepository: RecurringInAppPaymentRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(BadgesOverviewViewModel(badgeRepository, subscriptionsRepository)))