kopia lustrzana https://github.com/ryukoposting/Signal-Android
Update how we deal with failed or in progress subscriptions.
rodzic
b4fe5bdcc6
commit
8a00caabd7
|
@ -39,7 +39,7 @@ class AppSettingsViewModel(private val subscriptionsRepository: SubscriptionsRep
|
|||
}
|
||||
|
||||
subscriptionsRepository.getActiveSubscription().subscribeBy(
|
||||
onSuccess = { subscription -> store.update { it.copy(hasActiveSubscription = subscription.isActive) } },
|
||||
onSuccess = { subscription -> store.update { it.copy(hasActiveSubscription = subscription.activeSubscription != null) } },
|
||||
onError = { throwable ->
|
||||
if (throwable.isNotFoundException()) {
|
||||
Log.w(TAG, "Could not load active subscription due to unset SubscriberId (404).")
|
||||
|
|
|
@ -7,10 +7,10 @@ import android.widget.ProgressBar
|
|||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
|
@ -27,6 +27,7 @@ import java.util.Locale
|
|||
object ActiveSubscriptionPreference {
|
||||
|
||||
class Model(
|
||||
val price: FiatMoney,
|
||||
val subscription: Subscription,
|
||||
val onAddBoostClick: () -> Unit,
|
||||
val renewalTimestamp: Long = -1L,
|
||||
|
@ -62,7 +63,7 @@ object ActiveSubscriptionPreference {
|
|||
R.string.MySupportPreference__s_per_month,
|
||||
FiatMoneyUtil.format(
|
||||
context.resources,
|
||||
model.subscription.prices.first { it.currency == SignalStore.donationsValues().getSubscriptionCurrency() },
|
||||
model.price,
|
||||
FiatMoneyUtil.formatOptions()
|
||||
)
|
||||
)
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.widget.Toast
|
|||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
|
@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
|||
import org.thoughtcrime.securesms.help.HelpFragment
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import java.util.Currency
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
|
@ -85,20 +87,24 @@ class ManageDonationsFragment : DSLSettingsFragment() {
|
|||
)
|
||||
|
||||
if (state.transactionState is ManageDonationsState.TransactionState.NotInTransaction) {
|
||||
val activeSubscription = state.transactionState.activeSubscription
|
||||
if (activeSubscription.isActive) {
|
||||
val subscription: Subscription? = state.availableSubscriptions.firstOrNull { activeSubscription.activeSubscription.level == it.level }
|
||||
val activeSubscription = state.transactionState.activeSubscription.activeSubscription
|
||||
if (activeSubscription != null) {
|
||||
val subscription: Subscription? = state.availableSubscriptions.firstOrNull { activeSubscription.level == it.level }
|
||||
if (subscription != null) {
|
||||
space(DimensionUnit.DP.toPixels(12f).toInt())
|
||||
|
||||
val activeCurrency = Currency.getInstance(activeSubscription.currency)
|
||||
val activeAmount = activeSubscription.amount.movePointLeft(activeCurrency.defaultFractionDigits)
|
||||
|
||||
customPref(
|
||||
ActiveSubscriptionPreference.Model(
|
||||
price = FiatMoney(activeAmount, activeCurrency),
|
||||
subscription = subscription,
|
||||
onAddBoostClick = {
|
||||
findNavController().navigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToBoosts())
|
||||
},
|
||||
renewalTimestamp = TimeUnit.SECONDS.toMillis(activeSubscription.activeSubscription.endOfCurrentPeriod),
|
||||
redemptionState = state.subscriptionRedemptionState,
|
||||
renewalTimestamp = TimeUnit.SECONDS.toMillis(activeSubscription.endOfCurrentPeriod),
|
||||
redemptionState = state.getRedemptionState(),
|
||||
onContactSupport = {
|
||||
requireActivity().finish()
|
||||
requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.DONATION_INDEX))
|
||||
|
@ -120,7 +126,7 @@ class ManageDonationsFragment : DSLSettingsFragment() {
|
|||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ManageDonationsFragment__manage_subscription),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_person_white_24dp),
|
||||
isEnabled = state.subscriptionRedemptionState != ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS,
|
||||
isEnabled = state.getRedemptionState() != ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS,
|
||||
onClick = {
|
||||
findNavController().navigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToSubscribeFragment())
|
||||
}
|
||||
|
|
|
@ -8,8 +8,25 @@ data class ManageDonationsState(
|
|||
val featuredBadge: Badge? = null,
|
||||
val transactionState: TransactionState = TransactionState.Init,
|
||||
val availableSubscriptions: List<Subscription> = emptyList(),
|
||||
val subscriptionRedemptionState: SubscriptionRedemptionState = SubscriptionRedemptionState.NONE
|
||||
private val subscriptionRedemptionState: SubscriptionRedemptionState = SubscriptionRedemptionState.NONE
|
||||
) {
|
||||
|
||||
fun getRedemptionState(): SubscriptionRedemptionState {
|
||||
return when (transactionState) {
|
||||
TransactionState.Init -> subscriptionRedemptionState
|
||||
TransactionState.InTransaction -> SubscriptionRedemptionState.IN_PROGRESS
|
||||
is TransactionState.NotInTransaction -> getStateFromActiveSubscription(transactionState.activeSubscription) ?: subscriptionRedemptionState
|
||||
}
|
||||
}
|
||||
|
||||
fun getStateFromActiveSubscription(activeSubscription: ActiveSubscription): SubscriptionRedemptionState? {
|
||||
return when {
|
||||
activeSubscription.isFailedPayment -> SubscriptionRedemptionState.FAILED
|
||||
activeSubscription.isInProgress -> SubscriptionRedemptionState.IN_PROGRESS
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TransactionState {
|
||||
object Init : TransactionState()
|
||||
object InTransaction : TransactionState()
|
||||
|
|
|
@ -73,7 +73,7 @@ class ManageDonationsViewModel(
|
|||
it.copy(transactionState = transactionState)
|
||||
}
|
||||
|
||||
if (transactionState is ManageDonationsState.TransactionState.NotInTransaction && !transactionState.activeSubscription.isActive) {
|
||||
if (transactionState is ManageDonationsState.TransactionState.NotInTransaction && transactionState.activeSubscription.activeSubscription == null) {
|
||||
eventPublisher.onNext(ManageDonationsEvent.NOT_SUBSCRIBED)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -10,6 +10,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
||||
|
@ -35,6 +36,7 @@ import org.thoughtcrime.securesms.subscription.Subscription
|
|||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import java.util.Calendar
|
||||
import java.util.Currency
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
|
@ -167,9 +169,19 @@ class SubscribeFragment : DSLSettingsFragment(
|
|||
space(DimensionUnit.DP.toPixels(75f).toInt())
|
||||
} else {
|
||||
state.subscriptions.forEach {
|
||||
|
||||
val isActive = state.activeSubscription?.activeSubscription?.level == it.level && state.activeSubscription.isActive
|
||||
|
||||
val activePrice = state.activeSubscription?.activeSubscription?.let { sub ->
|
||||
val activeCurrency = Currency.getInstance(sub.currency)
|
||||
val activeAmount = sub.amount.movePointLeft(activeCurrency.defaultFractionDigits)
|
||||
|
||||
FiatMoney(activeAmount, activeCurrency)
|
||||
}
|
||||
|
||||
customPref(
|
||||
Subscription.Model(
|
||||
activePrice = if (isActive) { activePrice } else null,
|
||||
subscription = it,
|
||||
isSelected = state.selectedSubscription == it,
|
||||
isEnabled = areFieldsEnabled,
|
||||
|
|
|
@ -162,6 +162,20 @@ class SubscribeViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
private fun cancelActiveSubscriptionIfNecessary(): Completable {
|
||||
return Single.just(SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt).flatMapCompletable {
|
||||
if (it) {
|
||||
donationPaymentRepository.cancelActiveSubscription().doOnComplete {
|
||||
SignalStore.donationsValues().setLastEndOfPeriod(0L)
|
||||
SignalStore.donationsValues().clearLevelOperations()
|
||||
SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt = false
|
||||
}
|
||||
} else {
|
||||
Completable.complete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.CANCELLING) }
|
||||
disposables += donationPaymentRepository.cancelActiveSubscription().subscribeBy(
|
||||
|
@ -201,7 +215,10 @@ class SubscribeViewModel(
|
|||
|
||||
store.update { it.copy(stage = SubscribeState.Stage.PAYMENT_PIPELINE) }
|
||||
|
||||
val setup = ensureSubscriberId.andThen(continueSetup).onErrorResumeNext { Completable.error(DonationExceptions.SetupFailed(it)) }
|
||||
val setup = ensureSubscriberId
|
||||
.andThen(cancelActiveSubscriptionIfNecessary())
|
||||
.andThen(continueSetup)
|
||||
.onErrorResumeNext { Completable.error(DonationExceptions.SetupFailed(it)) }
|
||||
|
||||
setup.andThen(setLevel).subscribeBy(
|
||||
onError = { throwable ->
|
||||
|
@ -233,7 +250,7 @@ class SubscribeViewModel(
|
|||
|
||||
fun updateSubscription() {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.PAYMENT_PIPELINE) }
|
||||
donationPaymentRepository.setSubscriptionLevel(store.state.selectedSubscription!!.level.toString())
|
||||
cancelActiveSubscriptionIfNecessary().andThen(donationPaymentRepository.setSubscriptionLevel(store.state.selectedSubscription!!.level.toString()))
|
||||
.subscribeBy(
|
||||
onComplete = {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.READY) }
|
||||
|
|
|
@ -200,6 +200,11 @@ public class RefreshOwnProfileJob extends BaseJob {
|
|||
|
||||
Log.d(TAG, "Marking subscription badge as expired, should notifiy next time the conversation list is open.");
|
||||
SignalStore.donationsValues().setExpiredBadge(mostRecentExpiration);
|
||||
|
||||
if (!SignalStore.donationsValues().isUserManuallyCancelled()) {
|
||||
Log.d(TAG, "Detected an unexpected subscription expiry.");
|
||||
SignalStore.donationsValues().setShouldCancelSubscriptionBeforeNextSubscribeAttempt(true);
|
||||
}
|
||||
} else if (!remoteHasBoostBadges && localHasBoostBadges) {
|
||||
Badge mostRecentExpiration = Recipient.self()
|
||||
.getBadges()
|
||||
|
|
|
@ -118,6 +118,11 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
if (subscription == null || !subscription.isActive()) {
|
||||
Log.w(TAG, "User does not have an active subscription yet.", true);
|
||||
throw new RetryableException();
|
||||
} else if (subscription.isFailedPayment()) {
|
||||
Log.w(TAG, "Subscription payment failure. Passing through to redemption job.", true);
|
||||
SignalStore.donationsValues().setShouldCancelSubscriptionBeforeNextSubscribeAttempt(true);
|
||||
setOutputData(new Data.Builder().putBoolean(DonationReceiptRedemptionJob.INPUT_PAYMENT_FAILURE, true).build());
|
||||
return;
|
||||
} else {
|
||||
Log.i(TAG, "Recording end of period from active subscription.", true);
|
||||
SignalStore.donationsValues().setLastEndOfPeriod(subscription.getEndOfCurrentPeriod());
|
||||
|
@ -206,7 +211,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
}
|
||||
}
|
||||
|
||||
private static void handleApplicationError(ServiceResponse<ReceiptCredentialResponse> response) throws Exception {
|
||||
private void handleApplicationError(ServiceResponse<ReceiptCredentialResponse> response) throws Exception {
|
||||
switch (response.getStatus()) {
|
||||
case 204:
|
||||
Log.w(TAG, "User does not have receipts available to exchange. Exiting.", response.getApplicationError().get(), true);
|
||||
|
@ -214,6 +219,11 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
case 400:
|
||||
Log.w(TAG, "Receipt credential request failed to validate.", response.getApplicationError().get(), true);
|
||||
throw new Exception(response.getApplicationError().get());
|
||||
case 402:
|
||||
Log.w(TAG, "Subscription payment failure. Passing through to redemption job.", true);
|
||||
SignalStore.donationsValues().setShouldCancelSubscriptionBeforeNextSubscribeAttempt(true);
|
||||
setOutputData(new Data.Builder().putBoolean(DonationReceiptRedemptionJob.INPUT_PAYMENT_FAILURE, true).build());
|
||||
break;
|
||||
case 403:
|
||||
Log.w(TAG, "SubscriberId password mismatch or account auth was present.", response.getApplicationError().get(), true);
|
||||
throw new Exception(response.getApplicationError().get());
|
||||
|
|
|
@ -29,6 +29,7 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
|
|||
private const val KEY_LEVEL_HISTORY = "donation.level.history"
|
||||
private const val DISPLAY_BADGES_ON_PROFILE = "donation.display.badges.on.profile"
|
||||
private const val SUBSCRIPTION_REDEMPTION_FAILED = "donation.subscription.redemption.failed"
|
||||
private const val SHOULD_CANCEL_SUBSCRIPTION_BEFORE_NEXT_SUBSCRIBE_ATTEMPT = "donation.should.cancel.subscription.before.next.subscribe.attempt"
|
||||
}
|
||||
|
||||
override fun onFirstEverAppLaunch() = Unit
|
||||
|
@ -36,7 +37,8 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
|
|||
override fun getKeysToIncludeInBackup(): MutableList<String> = mutableListOf(
|
||||
KEY_CURRENCY_CODE_BOOST,
|
||||
KEY_LAST_KEEP_ALIVE_LAUNCH,
|
||||
KEY_LAST_END_OF_PERIOD
|
||||
KEY_LAST_END_OF_PERIOD,
|
||||
SHOULD_CANCEL_SUBSCRIPTION_BEFORE_NEXT_SUBSCRIBE_ATTEMPT
|
||||
)
|
||||
|
||||
private val subscriptionCurrencyPublisher: Subject<Currency> by lazy { BehaviorSubject.createDefault(getSubscriptionCurrency()) }
|
||||
|
@ -208,4 +210,17 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
|
|||
fun clearSubscriptionRedemptionFailed() {
|
||||
putBoolean(SUBSCRIPTION_REDEMPTION_FAILED, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Denotes that the previous attempt to subscribe failed in some way. Either an
|
||||
* automatic renewal failed resulting in an unexpected expiration, or payment failed
|
||||
* on Stripe's end.
|
||||
*
|
||||
* Before trying to resubscribe, we should first ensure there are no subscriptions set
|
||||
* on the server. Otherwise, we could get into a situation where the user is unable to
|
||||
* resubscribe.
|
||||
*/
|
||||
var shouldCancelSubscriptionBeforeNextSubscribeAttempt: Boolean
|
||||
get() = getBoolean(SHOULD_CANCEL_SUBSCRIPTION_BEFORE_NEXT_SUBSCRIBE_ATTEMPT, false)
|
||||
set(value) = putBoolean(SHOULD_CANCEL_SUBSCRIPTION_BEFORE_NEXT_SUBSCRIBE_ATTEMPT, value)
|
||||
}
|
||||
|
|
|
@ -79,6 +79,7 @@ data class Subscription(
|
|||
}
|
||||
|
||||
class Model(
|
||||
val activePrice: FiatMoney?,
|
||||
val subscription: Subscription,
|
||||
val isSelected: Boolean,
|
||||
val isActive: Boolean,
|
||||
|
@ -134,7 +135,7 @@ data class Subscription(
|
|||
|
||||
val formattedPrice = FiatMoneyUtil.format(
|
||||
context.resources,
|
||||
model.subscription.prices.first { it.currency == model.selectedCurrency },
|
||||
model.activePrice ?: model.subscription.prices.first { it.currency == model.selectedCurrency },
|
||||
FiatMoneyUtil.formatOptions()
|
||||
)
|
||||
|
||||
|
|
|
@ -4,9 +4,71 @@ import com.fasterxml.jackson.annotation.JsonCreator;
|
|||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ActiveSubscription {
|
||||
|
||||
private enum Status {
|
||||
/**
|
||||
* The subscription is currently in a trial period and it’s safe to provision your product for your customer.
|
||||
* The subscription transitions automatically to active when the first payment is made.
|
||||
*/
|
||||
TRIALING("trialing"),
|
||||
|
||||
/**
|
||||
* The subscription is in good standing and the most recent payment was successful. It’s safe to provision your product for your customer.
|
||||
*/
|
||||
ACTIVE("active"),
|
||||
|
||||
/**
|
||||
* Payment failed when you created the subscription. A successful payment needs to be made within 23 hours to activate the subscription.
|
||||
*/
|
||||
INCOMPLETE("incomplete"),
|
||||
|
||||
/**
|
||||
* The initial payment on the subscription failed and no successful payment was made within 23 hours of creating the subscription.
|
||||
* These subscriptions don’t bill customers. This status exists so you can track customers that failed to activate their subscriptions.
|
||||
*/
|
||||
INCOMPLETE_EXPIRED("incomplete_expired"),
|
||||
|
||||
/**
|
||||
* Payment on the latest invoice either failed or wasn’t attempted.
|
||||
*/
|
||||
PAST_DUE("past_due"),
|
||||
|
||||
/**
|
||||
* The subscription has been canceled. During cancellation, automatic collection for all unpaid invoices is disabled (auto_advance=false).
|
||||
*/
|
||||
CANCELED("canceled"),
|
||||
|
||||
/**
|
||||
* The latest invoice hasn’t been paid but the subscription remains in place.
|
||||
* The latest invoice remains open and invoices continue to be generated but payments aren’t attempted.
|
||||
*/
|
||||
UNPAID("unpaid");
|
||||
|
||||
private final String status;
|
||||
|
||||
Status(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
private static Status getStatus(String status) {
|
||||
for (Status s : Status.values()) {
|
||||
if (Objects.equals(status, s.status)) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unknown status " + status);
|
||||
}
|
||||
|
||||
static boolean isPaymentFailed(String status) {
|
||||
Status s = getStatus(status);
|
||||
return s == INCOMPLETE || s == INCOMPLETE_EXPIRED;
|
||||
}
|
||||
}
|
||||
|
||||
private final Subscription activeSubscription;
|
||||
|
||||
@JsonCreator
|
||||
|
@ -22,6 +84,14 @@ public final class ActiveSubscription {
|
|||
return activeSubscription != null && activeSubscription.isActive();
|
||||
}
|
||||
|
||||
public boolean isInProgress() {
|
||||
return activeSubscription != null && !isActive() && !isFailedPayment();
|
||||
}
|
||||
|
||||
public boolean isFailedPayment() {
|
||||
return activeSubscription != null && !isActive() && isFailedPayment();
|
||||
}
|
||||
|
||||
public static final class Subscription {
|
||||
private final int level;
|
||||
private final String currency;
|
||||
|
@ -30,6 +100,7 @@ public final class ActiveSubscription {
|
|||
private final boolean isActive;
|
||||
private final long billingCycleAnchor;
|
||||
private final boolean willCancelAtPeriodEnd;
|
||||
private final String status;
|
||||
|
||||
@JsonCreator
|
||||
public Subscription(@JsonProperty("level") int level,
|
||||
|
@ -38,7 +109,8 @@ public final class ActiveSubscription {
|
|||
@JsonProperty("endOfCurrentPeriod") long endOfCurrentPeriod,
|
||||
@JsonProperty("active") boolean isActive,
|
||||
@JsonProperty("billingCycleAnchor") long billingCycleAnchor,
|
||||
@JsonProperty("cancelAtPeriodEnd") boolean willCancelAtPeriodEnd)
|
||||
@JsonProperty("cancelAtPeriodEnd") boolean willCancelAtPeriodEnd,
|
||||
@JsonProperty("status") String status)
|
||||
{
|
||||
this.level = level;
|
||||
this.currency = currency;
|
||||
|
@ -47,6 +119,7 @@ public final class ActiveSubscription {
|
|||
this.isActive = isActive;
|
||||
this.billingCycleAnchor = billingCycleAnchor;
|
||||
this.willCancelAtPeriodEnd = willCancelAtPeriodEnd;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public int getLevel() {
|
||||
|
@ -89,5 +162,21 @@ public final class ActiveSubscription {
|
|||
public boolean willCancelAtPeriodEnd() {
|
||||
return willCancelAtPeriodEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Stripe status of this subscription (see https://stripe.com/docs/billing/subscriptions/overview#subscription-statuses)
|
||||
*/
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public boolean isInProgress() {
|
||||
return !isActive() &&
|
||||
!Status.isPaymentFailed(getStatus());
|
||||
}
|
||||
|
||||
public boolean isFailedPayment() {
|
||||
return Status.isPaymentFailed(getStatus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue