mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-02 08:23:00 +01:00
Utilize InAppPaymentTable as the SSOT for ManageDonationsFragment.
This commit is contained in:
committed by
jeffrey-signal
parent
d06febd5b5
commit
7fb866fcfb
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.manage
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNotNull
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import io.reactivex.rxjava3.observers.TestObserver
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.deleteAll
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatValue
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ManageDonationsViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private val testAmount = FiatMoney(BigDecimal.valueOf(5), Currency.getInstance("USD")).toFiatValue()
|
||||
private val testBadge = BadgeList.Badge(id = "test-badge")
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
SignalDatabase.inAppPayments.writableDatabase.deleteAll(InAppPaymentTable.TABLE_NAME)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenEndRecordWithNoError_whenIQueryLatest_thenIGetActiveSubscription() {
|
||||
SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.RECURRING_DONATION,
|
||||
state = InAppPaymentTable.State.END,
|
||||
subscriberId = null,
|
||||
endOfPeriod = 1000.seconds,
|
||||
inAppPaymentData = InAppPaymentData(
|
||||
level = 500L,
|
||||
amount = testAmount,
|
||||
badge = testBadge
|
||||
)
|
||||
)
|
||||
|
||||
val latest = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_DONATION)
|
||||
assertThat(latest).isNotNull()
|
||||
assertThat(latest!!.state).isEqualTo(InAppPaymentTable.State.END)
|
||||
assertThat(latest.data.cancellation).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenEmptyDatabase_whenIQueryLatest_thenIGetNull() {
|
||||
val latest = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_DONATION)
|
||||
assertThat(latest).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTransactingRecord_whenIQueryLatest_thenItIsReturned() {
|
||||
val id = SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.RECURRING_DONATION,
|
||||
state = InAppPaymentTable.State.CREATED,
|
||||
subscriberId = null,
|
||||
endOfPeriod = null,
|
||||
inAppPaymentData = InAppPaymentData(
|
||||
level = 500L,
|
||||
amount = testAmount
|
||||
)
|
||||
)
|
||||
|
||||
SignalDatabase.inAppPayments.moveToTransacting(id)
|
||||
|
||||
val latest = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_DONATION)
|
||||
assertThat(latest).isNotNull()
|
||||
assertThat(latest!!.state).isEqualTo(InAppPaymentTable.State.TRANSACTING)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenCreatedRecord_whenIQueryLatest_thenItIsFiltered() {
|
||||
SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.RECURRING_DONATION,
|
||||
state = InAppPaymentTable.State.CREATED,
|
||||
subscriberId = null,
|
||||
endOfPeriod = null,
|
||||
inAppPaymentData = InAppPaymentData()
|
||||
)
|
||||
|
||||
val latest = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_DONATION)
|
||||
assertThat(latest).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenEndRecordWithError_whenIObserveRedemption_thenIGetFailedSubscription() {
|
||||
SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.RECURRING_DONATION,
|
||||
state = InAppPaymentTable.State.END,
|
||||
subscriberId = null,
|
||||
endOfPeriod = 1000.seconds,
|
||||
inAppPaymentData = InAppPaymentData(
|
||||
level = 500L,
|
||||
amount = testAmount,
|
||||
error = InAppPaymentData.Error(type = InAppPaymentData.Error.Type.PAYMENT_PROCESSING)
|
||||
)
|
||||
)
|
||||
|
||||
val observer = TestObserver<DonationRedemptionJobStatus>()
|
||||
InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentType.RECURRING_DONATION)
|
||||
.take(1)
|
||||
.subscribe(observer)
|
||||
observer.awaitCount(1)
|
||||
observer.assertValue(DonationRedemptionJobStatus.FailedSubscription)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenEndRecordWithCancellation_whenIObserveRedemption_thenIGetNone() {
|
||||
SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.RECURRING_DONATION,
|
||||
state = InAppPaymentTable.State.END,
|
||||
subscriberId = null,
|
||||
endOfPeriod = 1000.seconds,
|
||||
inAppPaymentData = InAppPaymentData(
|
||||
level = 500L,
|
||||
amount = testAmount,
|
||||
cancellation = InAppPaymentData.Cancellation(reason = InAppPaymentData.Cancellation.Reason.CANCELED)
|
||||
)
|
||||
)
|
||||
|
||||
val observer = TestObserver<DonationRedemptionJobStatus>()
|
||||
InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentType.RECURRING_DONATION)
|
||||
.take(1)
|
||||
.subscribe(observer)
|
||||
observer.awaitCount(1)
|
||||
observer.assertValue(DonationRedemptionJobStatus.None)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPendingBankTransferRecord_whenIObserveRedemption_thenIGetPendingExternalVerification() {
|
||||
SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.RECURRING_DONATION,
|
||||
state = InAppPaymentTable.State.WAITING_FOR_AUTHORIZATION,
|
||||
subscriberId = null,
|
||||
endOfPeriod = null,
|
||||
inAppPaymentData = InAppPaymentData(
|
||||
level = 500L,
|
||||
amount = FiatMoney(BigDecimal.valueOf(5), Currency.getInstance("EUR")).toFiatValue(),
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.SEPA_DEBIT,
|
||||
waitForAuth = InAppPaymentData.WaitingForAuthorizationState()
|
||||
)
|
||||
)
|
||||
|
||||
val observer = TestObserver<DonationRedemptionJobStatus>()
|
||||
InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentType.RECURRING_DONATION)
|
||||
.take(1)
|
||||
.subscribe(observer)
|
||||
observer.awaitCount(1)
|
||||
|
||||
val status = observer.values().first()
|
||||
assertThat(status is DonationRedemptionJobStatus.PendingExternalVerification).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenKeepAlivePendingRecord_whenIObserveRedemption_thenIGetPendingKeepAlive() {
|
||||
SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.RECURRING_DONATION,
|
||||
state = InAppPaymentTable.State.PENDING,
|
||||
subscriberId = null,
|
||||
endOfPeriod = 1000.seconds,
|
||||
inAppPaymentData = InAppPaymentData(
|
||||
level = 500L,
|
||||
amount = testAmount,
|
||||
redemption = InAppPaymentData.RedemptionState(
|
||||
stage = InAppPaymentData.RedemptionState.Stage.INIT,
|
||||
keepAlive = true
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val observer = TestObserver<DonationRedemptionJobStatus>()
|
||||
InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentType.RECURRING_DONATION)
|
||||
.take(1)
|
||||
.subscribe(observer)
|
||||
observer.awaitCount(1)
|
||||
observer.assertValue(DonationRedemptionJobStatus.PendingKeepAlive)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPendingOneTimeDonation_whenIObserveRedemption_thenIGetPendingStatus() {
|
||||
SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.ONE_TIME_DONATION,
|
||||
state = InAppPaymentTable.State.PENDING,
|
||||
subscriberId = null,
|
||||
endOfPeriod = null,
|
||||
inAppPaymentData = InAppPaymentData(
|
||||
level = 1L,
|
||||
amount = testAmount,
|
||||
badge = testBadge,
|
||||
redemption = InAppPaymentData.RedemptionState(
|
||||
stage = InAppPaymentData.RedemptionState.Stage.INIT
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val observer = TestObserver<DonationRedemptionJobStatus>()
|
||||
InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentType.ONE_TIME_DONATION)
|
||||
.take(1)
|
||||
.subscribe(observer)
|
||||
observer.awaitCount(1)
|
||||
observer.assertValue(DonationRedemptionJobStatus.PendingReceiptRequest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenEndRecordWithNonRedemptionError_whenICheckPaymentFailure_thenItIsTrue() {
|
||||
val id = SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.RECURRING_DONATION,
|
||||
state = InAppPaymentTable.State.END,
|
||||
subscriberId = null,
|
||||
endOfPeriod = 1000.seconds,
|
||||
inAppPaymentData = InAppPaymentData(
|
||||
level = 500L,
|
||||
amount = testAmount,
|
||||
error = InAppPaymentData.Error(type = InAppPaymentData.Error.Type.PAYMENT_PROCESSING)
|
||||
)
|
||||
)
|
||||
|
||||
val payment = SignalDatabase.inAppPayments.getById(id)!!
|
||||
val isPaymentFailure = payment.data.error?.let {
|
||||
it.type != InAppPaymentData.Error.Type.REDEMPTION
|
||||
} ?: false
|
||||
|
||||
assertThat(isPaymentFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenEndRecordWithRedemptionError_whenICheckPaymentFailure_thenItIsFalse() {
|
||||
val id = SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.RECURRING_DONATION,
|
||||
state = InAppPaymentTable.State.END,
|
||||
subscriberId = null,
|
||||
endOfPeriod = 1000.seconds,
|
||||
inAppPaymentData = InAppPaymentData(
|
||||
level = 500L,
|
||||
amount = testAmount,
|
||||
error = InAppPaymentData.Error(type = InAppPaymentData.Error.Type.REDEMPTION)
|
||||
)
|
||||
)
|
||||
|
||||
val payment = SignalDatabase.inAppPayments.getById(id)!!
|
||||
val isPaymentFailure = payment.data.error?.let {
|
||||
it.type != InAppPaymentData.Error.Type.REDEMPTION
|
||||
} ?: false
|
||||
|
||||
assertThat(isPaymentFailure).isEqualTo(false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenStateTransition_whenIUpdateRecord_thenObserverSeesNewState() {
|
||||
val id = SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.RECURRING_DONATION,
|
||||
state = InAppPaymentTable.State.PENDING,
|
||||
subscriberId = null,
|
||||
endOfPeriod = 1000.seconds,
|
||||
inAppPaymentData = InAppPaymentData(
|
||||
level = 500L,
|
||||
amount = testAmount,
|
||||
redemption = InAppPaymentData.RedemptionState(
|
||||
stage = InAppPaymentData.RedemptionState.Stage.INIT,
|
||||
keepAlive = true
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val observer = TestObserver<DonationRedemptionJobStatus>()
|
||||
InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentType.RECURRING_DONATION)
|
||||
.take(2)
|
||||
.subscribe(observer)
|
||||
|
||||
observer.awaitCount(1)
|
||||
assertThat(observer.values().first()).isEqualTo(DonationRedemptionJobStatus.PendingKeepAlive)
|
||||
|
||||
val payment = SignalDatabase.inAppPayments.getById(id)!!
|
||||
SignalDatabase.inAppPayments.update(
|
||||
payment.copy(
|
||||
state = InAppPaymentTable.State.END,
|
||||
data = payment.data.copy(
|
||||
error = InAppPaymentData.Error(type = InAppPaymentData.Error.Type.PAYMENT_PROCESSING)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
observer.awaitCount(2)
|
||||
assertThat(observer.values().last()).isEqualTo(DonationRedemptionJobStatus.FailedSubscription)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNonVerifiedIdealRecurring_whenIObserveRedemption_thenIGetNonVerifiedMonthlyDonation() {
|
||||
SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.RECURRING_DONATION,
|
||||
state = InAppPaymentTable.State.WAITING_FOR_AUTHORIZATION,
|
||||
subscriberId = null,
|
||||
endOfPeriod = null,
|
||||
inAppPaymentData = InAppPaymentData(
|
||||
level = 500L,
|
||||
amount = FiatMoney(BigDecimal.valueOf(5), Currency.getInstance("EUR")).toFiatValue(),
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.IDEAL,
|
||||
waitForAuth = InAppPaymentData.WaitingForAuthorizationState(checkedVerification = true)
|
||||
)
|
||||
)
|
||||
|
||||
val observer = TestObserver<DonationRedemptionJobStatus>()
|
||||
InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentType.RECURRING_DONATION)
|
||||
.take(1)
|
||||
.subscribe(observer)
|
||||
observer.awaitCount(1)
|
||||
|
||||
val status = observer.values().first()
|
||||
assertThat(status is DonationRedemptionJobStatus.PendingExternalVerification).isTrue()
|
||||
val verification = status as DonationRedemptionJobStatus.PendingExternalVerification
|
||||
assertThat(verification.nonVerifiedMonthlyDonation).isNotNull()
|
||||
assertThat(verification.nonVerifiedMonthlyDonation!!.checkedVerification).isTrue()
|
||||
}
|
||||
}
|
||||
@@ -81,15 +81,24 @@ object InAppPaymentsRepository {
|
||||
* This operation will only be performed if we find a latest payment for the given subscriber id in the END state without cancelation data
|
||||
*/
|
||||
fun updateBackupInAppPaymentWithCancelation(activeSubscription: ActiveSubscription) {
|
||||
if (activeSubscription.isCanceled || activeSubscription.willCancelAtPeriodEnd()) {
|
||||
val subscriber = getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP) ?: return
|
||||
updateInAppPaymentWithCancelation(activeSubscription, InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the latest payment object for the given subscriber type with cancelation information as necessary.
|
||||
*
|
||||
* This operation will only be performed if we find a latest payment for the given subscriber id in the END state without cancelation data.
|
||||
*/
|
||||
fun updateInAppPaymentWithCancelation(activeSubscription: ActiveSubscription, subscriberType: InAppPaymentSubscriberRecord.Type) {
|
||||
if (activeSubscription.isCanceled || (subscriberType == InAppPaymentSubscriberRecord.Type.BACKUP && activeSubscription.willCancelAtPeriodEnd()) || activeSubscription.isFailedPayment) {
|
||||
val subscriber = getSubscriber(subscriberType) ?: return
|
||||
val latestPayment = SignalDatabase.inAppPayments.getLatestBySubscriberId(subscriber.subscriberId) ?: return
|
||||
if (latestPayment.state == InAppPaymentTable.State.END && latestPayment.data.cancellation == null) {
|
||||
synchronized(subscriber.type.lock) {
|
||||
val payment = SignalDatabase.inAppPayments.getLatestBySubscriberId(subscriber.subscriberId) ?: return
|
||||
val chargeFailure: ActiveSubscription.ChargeFailure? = activeSubscription.chargeFailure
|
||||
|
||||
Log.i(TAG, "Recording cancelation in the database. (has charge failure? ${chargeFailure != null})")
|
||||
Log.i(TAG, "[$subscriberType] Recording cancelation in the database. (has charge failure? ${chargeFailure != null})")
|
||||
SignalDatabase.inAppPayments.update(
|
||||
payment.copy(
|
||||
data = payment.data.newBuilder()
|
||||
|
||||
@@ -17,7 +17,6 @@ import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
@@ -31,7 +30,7 @@ object ActiveSubscriptionPreference {
|
||||
val subscription: Subscription,
|
||||
val renewalTimestamp: Long = -1L,
|
||||
val redemptionState: ManageDonationsState.RedemptionState,
|
||||
val activeSubscription: ActiveSubscription.Subscription?,
|
||||
val isPaymentFailure: Boolean = false,
|
||||
val subscriberRequiresCancel: Boolean,
|
||||
val onContactSupport: () -> Unit,
|
||||
val onRowClick: (ManageDonationsState.RedemptionState) -> Unit
|
||||
@@ -46,7 +45,7 @@ object ActiveSubscriptionPreference {
|
||||
renewalTimestamp == newItem.renewalTimestamp &&
|
||||
redemptionState == newItem.redemptionState &&
|
||||
FiatMoney.equals(price, newItem.price) &&
|
||||
activeSubscription == newItem.activeSubscription
|
||||
isPaymentFailure == newItem.isPaymentFailure
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +111,7 @@ object ActiveSubscriptionPreference {
|
||||
}
|
||||
|
||||
private fun presentFailureState(model: Model) {
|
||||
if (model.activeSubscription?.isFailedPayment == true || model.subscriberRequiresCancel) {
|
||||
if (model.isPaymentFailure || model.subscriberRequiresCancel) {
|
||||
presentPaymentFailureState(model)
|
||||
} else {
|
||||
presentRedemptionFailureState(model)
|
||||
|
||||
@@ -33,9 +33,12 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.models.Ne
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.thanks.ThanksForYourSupportBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.help.HelpFragment
|
||||
import org.thoughtcrime.securesms.jobs.InAppPaymentKeepAliveJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
@@ -44,9 +47,7 @@ import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import java.util.Currency
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
/**
|
||||
@@ -193,13 +194,16 @@ class ManageDonationsFragment :
|
||||
|
||||
space(16.dp)
|
||||
|
||||
if (state.subscriptionTransactionState is ManageDonationsState.TransactionState.NotInTransaction) {
|
||||
val activeSubscription = state.subscriptionTransactionState.activeSubscription.activeSubscription
|
||||
|
||||
if (activeSubscription != null && !activeSubscription.isCanceled) {
|
||||
val subscription: Subscription? = state.availableSubscriptions.firstOrNull { it.level == activeSubscription.level }
|
||||
if (!state.isLoaded) {
|
||||
customPref(IndeterminateLoadingCircle)
|
||||
} else if (state.networkError) {
|
||||
presentNetworkFailureSettings(state, state.hasReceipts)
|
||||
} else {
|
||||
val payment = state.activeSubscription
|
||||
if (payment != null && payment.data.cancellation == null) {
|
||||
val subscription: Subscription? = state.availableSubscriptions.firstOrNull { it.level == payment.data.level.toInt() }
|
||||
if (subscription != null) {
|
||||
presentSubscriptionSettings(activeSubscription, subscription, state)
|
||||
presentSubscriptionSettings(payment, subscription, state)
|
||||
} else {
|
||||
customPref(IndeterminateLoadingCircle)
|
||||
}
|
||||
@@ -215,10 +219,6 @@ class ManageDonationsFragment :
|
||||
} else {
|
||||
presentNotADonorSettings(state.hasReceipts)
|
||||
}
|
||||
} else if (state.subscriptionTransactionState == ManageDonationsState.TransactionState.NetworkFailure) {
|
||||
presentNetworkFailureSettings(state, state.hasReceipts)
|
||||
} else {
|
||||
customPref(IndeterminateLoadingCircle)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,22 +275,28 @@ class ManageDonationsFragment :
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentSubscriptionSettings(
|
||||
activeSubscription: ActiveSubscription.Subscription,
|
||||
payment: InAppPaymentTable.InAppPayment,
|
||||
subscription: Subscription,
|
||||
state: ManageDonationsState
|
||||
) {
|
||||
val price = payment.data.amount!!.toFiatMoney()
|
||||
val renewalTimestamp = payment.endOfPeriodSeconds.seconds.inWholeMilliseconds
|
||||
val isPaymentFailure = payment.data.error?.let {
|
||||
it.type != InAppPaymentData.Error.Type.REDEMPTION && it.data_ != InAppPaymentKeepAliveJob.KEEP_ALIVE
|
||||
} ?: false
|
||||
|
||||
presentSubscriptionSettingsWithState(state) {
|
||||
customPref(
|
||||
ActiveSubscriptionPreference.Model(
|
||||
price = FiatMoney.fromSignalNetworkAmount(activeSubscription.amount, Currency.getInstance(activeSubscription.currency)),
|
||||
price = price,
|
||||
subscription = subscription,
|
||||
renewalTimestamp = TimeUnit.SECONDS.toMillis(activeSubscription.endOfCurrentPeriod),
|
||||
redemptionState = state.getMonthlyDonorRedemptionState(),
|
||||
renewalTimestamp = renewalTimestamp,
|
||||
redemptionState = state.subscriptionRedemptionState,
|
||||
onContactSupport = {
|
||||
requireActivity().finish()
|
||||
requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.DONATION_INDEX))
|
||||
},
|
||||
activeSubscription = activeSubscription,
|
||||
isPaymentFailure = isPaymentFailure,
|
||||
subscriberRequiresCancel = state.subscriberRequiresCancel,
|
||||
onRowClick = {
|
||||
launcher.launch(InAppPaymentType.RECURRING_DONATION)
|
||||
@@ -312,7 +318,6 @@ class ManageDonationsFragment :
|
||||
subscription = subscription,
|
||||
redemptionState = ManageDonationsState.RedemptionState.IN_PROGRESS,
|
||||
onContactSupport = {},
|
||||
activeSubscription = null,
|
||||
subscriberRequiresCancel = state.subscriberRequiresCancel,
|
||||
onRowClick = {}
|
||||
)
|
||||
|
||||
@@ -1,47 +1,24 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.manage
|
||||
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
|
||||
data class ManageDonationsState(
|
||||
val hasOneTimeBadge: Boolean = false,
|
||||
val hasReceipts: Boolean = false,
|
||||
val featuredBadge: Badge? = null,
|
||||
val subscriptionTransactionState: TransactionState = TransactionState.Init,
|
||||
val isLoaded: Boolean = false,
|
||||
val networkError: Boolean = false,
|
||||
val availableSubscriptions: List<Subscription> = emptyList(),
|
||||
val activeSubscription: InAppPaymentTable.InAppPayment? = null,
|
||||
val subscriptionRedemptionState: RedemptionState = RedemptionState.NONE,
|
||||
val pendingOneTimeDonation: PendingOneTimeDonation? = null,
|
||||
val nonVerifiedMonthlyDonation: NonVerifiedMonthlyDonation? = null,
|
||||
val subscriberRequiresCancel: Boolean = false,
|
||||
private val subscriptionRedemptionState: RedemptionState = RedemptionState.NONE
|
||||
val subscriberRequiresCancel: Boolean = false
|
||||
) {
|
||||
|
||||
fun getMonthlyDonorRedemptionState(): RedemptionState {
|
||||
return when (subscriptionTransactionState) {
|
||||
TransactionState.Init -> subscriptionRedemptionState
|
||||
TransactionState.NetworkFailure -> subscriptionRedemptionState
|
||||
TransactionState.InTransaction -> RedemptionState.IN_PROGRESS
|
||||
is TransactionState.NotInTransaction -> getStateFromActiveSubscription(subscriptionTransactionState.activeSubscription) ?: subscriptionRedemptionState
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStateFromActiveSubscription(activeSubscription: ActiveSubscription): RedemptionState? {
|
||||
return when {
|
||||
activeSubscription.isFailedPayment && !activeSubscription.isPastDue -> RedemptionState.FAILED
|
||||
activeSubscription.isPendingBankTransfer -> RedemptionState.IS_PENDING_BANK_TRANSFER
|
||||
activeSubscription.isInProgress -> RedemptionState.IN_PROGRESS
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TransactionState {
|
||||
data object Init : TransactionState()
|
||||
data object NetworkFailure : TransactionState()
|
||||
data object InTransaction : TransactionState()
|
||||
class NotInTransaction(val activeSubscription: ActiveSubscription) : TransactionState()
|
||||
}
|
||||
|
||||
enum class RedemptionState {
|
||||
NONE,
|
||||
IN_PROGRESS,
|
||||
|
||||
@@ -3,33 +3,35 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.manage
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.asFlow
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.jobs.InAppPaymentKeepAliveJob
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import java.util.Optional
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class ManageDonationsViewModel : ViewModel() {
|
||||
|
||||
@@ -62,6 +64,46 @@ class ManageDonationsViewModel : ViewModel() {
|
||||
internalDisplayThanksBottomSheetPulse.emit(Badges.fromDatabaseBadge(it.data.badge!!))
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
updateRecurringDonationState()
|
||||
|
||||
InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentType.RECURRING_DONATION)
|
||||
.asFlow()
|
||||
.collect { redemptionStatus ->
|
||||
val latestPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_DONATION)
|
||||
|
||||
val activeSubscription: InAppPaymentTable.InAppPayment? = latestPayment?.let {
|
||||
if (it.data.cancellation == null) it else null
|
||||
}
|
||||
|
||||
store.update { manageDonationsState ->
|
||||
manageDonationsState.copy(
|
||||
nonVerifiedMonthlyDonation = if (redemptionStatus is DonationRedemptionJobStatus.PendingExternalVerification) redemptionStatus.nonVerifiedMonthlyDonation else null,
|
||||
subscriptionRedemptionState = deriveRedemptionState(redemptionStatus, latestPayment),
|
||||
activeSubscription = activeSubscription
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentType.ONE_TIME_DONATION)
|
||||
.asFlow()
|
||||
.collect { redemptionStatus ->
|
||||
val pendingOneTimeDonation = when (redemptionStatus) {
|
||||
is DonationRedemptionJobStatus.PendingExternalVerification -> redemptionStatus.pendingOneTimeDonation
|
||||
DonationRedemptionJobStatus.PendingReceiptRedemption,
|
||||
DonationRedemptionJobStatus.PendingReceiptRequest -> {
|
||||
val latestPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.ONE_TIME_DONATION)
|
||||
latestPayment?.toPendingOneTimeDonation()
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
store.update { it.copy(pendingOneTimeDonation = pendingOneTimeDonation) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
@@ -69,8 +111,8 @@ class ManageDonationsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun retry() {
|
||||
if (!disposables.isDisposed && store.state.subscriptionTransactionState == ManageDonationsState.TransactionState.NetworkFailure) {
|
||||
store.update { it.copy(subscriptionTransactionState = ManageDonationsState.TransactionState.Init) }
|
||||
if (!disposables.isDisposed && store.state.networkError) {
|
||||
store.update { it.copy(networkError = false) }
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
@@ -78,16 +120,23 @@ class ManageDonationsViewModel : ViewModel() {
|
||||
fun refresh() {
|
||||
disposables.clear()
|
||||
|
||||
val levelUpdateOperationEdges: Observable<Boolean> = LevelUpdate.isProcessing.distinctUntilChanged()
|
||||
val activeSubscription: Single<ActiveSubscription> = RecurringInAppPaymentRepository.getActiveSubscription(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
InAppPaymentKeepAliveJob.enqueueAndTrackTime(System.currentTimeMillis().milliseconds)
|
||||
|
||||
disposables += Single.fromCallable {
|
||||
InAppPaymentsRepository.getShouldCancelSubscriptionBeforeNextSubscribeAttempt(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
}.subscribeOn(Schedulers.io()).subscribeBy { requiresCancel ->
|
||||
store.update {
|
||||
it.copy(subscriberRequiresCancel = requiresCancel)
|
||||
}.subscribeOn(Schedulers.io()).subscribeBy(
|
||||
onSuccess = { requiresCancel ->
|
||||
store.update {
|
||||
it.copy(subscriberRequiresCancel = requiresCancel)
|
||||
}
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Error retrieving cancel state", throwable)
|
||||
store.update {
|
||||
it.copy(networkError = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
disposables += Recipient.observable(Recipient.self().id).map { it.badges }.subscribeBy { badges ->
|
||||
store.update { state ->
|
||||
@@ -101,53 +150,6 @@ class ManageDonationsViewModel : ViewModel() {
|
||||
store.update { it.copy(hasReceipts = hasReceipts) }
|
||||
}
|
||||
|
||||
disposables += InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentType.RECURRING_DONATION).subscribeBy { redemptionStatus ->
|
||||
store.update { manageDonationsState ->
|
||||
manageDonationsState.copy(
|
||||
nonVerifiedMonthlyDonation = if (redemptionStatus is DonationRedemptionJobStatus.PendingExternalVerification) redemptionStatus.nonVerifiedMonthlyDonation else null,
|
||||
subscriptionRedemptionState = mapStatusToRedemptionState(redemptionStatus)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
disposables += Observable.combineLatest(
|
||||
SignalStore.inAppPayments.observablePendingOneTimeDonation,
|
||||
InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentType.ONE_TIME_DONATION)
|
||||
) { pendingFromStore, pendingFromJob ->
|
||||
if (pendingFromStore.isPresent) {
|
||||
pendingFromStore
|
||||
} else if (pendingFromJob is DonationRedemptionJobStatus.PendingExternalVerification) {
|
||||
Optional.ofNullable(pendingFromJob.pendingOneTimeDonation)
|
||||
} else {
|
||||
Optional.empty()
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.subscribeBy { pending ->
|
||||
store.update { it.copy(pendingOneTimeDonation = pending.orNull()) }
|
||||
}
|
||||
|
||||
disposables += levelUpdateOperationEdges.switchMapSingle { isProcessing ->
|
||||
if (isProcessing) {
|
||||
Single.just(ManageDonationsState.TransactionState.InTransaction)
|
||||
} else {
|
||||
activeSubscription.map { ManageDonationsState.TransactionState.NotInTransaction(it) }
|
||||
}
|
||||
}.subscribeBy(
|
||||
onNext = { transactionState ->
|
||||
store.update {
|
||||
it.copy(subscriptionTransactionState = transactionState)
|
||||
}
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Error retrieving subscription transaction state", throwable)
|
||||
|
||||
store.update {
|
||||
it.copy(subscriptionTransactionState = ManageDonationsState.TransactionState.NetworkFailure)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
disposables += RecurringInAppPaymentRepository.getSubscriptions().subscribeBy(
|
||||
onSuccess = { subs ->
|
||||
store.update { it.copy(availableSubscriptions = subs) }
|
||||
@@ -158,18 +160,70 @@ class ManageDonationsViewModel : ViewModel() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapStatusToRedemptionState(status: DonationRedemptionJobStatus): ManageDonationsState.RedemptionState {
|
||||
private fun updateRecurringDonationState() {
|
||||
val latestPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_DONATION)
|
||||
|
||||
val activeSubscription: InAppPaymentTable.InAppPayment? = latestPayment?.let {
|
||||
if (it.data.cancellation == null) it else null
|
||||
}
|
||||
|
||||
store.update { manageDonationsState ->
|
||||
manageDonationsState.copy(
|
||||
isLoaded = true,
|
||||
activeSubscription = activeSubscription
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deriveRedemptionState(status: DonationRedemptionJobStatus, latestPayment: InAppPaymentTable.InAppPayment?): ManageDonationsState.RedemptionState {
|
||||
return when (status) {
|
||||
DonationRedemptionJobStatus.FailedSubscription -> ManageDonationsState.RedemptionState.FAILED
|
||||
DonationRedemptionJobStatus.None -> ManageDonationsState.RedemptionState.NONE
|
||||
DonationRedemptionJobStatus.PendingKeepAlive -> ManageDonationsState.RedemptionState.SUBSCRIPTION_REFRESH
|
||||
DonationRedemptionJobStatus.FailedSubscription -> ManageDonationsState.RedemptionState.FAILED
|
||||
|
||||
is DonationRedemptionJobStatus.PendingExternalVerification -> {
|
||||
if (latestPayment != null && (latestPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.SEPA_DEBIT || latestPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.IDEAL)) {
|
||||
ManageDonationsState.RedemptionState.IS_PENDING_BANK_TRANSFER
|
||||
} else {
|
||||
ManageDonationsState.RedemptionState.IN_PROGRESS
|
||||
}
|
||||
}
|
||||
|
||||
is DonationRedemptionJobStatus.PendingExternalVerification,
|
||||
DonationRedemptionJobStatus.PendingReceiptRedemption,
|
||||
DonationRedemptionJobStatus.PendingReceiptRequest -> ManageDonationsState.RedemptionState.IN_PROGRESS
|
||||
}
|
||||
}
|
||||
|
||||
private fun InAppPaymentTable.InAppPayment.toPendingOneTimeDonation(): PendingOneTimeDonation? {
|
||||
if (type.recurring || data.amount == null || data.badge == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return PendingOneTimeDonation(
|
||||
paymentMethodType = when (data.paymentMethodType) {
|
||||
InAppPaymentData.PaymentMethodType.UNKNOWN -> PendingOneTimeDonation.PaymentMethodType.CARD
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PAY -> PendingOneTimeDonation.PaymentMethodType.CARD
|
||||
InAppPaymentData.PaymentMethodType.CARD -> PendingOneTimeDonation.PaymentMethodType.CARD
|
||||
InAppPaymentData.PaymentMethodType.SEPA_DEBIT -> PendingOneTimeDonation.PaymentMethodType.SEPA_DEBIT
|
||||
InAppPaymentData.PaymentMethodType.IDEAL -> PendingOneTimeDonation.PaymentMethodType.IDEAL
|
||||
InAppPaymentData.PaymentMethodType.PAYPAL -> PendingOneTimeDonation.PaymentMethodType.PAYPAL
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING -> error("Not supported.")
|
||||
},
|
||||
amount = data.amount,
|
||||
badge = data.badge,
|
||||
timestamp = insertedAt.inWholeMilliseconds,
|
||||
error = data.error?.takeIf { it.data_ != InAppPaymentKeepAliveJob.KEEP_ALIVE }?.let {
|
||||
DonationErrorValue(
|
||||
type = when (it.type) {
|
||||
InAppPaymentData.Error.Type.REDEMPTION -> DonationErrorValue.Type.REDEMPTION
|
||||
InAppPaymentData.Error.Type.PAYMENT_PROCESSING -> DonationErrorValue.Type.PAYMENT
|
||||
else -> DonationErrorValue.Type.PAYMENT
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ManageDonationsViewModel::class.java)
|
||||
}
|
||||
|
||||
@@ -355,10 +355,8 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
return readableDatabase.select()
|
||||
.from(TABLE_NAME)
|
||||
.where(
|
||||
"($STATE = ? OR $STATE = ? OR $STATE = ?) AND $TYPE = ?",
|
||||
State.serialize(State.PENDING),
|
||||
State.serialize(State.WAITING_FOR_AUTHORIZATION),
|
||||
State.serialize(State.END),
|
||||
"$STATE != ? AND $TYPE = ?",
|
||||
State.serialize(State.CREATED),
|
||||
InAppPaymentType.serialize(type)
|
||||
)
|
||||
.orderBy("$INSERTED_AT DESC")
|
||||
|
||||
@@ -153,6 +153,12 @@ class InAppPaymentKeepAliveJob private constructor(
|
||||
return
|
||||
}
|
||||
|
||||
if (type == InAppPaymentSubscriberRecord.Type.DONATION && activeSubscription.isCanceled) {
|
||||
info(type, "Subscription is canceled. Writing cancellation to DB.")
|
||||
InAppPaymentsRepository.updateInAppPaymentWithCancelation(activeSubscription, type)
|
||||
return
|
||||
}
|
||||
|
||||
val activeInAppPayment = getActiveInAppPayment(subscriber, subscription)
|
||||
if (activeInAppPayment == null) {
|
||||
warn(type, "Failed to generate active in-app payment. Exiting")
|
||||
|
||||
@@ -5,19 +5,67 @@
|
||||
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import android.app.Application
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isTrue
|
||||
import io.mockk.every
|
||||
import io.mockk.verify
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsTestRule
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule
|
||||
import org.thoughtcrime.securesms.testutil.MockSignalStoreRule
|
||||
import org.thoughtcrime.securesms.testutil.SystemOutLogger
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription.ChargeFailure
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import org.whispersystems.signalservice.internal.EmptyResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class InAppPaymentKeepAliveJobTest {
|
||||
|
||||
@get:Rule
|
||||
val mockSignalStore = MockSignalStoreRule()
|
||||
|
||||
@get:Rule
|
||||
val appDependencies = MockAppDependenciesRule()
|
||||
|
||||
@get:Rule
|
||||
val inAppPaymentsTestRule = InAppPaymentsTestRule()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
Log.initialize(SystemOutLogger())
|
||||
|
||||
every { mockSignalStore.account.isRegistered } returns true
|
||||
every { mockSignalStore.account.isLinkedDevice } returns false
|
||||
|
||||
every { SignalDatabase.inAppPayments.getOldPendingPayments(any()) } returns emptyList()
|
||||
every { SignalDatabase.inAppPayments.hasPrePendingRecurringTransaction(any()) } returns false
|
||||
|
||||
val subscriber = InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = java.util.Currency.getInstance("USD"),
|
||||
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData.PaymentMethodType.CARD,
|
||||
iapSubscriptionId = null
|
||||
)
|
||||
every { InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION) } returns subscriber
|
||||
|
||||
every { AppDependencies.donationsService.putSubscription(any()) } returns ServiceResponse(200, "", EmptyResponse.INSTANCE, null, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an unregistered local user, when I run, then I expect skip`() {
|
||||
every { mockSignalStore.account.isRegistered } returns false
|
||||
@@ -28,4 +76,37 @@ class InAppPaymentKeepAliveJobTest {
|
||||
|
||||
assertThat(result.isSuccess).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a canceled subscription, when I run, then I write cancellation and return early`() {
|
||||
val activeSubscription = inAppPaymentsTestRule.createActiveSubscription(
|
||||
status = "canceled",
|
||||
isActive = false
|
||||
)
|
||||
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = activeSubscription)
|
||||
every { InAppPaymentsRepository.updateInAppPaymentWithCancelation(any(), any()) } returns Unit
|
||||
|
||||
val job = InAppPaymentKeepAliveJob.create(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
val result = job.run()
|
||||
|
||||
assertThat(result.isSuccess).isTrue()
|
||||
verify(exactly = 1) { InAppPaymentsRepository.updateInAppPaymentWithCancelation(activeSubscription, InAppPaymentSubscriberRecord.Type.DONATION) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a past-due subscription with charge failure, when I run, then I do not write cancellation`() {
|
||||
val chargeFailure = ChargeFailure("test", "", "", "", "")
|
||||
val activeSubscription = inAppPaymentsTestRule.createActiveSubscription(
|
||||
status = "past_due",
|
||||
isActive = false,
|
||||
chargeFailure = chargeFailure
|
||||
)
|
||||
inAppPaymentsTestRule.initializeActiveSubscriptionMock(activeSubscription = activeSubscription)
|
||||
every { InAppPaymentsRepository.updateInAppPaymentWithCancelation(any(), any()) } returns Unit
|
||||
|
||||
val job = InAppPaymentKeepAliveJob.create(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
job.run()
|
||||
|
||||
verify(exactly = 0) { InAppPaymentsRepository.updateInAppPaymentWithCancelation(any(), any()) }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user