mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Implement isLongRunning wiring for receipt redemption jobs.
This commit is contained in:
committed by
Cody Henthorne
parent
9cc020a2c7
commit
5ac363232f
@@ -124,16 +124,16 @@ class ViewReceivedGiftViewModel(
|
||||
}
|
||||
else -> {
|
||||
Log.w(TAG, "Gift redemption job chain ignored due to in-progress jobs.", true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION))
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION, false))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Timeout awaiting for gift token redemption and profile refresh", true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION))
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION, false))
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
Log.w(TAG, "Interrupted awaiting for gift token redemption and profile refresh", true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION))
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION, false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -725,7 +725,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
}
|
||||
|
||||
private fun enqueueSubscriptionRedemption() {
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(-1L).enqueue()
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(-1L, false).enqueue()
|
||||
}
|
||||
|
||||
private fun enqueueSubscriptionKeepAlive() {
|
||||
|
||||
@@ -147,7 +147,7 @@ class MonthlyDonationRepository(private val donationsService: DonationsService)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubscriptionLevel(subscriptionLevel: String, uiSessionKey: Long): Completable {
|
||||
fun setSubscriptionLevel(subscriptionLevel: String, uiSessionKey: Long, isLongRunning: Boolean): Completable {
|
||||
return getOrCreateLevelUpdateOperation(subscriptionLevel)
|
||||
.flatMapCompletable { levelUpdateOperation ->
|
||||
val subscriber = SignalStore.donationsValues().requireSubscriber()
|
||||
@@ -186,7 +186,7 @@ class MonthlyDonationRepository(private val donationsService: DonationsService)
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
var finalJobState: JobTracker.JobState? = null
|
||||
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(uiSessionKey).enqueue { _, jobState ->
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(uiSessionKey, isLongRunning).enqueue { _, jobState ->
|
||||
if (jobState.isComplete) {
|
||||
finalJobState = jobState
|
||||
countDownLatch.countDown()
|
||||
@@ -206,16 +206,16 @@ class MonthlyDonationRepository(private val donationsService: DonationsService)
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Subscription request response job chain ignored due to in-progress jobs.", true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION))
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION, isLongRunning))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Subscription request response job timed out.", true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION))
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION, isLongRunning))
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
Log.w(TAG, "Subscription request response interrupted.", e, true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION))
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION, isLongRunning))
|
||||
}
|
||||
}
|
||||
}.doOnError {
|
||||
|
||||
@@ -112,7 +112,8 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
||||
additionalMessage: String?,
|
||||
badgeLevel: Long,
|
||||
donationProcessor: DonationProcessor,
|
||||
uiSessionKey: Long
|
||||
uiSessionKey: Long,
|
||||
isLongRunning: Boolean
|
||||
): Completable {
|
||||
val isBoost = badgeRecipient == Recipient.self().id
|
||||
val donationErrorSource: DonationErrorSource = if (isBoost) DonationErrorSource.BOOST else DonationErrorSource.GIFT
|
||||
@@ -132,9 +133,9 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
var finalJobState: JobTracker.JobState? = null
|
||||
val chain = if (isBoost) {
|
||||
BoostReceiptRequestResponseJob.createJobChainForBoost(paymentIntentId, donationProcessor, uiSessionKey)
|
||||
BoostReceiptRequestResponseJob.createJobChainForBoost(paymentIntentId, donationProcessor, uiSessionKey, isLongRunning)
|
||||
} else {
|
||||
BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, badgeRecipient, additionalMessage, badgeLevel, donationProcessor, uiSessionKey)
|
||||
BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, badgeRecipient, additionalMessage, badgeLevel, donationProcessor, uiSessionKey, isLongRunning)
|
||||
}
|
||||
|
||||
chain.enqueue { _, jobState ->
|
||||
@@ -157,16 +158,16 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "$donationTypeLabel request response job chain ignored due to in-progress jobs.", true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(donationErrorSource))
|
||||
it.onError(DonationError.timeoutWaitingForToken(donationErrorSource, isLongRunning))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "$donationTypeLabel job chain timed out waiting for job completion.", true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(donationErrorSource))
|
||||
it.onError(DonationError.timeoutWaitingForToken(donationErrorSource, isLongRunning))
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
Log.d(TAG, "$donationTypeLabel job chain interrupted", e, true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(donationErrorSource))
|
||||
it.onError(DonationError.timeoutWaitingForToken(donationErrorSource, isLongRunning))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@ sealed class DonateToSignalAction {
|
||||
data class DisplayCurrencySelectionDialog(val donateToSignalType: DonateToSignalType, val supportedCurrencies: List<String>) : DonateToSignalAction()
|
||||
data class DisplayGatewaySelectorDialog(val gatewayRequest: GatewayRequest) : DonateToSignalAction()
|
||||
data class CancelSubscription(val gatewayRequest: GatewayRequest) : DonateToSignalAction()
|
||||
data class UpdateSubscription(val gatewayRequest: GatewayRequest) : DonateToSignalAction()
|
||||
data class UpdateSubscription(val gatewayRequest: GatewayRequest, val isLongRunning: Boolean) : DonateToSignalAction()
|
||||
}
|
||||
|
||||
@@ -73,6 +73,9 @@ data class DonateToSignalState(
|
||||
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||
}
|
||||
|
||||
val isUpdateLongRunning: Boolean
|
||||
get() = monthlyDonationState.activeSubscription?.paymentMethod == ActiveSubscription.PAYMENT_METHOD_SEPA_DEBIT
|
||||
|
||||
data class OneTimeDonationState(
|
||||
val badge: Badge? = null,
|
||||
val selectedCurrency: Currency = SignalStore.donationsValues().getOneTimeCurrency(),
|
||||
|
||||
@@ -106,7 +106,7 @@ class DonateToSignalViewModel(
|
||||
fun updateSubscription() {
|
||||
val snapshot = store.state
|
||||
if (snapshot.areFieldsEnabled) {
|
||||
_actions.onNext(DonateToSignalAction.UpdateSubscription(createGatewayRequest(snapshot)))
|
||||
_actions.onNext(DonateToSignalAction.UpdateSubscription(createGatewayRequest(snapshot), snapshot.isUpdateLongRunning))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -262,6 +262,12 @@ class DonationCheckoutDelegate(
|
||||
return
|
||||
}
|
||||
|
||||
if (throwable is DonationError.BadgeRedemptionError.DonationPending) {
|
||||
Log.d(TAG, "Long-running donation is still pending.", true)
|
||||
// TODO [sepa] Pop donation pending sheet.
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Displaying donation error dialog.", true)
|
||||
errorDialog = DonationErrorDialogs.show(
|
||||
fragment!!.requireContext(),
|
||||
|
||||
@@ -83,7 +83,7 @@ class PayPalPaymentInProgressViewModel(
|
||||
Log.d(TAG, "Beginning subscription update...", true)
|
||||
|
||||
store.update { DonationProcessorStage.PAYMENT_PIPELINE }
|
||||
disposables += monthlyDonationRepository.cancelActiveSubscriptionIfNecessary().andThen(monthlyDonationRepository.setSubscriptionLevel(request.level.toString(), request.uiSessionKey))
|
||||
disposables += monthlyDonationRepository.cancelActiveSubscriptionIfNecessary().andThen(monthlyDonationRepository.setSubscriptionLevel(request.level.toString(), request.uiSessionKey, false))
|
||||
.subscribeBy(
|
||||
onComplete = {
|
||||
Log.w(TAG, "Completed subscription update", true)
|
||||
@@ -159,7 +159,8 @@ class PayPalPaymentInProgressViewModel(
|
||||
additionalMessage = request.additionalMessage,
|
||||
badgeLevel = request.level,
|
||||
donationProcessor = DonationProcessor.PAYPAL,
|
||||
uiSessionKey = request.uiSessionKey
|
||||
uiSessionKey = request.uiSessionKey,
|
||||
isLongRunning = false
|
||||
)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -192,7 +193,7 @@ class PayPalPaymentInProgressViewModel(
|
||||
.flatMapCompletable { payPalRepository.setDefaultPaymentMethod(it.paymentId) }
|
||||
.onErrorResumeNext { Completable.error(DonationError.getPaymentSetupError(DonationErrorSource.SUBSCRIPTION, it, PaymentSourceType.PayPal)) }
|
||||
|
||||
disposables += setup.andThen(monthlyDonationRepository.setSubscriptionLevel(request.level.toString(), request.uiSessionKey))
|
||||
disposables += setup.andThen(monthlyDonationRepository.setSubscriptionLevel(request.level.toString(), request.uiSessionKey, false))
|
||||
.subscribeBy(
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failure in monthly payment pipeline...", throwable, true)
|
||||
|
||||
@@ -66,7 +66,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
viewModel.processNewDonation(args.request, this::handleSecure3dsAction)
|
||||
}
|
||||
DonationProcessorAction.UPDATE_SUBSCRIPTION -> {
|
||||
viewModel.updateSubscription(args.request)
|
||||
viewModel.updateSubscription(args.request, args.isLongRunning)
|
||||
}
|
||||
DonationProcessorAction.CANCEL_SUBSCRIPTION -> {
|
||||
viewModel.cancelSubscription()
|
||||
|
||||
@@ -135,7 +135,7 @@ class StripePaymentInProgressViewModel(
|
||||
stripeRepository.createAndConfirmSetupIntent(it, paymentSourceProvider.paymentSourceType as PaymentSourceType.Stripe)
|
||||
}
|
||||
|
||||
val setLevel: Completable = monthlyDonationRepository.setSubscriptionLevel(request.level.toString(), request.uiSessionKey)
|
||||
val setLevel: Completable = monthlyDonationRepository.setSubscriptionLevel(request.level.toString(), request.uiSessionKey, paymentSourceProvider.paymentSourceType.isLongRunning)
|
||||
|
||||
Log.d(TAG, "Starting subscription payment pipeline...", true)
|
||||
store.update { DonationProcessorStage.PAYMENT_PIPELINE }
|
||||
@@ -205,7 +205,8 @@ class StripePaymentInProgressViewModel(
|
||||
additionalMessage = request.additionalMessage,
|
||||
badgeLevel = request.level,
|
||||
donationProcessor = DonationProcessor.STRIPE,
|
||||
uiSessionKey = request.uiSessionKey
|
||||
uiSessionKey = request.uiSessionKey,
|
||||
isLongRunning = paymentSource.type.isLongRunning
|
||||
)
|
||||
}
|
||||
}.subscribeBy(
|
||||
@@ -246,11 +247,10 @@ class StripePaymentInProgressViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
fun updateSubscription(request: GatewayRequest) {
|
||||
fun updateSubscription(request: GatewayRequest, isLongRunning: Boolean) {
|
||||
Log.d(TAG, "Beginning subscription update...", true)
|
||||
|
||||
store.update { DonationProcessorStage.PAYMENT_PIPELINE }
|
||||
disposables += monthlyDonationRepository.cancelActiveSubscriptionIfNecessary().andThen(monthlyDonationRepository.setSubscriptionLevel(request.level.toString(), request.uiSessionKey))
|
||||
disposables += monthlyDonationRepository.cancelActiveSubscriptionIfNecessary().andThen(monthlyDonationRepository.setSubscriptionLevel(request.level.toString(), request.uiSessionKey, isLongRunning))
|
||||
.subscribeBy(
|
||||
onComplete = {
|
||||
Log.w(TAG, "Completed subscription update", true)
|
||||
|
||||
@@ -88,6 +88,12 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
||||
* Errors that can occur during the badge redemption process.
|
||||
*/
|
||||
sealed class BadgeRedemptionError(source: DonationErrorSource, cause: Throwable) : DonationError(source, cause) {
|
||||
/**
|
||||
* Timeout elapsed while the user was waiting for badge redemption to complete for a long-running payment.
|
||||
* This is not an indication that redemption failed, just that it could take a few days to process the payment.
|
||||
*/
|
||||
class DonationPending(source: DonationErrorSource) : BadgeRedemptionError(source, Exception("Long-running donation is still pending."))
|
||||
|
||||
/**
|
||||
* Timeout elapsed while the user was waiting for badge redemption to complete. This is not an indication that
|
||||
* redemption failed, just that it is taking longer than we can reasonably show a spinner.
|
||||
@@ -210,7 +216,13 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
||||
fun invalidCurrencyForOneTimeDonation(source: DonationErrorSource): DonationError = OneTimeDonationError.InvalidCurrencyError(source)
|
||||
|
||||
@JvmStatic
|
||||
fun timeoutWaitingForToken(source: DonationErrorSource): DonationError = BadgeRedemptionError.TimeoutWaitingForTokenError(source)
|
||||
fun timeoutWaitingForToken(source: DonationErrorSource, isLongRunning: Boolean): DonationError {
|
||||
return if (isLongRunning) {
|
||||
BadgeRedemptionError.DonationPending(source)
|
||||
} else {
|
||||
BadgeRedemptionError.TimeoutWaitingForTokenError(source)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun genericBadgeRedemptionFailure(source: DonationErrorSource): DonationError = BadgeRedemptionError.GenericError(source)
|
||||
|
||||
@@ -34,6 +34,14 @@ class DonationErrorParams<V> private constructor(
|
||||
negativeAction = null
|
||||
)
|
||||
|
||||
// TODO [sepa] -- This is only used for the notification, and will be rare, but we should probably have better copy here.
|
||||
is DonationError.BadgeRedemptionError.DonationPending -> DonationErrorParams(
|
||||
title = R.string.DonationsErrors__still_processing,
|
||||
message = R.string.DonationsErrors__your_payment_is_still,
|
||||
positiveAction = callback.onOk(context),
|
||||
negativeAction = null
|
||||
)
|
||||
|
||||
is DonationError.BadgeRedemptionError.TimeoutWaitingForTokenError -> DonationErrorParams(
|
||||
title = R.string.DonationsErrors__still_processing,
|
||||
message = R.string.DonationsErrors__your_payment_is_still,
|
||||
|
||||
@@ -33,7 +33,8 @@ object ActiveSubscriptionPreference {
|
||||
val renewalTimestamp: Long = -1L,
|
||||
val redemptionState: ManageDonationsState.SubscriptionRedemptionState,
|
||||
val activeSubscription: ActiveSubscription.Subscription,
|
||||
val onContactSupport: () -> Unit
|
||||
val onContactSupport: () -> Unit,
|
||||
val onPendingClick: (FiatMoney) -> Unit
|
||||
) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return subscription.id == newItem.subscription.id
|
||||
@@ -57,6 +58,8 @@ object ActiveSubscriptionPreference {
|
||||
val progress: ProgressBar = itemView.findViewById(R.id.my_support_progress)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
itemView.setOnClickListener(null)
|
||||
|
||||
badge.setBadge(model.subscription.badge)
|
||||
|
||||
title.text = context.getString(
|
||||
@@ -72,6 +75,7 @@ object ActiveSubscriptionPreference {
|
||||
|
||||
when (model.redemptionState) {
|
||||
ManageDonationsState.SubscriptionRedemptionState.NONE -> presentRenewalState(model)
|
||||
ManageDonationsState.SubscriptionRedemptionState.IS_PENDING_BANK_TRANSFER -> presentPendingBankTransferState(model)
|
||||
ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS -> presentInProgressState()
|
||||
ManageDonationsState.SubscriptionRedemptionState.FAILED -> presentFailureState(model)
|
||||
}
|
||||
@@ -89,6 +93,13 @@ object ActiveSubscriptionPreference {
|
||||
progress.visible = false
|
||||
}
|
||||
|
||||
private fun presentPendingBankTransferState(model: Model) {
|
||||
expiry.text = context.getString(R.string.MySupportPreference__payment_pending)
|
||||
badge.alpha = 0.2f
|
||||
progress.visible = true
|
||||
itemView.setOnClickListener { model.onPendingClick(model.price) }
|
||||
}
|
||||
|
||||
private fun presentInProgressState() {
|
||||
expiry.text = context.getString(R.string.MySupportPreference__processing_transaction)
|
||||
badge.alpha = 0.2f
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.dp
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -25,6 +26,7 @@ import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadin
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.help.HelpFragment
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
@@ -199,7 +201,10 @@ class ManageDonationsFragment :
|
||||
requireActivity().finish()
|
||||
requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.DONATION_INDEX))
|
||||
},
|
||||
activeSubscription = activeSubscription
|
||||
activeSubscription = activeSubscription,
|
||||
onPendingClick = {
|
||||
displayPendingDialog(it)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -287,6 +292,22 @@ class ManageDonationsFragment :
|
||||
)
|
||||
}
|
||||
|
||||
private fun displayPendingDialog(fiatMoney: FiatMoney) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.MySupportPreference__payment_pending)
|
||||
.setMessage(
|
||||
getString(
|
||||
R.string.MySupportPreference__your_bank_transfer_of_s,
|
||||
FiatMoneyUtil.format(resources, fiatMoney, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
)
|
||||
)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(R.string.MySupportPreference__learn_more) { _, _ ->
|
||||
// TODO [sepa] Where this go?
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onMakeAMonthlyDonation() {
|
||||
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(DonateToSignalType.MONTHLY))
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ data class ManageDonationsState(
|
||||
private fun getStateFromActiveSubscription(activeSubscription: ActiveSubscription): SubscriptionRedemptionState? {
|
||||
return when {
|
||||
activeSubscription.isFailedPayment -> SubscriptionRedemptionState.FAILED
|
||||
activeSubscription.isPendingBankTransfer -> SubscriptionRedemptionState.IS_PENDING_BANK_TRANSFER
|
||||
activeSubscription.isInProgress -> SubscriptionRedemptionState.IN_PROGRESS
|
||||
else -> null
|
||||
}
|
||||
@@ -40,6 +41,7 @@ data class ManageDonationsState(
|
||||
enum class SubscriptionRedemptionState {
|
||||
NONE,
|
||||
IN_PROGRESS,
|
||||
IS_PENDING_BANK_TRANSFER,
|
||||
FAILED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,8 +41,9 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "BoostReceiptCredentialsSubmissionJob";
|
||||
|
||||
private static final String BOOST_QUEUE = "BoostReceiptRedemption";
|
||||
private static final String GIFT_QUEUE = "GiftReceiptRedemption";
|
||||
private static final String BOOST_QUEUE = "BoostReceiptRedemption";
|
||||
private static final String GIFT_QUEUE = "GiftReceiptRedemption";
|
||||
private static final String LONG_RUNNING_SUFFIX = "__LongRunning";
|
||||
|
||||
private static final String DATA_REQUEST_BYTES = "data.request.bytes";
|
||||
private static final String DATA_PAYMENT_INTENT_ID = "data.payment.intent.id";
|
||||
@@ -50,6 +51,7 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
private static final String DATA_BADGE_LEVEL = "data.badge.level";
|
||||
private static final String DATA_DONATION_PROCESSOR = "data.donation.processor";
|
||||
private static final String DATA_UI_SESSION_KEY = "data.ui.session.key";
|
||||
private static final String DATA_IS_LONG_RUNNING = "data.is.long.running";
|
||||
|
||||
private ReceiptCredentialRequestContext requestContext;
|
||||
|
||||
@@ -58,14 +60,24 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
private final long badgeLevel;
|
||||
private final DonationProcessor donationProcessor;
|
||||
private final long uiSessionKey;
|
||||
private final boolean isLongRunning;
|
||||
|
||||
private static BoostReceiptRequestResponseJob createJob(String paymentIntentId, DonationErrorSource donationErrorSource, long badgeLevel, DonationProcessor donationProcessor, long uiSessionKey) {
|
||||
private static String resolveQueue(DonationErrorSource donationErrorSource, boolean isLongRunning) {
|
||||
String baseQueue = donationErrorSource == DonationErrorSource.BOOST ? BOOST_QUEUE : GIFT_QUEUE;
|
||||
return isLongRunning ? baseQueue + LONG_RUNNING_SUFFIX : baseQueue;
|
||||
}
|
||||
|
||||
private static long resolveLifespan(boolean isLongRunning) {
|
||||
return isLongRunning ? TimeUnit.DAYS.toMillis(14) : TimeUnit.DAYS.toMillis(1);
|
||||
}
|
||||
|
||||
private static BoostReceiptRequestResponseJob createJob(String paymentIntentId, DonationErrorSource donationErrorSource, long badgeLevel, DonationProcessor donationProcessor, long uiSessionKey, boolean isLongRunning) {
|
||||
return new BoostReceiptRequestResponseJob(
|
||||
new Parameters
|
||||
.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(donationErrorSource == DonationErrorSource.BOOST ? BOOST_QUEUE : GIFT_QUEUE)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setQueue(resolveQueue(donationErrorSource, isLongRunning))
|
||||
.setLifespan(resolveLifespan(isLongRunning))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
null,
|
||||
@@ -73,15 +85,17 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
donationErrorSource,
|
||||
badgeLevel,
|
||||
donationProcessor,
|
||||
uiSessionKey
|
||||
uiSessionKey,
|
||||
isLongRunning
|
||||
);
|
||||
}
|
||||
|
||||
public static JobManager.Chain createJobChainForBoost(@NonNull String paymentIntentId,
|
||||
@NonNull DonationProcessor donationProcessor,
|
||||
long uiSessionKey)
|
||||
long uiSessionKey,
|
||||
boolean isLongRunning)
|
||||
{
|
||||
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.BOOST, Long.parseLong(SubscriptionLevels.BOOST_LEVEL), donationProcessor, uiSessionKey);
|
||||
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.BOOST, Long.parseLong(SubscriptionLevels.BOOST_LEVEL), donationProcessor, uiSessionKey, isLongRunning);
|
||||
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForBoost(uiSessionKey);
|
||||
RefreshOwnProfileJob refreshOwnProfileJob = RefreshOwnProfileJob.forBoost();
|
||||
MultiDeviceProfileContentUpdateJob multiDeviceProfileContentUpdateJob = new MultiDeviceProfileContentUpdateJob();
|
||||
@@ -98,9 +112,10 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
@Nullable String additionalMessage,
|
||||
long badgeLevel,
|
||||
@NonNull DonationProcessor donationProcessor,
|
||||
long uiSessionKey)
|
||||
long uiSessionKey,
|
||||
boolean isLongRunning)
|
||||
{
|
||||
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.GIFT, badgeLevel, donationProcessor, uiSessionKey);
|
||||
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.GIFT, badgeLevel, donationProcessor, uiSessionKey, isLongRunning);
|
||||
GiftSendJob giftSendJob = new GiftSendJob(recipientId, additionalMessage);
|
||||
|
||||
|
||||
@@ -115,7 +130,8 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
@NonNull DonationErrorSource donationErrorSource,
|
||||
long badgeLevel,
|
||||
@NonNull DonationProcessor donationProcessor,
|
||||
long uiSessionKey)
|
||||
long uiSessionKey,
|
||||
boolean isLongRunning)
|
||||
{
|
||||
super(parameters);
|
||||
this.requestContext = requestContext;
|
||||
@@ -124,6 +140,7 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
this.badgeLevel = badgeLevel;
|
||||
this.donationProcessor = donationProcessor;
|
||||
this.uiSessionKey = uiSessionKey;
|
||||
this.isLongRunning = isLongRunning;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -132,7 +149,8 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
.putString(DATA_ERROR_SOURCE, donationErrorSource.serialize())
|
||||
.putLong(DATA_BADGE_LEVEL, badgeLevel)
|
||||
.putString(DATA_DONATION_PROCESSOR, donationProcessor.getCode())
|
||||
.putLong(DATA_UI_SESSION_KEY, uiSessionKey);
|
||||
.putLong(DATA_UI_SESSION_KEY, uiSessionKey)
|
||||
.putBoolean(DATA_IS_LONG_RUNNING, isLongRunning);
|
||||
|
||||
if (requestContext != null) {
|
||||
builder.putBlobAsString(DATA_REQUEST_BYTES, requestContext.serialize());
|
||||
@@ -150,6 +168,15 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
public void onFailure() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNextRunAttemptBackoff(int pastAttemptCount, @NonNull Exception exception) {
|
||||
if (isLongRunning) {
|
||||
return TimeUnit.DAYS.toMillis(1);
|
||||
} else {
|
||||
return super.getNextRunAttemptBackoff(pastAttemptCount, exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
if (requestContext == null) {
|
||||
@@ -283,15 +310,16 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||
String rawDonationProcessor = data.getStringOrDefault(DATA_DONATION_PROCESSOR, DonationProcessor.STRIPE.getCode());
|
||||
DonationProcessor donationProcessor = DonationProcessor.fromCode(rawDonationProcessor);
|
||||
long uiSessionKey = data.getLongOrDefault(DATA_UI_SESSION_KEY, -1L);
|
||||
boolean isLongRunning = data.getBooleanOrDefault(DATA_IS_LONG_RUNNING, false);
|
||||
|
||||
try {
|
||||
if (data.hasString(DATA_REQUEST_BYTES)) {
|
||||
byte[] blob = data.getStringAsBlob(DATA_REQUEST_BYTES);
|
||||
ReceiptCredentialRequestContext requestContext = new ReceiptCredentialRequestContext(blob);
|
||||
|
||||
return new BoostReceiptRequestResponseJob(parameters, requestContext, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor, uiSessionKey);
|
||||
return new BoostReceiptRequestResponseJob(parameters, requestContext, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor, uiSessionKey, isLongRunning);
|
||||
} else {
|
||||
return new BoostReceiptRequestResponseJob(parameters, null, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor, uiSessionKey);
|
||||
return new BoostReceiptRequestResponseJob(parameters, null, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor, uiSessionKey, isLongRunning);
|
||||
}
|
||||
} catch (InvalidInputException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@@ -131,11 +132,13 @@ public class SubscriptionKeepAliveJob extends BaseJob {
|
||||
MultiDeviceSubscriptionSyncRequestJob.enqueue();
|
||||
}
|
||||
|
||||
boolean isLongRunning = Objects.equals(activeSubscription.getActiveSubscription().getPaymentMethod(), ActiveSubscription.PAYMENT_METHOD_SEPA_DEBIT);
|
||||
if (endOfCurrentPeriod > SignalStore.donationsValues().getSubscriptionEndOfPeriodConversionStarted()) {
|
||||
Log.i(TAG, "Subscription end of period is after the conversion end of period. Storing it, generating a credential, and enqueuing the continuation job chain.", true);
|
||||
SignalStore.donationsValues().setSubscriptionEndOfPeriodConversionStarted(endOfCurrentPeriod);
|
||||
SignalStore.donationsValues().refreshSubscriptionRequestCredential();
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(true, -1L).enqueue();
|
||||
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(true, -1L, isLongRunning).enqueue();
|
||||
} else if (endOfCurrentPeriod > SignalStore.donationsValues().getSubscriptionEndOfPeriodRedemptionStarted()) {
|
||||
if (SignalStore.donationsValues().getSubscriptionRequestCredential() == null) {
|
||||
Log.i(TAG, "We have not started a redemption, but do not have a request credential. Possible that the subscription changed.", true);
|
||||
@@ -143,7 +146,7 @@ public class SubscriptionKeepAliveJob extends BaseJob {
|
||||
}
|
||||
|
||||
Log.i(TAG, "We have a request credential and have not yet turned it into a redeemable token.", true);
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(true, -1L).enqueue();
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(true, -1L, isLongRunning).enqueue();
|
||||
} else if (endOfCurrentPeriod > SignalStore.donationsValues().getSubscriptionEndOfPeriodRedeemed()) {
|
||||
if (SignalStore.donationsValues().getSubscriptionReceiptCredential() == null) {
|
||||
Log.i(TAG, "We have successfully started redemption but have no stored token. Possible that the subscription changed.", true);
|
||||
|
||||
@@ -48,36 +48,39 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
||||
private static final String DATA_SUBSCRIBER_ID = "data.subscriber.id";
|
||||
private static final String DATA_IS_FOR_KEEP_ALIVE = "data.is.for.keep.alive";
|
||||
private static final String DATA_UI_SESSION_KEY = "data.ui.session.key";
|
||||
private static final String DATA_IS_LONG_RUNNING = "data.is.long.running";
|
||||
|
||||
public static final Object MUTEX = new Object();
|
||||
|
||||
private final SubscriberId subscriberId;
|
||||
private final boolean isForKeepAlive;
|
||||
private final long uiSessionKey;
|
||||
private final boolean isLongRunning;
|
||||
|
||||
private static SubscriptionReceiptRequestResponseJob createJob(SubscriberId subscriberId, boolean isForKeepAlive, long uiSessionKey) {
|
||||
private static SubscriptionReceiptRequestResponseJob createJob(SubscriberId subscriberId, boolean isForKeepAlive, long uiSessionKey, boolean isLongRunning) {
|
||||
return new SubscriptionReceiptRequestResponseJob(
|
||||
new Parameters
|
||||
.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue("ReceiptRedemption")
|
||||
.setMaxInstancesForQueue(1)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setLifespan(isLongRunning ? TimeUnit.DAYS.toMillis(14) : TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
subscriberId,
|
||||
isForKeepAlive,
|
||||
uiSessionKey
|
||||
uiSessionKey,
|
||||
isLongRunning
|
||||
);
|
||||
}
|
||||
|
||||
public static JobManager.Chain createSubscriptionContinuationJobChain(long uiSessionKey) {
|
||||
return createSubscriptionContinuationJobChain(false, uiSessionKey);
|
||||
public static JobManager.Chain createSubscriptionContinuationJobChain(long uiSessionKey, boolean isLongRunning) {
|
||||
return createSubscriptionContinuationJobChain(false, uiSessionKey, isLongRunning);
|
||||
}
|
||||
|
||||
public static JobManager.Chain createSubscriptionContinuationJobChain(boolean isForKeepAlive, long uiSessionKey) {
|
||||
public static JobManager.Chain createSubscriptionContinuationJobChain(boolean isForKeepAlive, long uiSessionKey, boolean isLongRunning) {
|
||||
Subscriber subscriber = SignalStore.donationsValues().requireSubscriber();
|
||||
SubscriptionReceiptRequestResponseJob requestReceiptJob = createJob(subscriber.getSubscriberId(), isForKeepAlive, uiSessionKey);
|
||||
SubscriptionReceiptRequestResponseJob requestReceiptJob = createJob(subscriber.getSubscriberId(), isForKeepAlive, uiSessionKey, isLongRunning);
|
||||
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForSubscription(requestReceiptJob.getErrorSource(), uiSessionKey);
|
||||
RefreshOwnProfileJob refreshOwnProfileJob = RefreshOwnProfileJob.forSubscription();
|
||||
MultiDeviceProfileContentUpdateJob multiDeviceProfileContentUpdateJob = new MultiDeviceProfileContentUpdateJob();
|
||||
@@ -92,19 +95,22 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
||||
private SubscriptionReceiptRequestResponseJob(@NonNull Parameters parameters,
|
||||
@NonNull SubscriberId subscriberId,
|
||||
boolean isForKeepAlive,
|
||||
long uiSessionKey)
|
||||
long uiSessionKey,
|
||||
boolean isLongRunning)
|
||||
{
|
||||
super(parameters);
|
||||
this.subscriberId = subscriberId;
|
||||
this.isForKeepAlive = isForKeepAlive;
|
||||
this.uiSessionKey = uiSessionKey;
|
||||
this.isLongRunning = isLongRunning;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable byte[] serialize() {
|
||||
JsonJobData.Builder builder = new JsonJobData.Builder().putBlobAsString(DATA_SUBSCRIBER_ID, subscriberId.getBytes())
|
||||
.putBoolean(DATA_IS_FOR_KEEP_ALIVE, isForKeepAlive)
|
||||
.putLong(DATA_UI_SESSION_KEY, uiSessionKey);
|
||||
.putLong(DATA_UI_SESSION_KEY, uiSessionKey)
|
||||
.putBoolean(DATA_IS_LONG_RUNNING, isLongRunning);
|
||||
|
||||
return builder.serialize();
|
||||
}
|
||||
@@ -436,6 +442,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
||||
String requestString = data.getStringOrDefault(DATA_REQUEST_BYTES, null);
|
||||
byte[] requestContextBytes = requestString != null ? Base64.decodeOrThrow(requestString) : null;
|
||||
long uiSessionKey = data.getLongOrDefault(DATA_UI_SESSION_KEY, -1L);
|
||||
boolean isLongRunning = data.getBooleanOrDefault(DATA_IS_LONG_RUNNING, false);
|
||||
|
||||
ReceiptCredentialRequestContext requestContext;
|
||||
if (requestContextBytes != null && SignalStore.donationsValues().getSubscriptionRequestCredential() == null) {
|
||||
@@ -448,7 +455,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
||||
}
|
||||
}
|
||||
|
||||
return new SubscriptionReceiptRequestResponseJob(parameters, subscriberId, isForKeepAlive, uiSessionKey);
|
||||
return new SubscriptionReceiptRequestResponseJob(parameters, subscriberId, isForKeepAlive, uiSessionKey, isLongRunning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,12 @@
|
||||
android:name="request"
|
||||
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest"
|
||||
app:nullable="false" />
|
||||
|
||||
<argument
|
||||
android:name="is_long_running"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_stripePaymentInProgressFragment_to_stripe3dsDialogFragment"
|
||||
app:destination="@id/stripe3dsDialogFragment" />
|
||||
|
||||
@@ -95,6 +95,11 @@
|
||||
android:name="request"
|
||||
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest"
|
||||
app:nullable="false" />
|
||||
|
||||
<argument
|
||||
android:name="is_long_running"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
<action
|
||||
android:id="@+id/action_stripePaymentInProgressFragment_to_stripe3dsDialogFragment"
|
||||
app:destination="@id/stripe3dsDialogFragment" />
|
||||
|
||||
@@ -4761,6 +4761,12 @@
|
||||
<!-- Displayed on "My Support" screen when user badge failed to be added to their account -->
|
||||
<string name="MySupportPreference__couldnt_add_badge_s">Couldn\'t add badge. %1$s</string>
|
||||
<string name="MySupportPreference__please_contact_support">Please contact support.</string>
|
||||
<!-- Displayed as a subtitle on a row in the Manage Donations screen when payment for a donation is pending -->
|
||||
<string name="MySupportPreference__payment_pending">Payment pending</string>
|
||||
<!-- Displayed as a dialog message when clicking on a donation row that is pending. Placeholder is a formatted fiat amount -->
|
||||
<string name="MySupportPreference__your_bank_transfer_of_s">Your bank transfer of %1$s is pending. Bank transfers usually take 1 business day to complete. </string>
|
||||
<!-- Displayed in the pending help dialog, used to launch user to more details about bank transfers -->
|
||||
<string name="MySupportPreference__learn_more">Learn more</string>
|
||||
|
||||
<!-- Title of dialog telling user they need to update signal as it expired -->
|
||||
<string name="UpdateSignalExpiredDialog__title">Update Signal</string>
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.signal.donations
|
||||
|
||||
sealed class PaymentSourceType {
|
||||
abstract val code: String
|
||||
open val isLongRunning: Boolean = false
|
||||
|
||||
object Unknown : PaymentSourceType() {
|
||||
override val code: String = Codes.UNKNOWN.code
|
||||
@@ -11,10 +12,10 @@ sealed class PaymentSourceType {
|
||||
override val code: String = Codes.PAY_PAL.code
|
||||
}
|
||||
|
||||
sealed class Stripe(override val code: String, val paymentMethod: String) : PaymentSourceType() {
|
||||
object CreditCard : Stripe(Codes.CREDIT_CARD.code, "CARD")
|
||||
object GooglePay : Stripe(Codes.GOOGLE_PAY.code, "CARD")
|
||||
object SEPADebit : Stripe(Codes.SEPA_DEBIT.code, "SEPA_DEBIT")
|
||||
sealed class Stripe(override val code: String, val paymentMethod: String, override val isLongRunning: Boolean) : PaymentSourceType() {
|
||||
object CreditCard : Stripe(Codes.CREDIT_CARD.code, "CARD", false)
|
||||
object GooglePay : Stripe(Codes.GOOGLE_PAY.code, "CARD", false)
|
||||
object SEPADebit : Stripe(Codes.SEPA_DEBIT.code, "SEPA_DEBIT", true)
|
||||
}
|
||||
|
||||
private enum class Codes(val code: String) {
|
||||
|
||||
@@ -13,6 +13,8 @@ import javax.annotation.Nullable;
|
||||
|
||||
public final class ActiveSubscription {
|
||||
|
||||
public static final String PAYMENT_METHOD_SEPA_DEBIT = "SEPA_DEBIT";
|
||||
|
||||
public static final ActiveSubscription EMPTY = new ActiveSubscription(null, null);
|
||||
|
||||
public enum Processor {
|
||||
@@ -129,6 +131,10 @@ public final class ActiveSubscription {
|
||||
return activeSubscription != null && activeSubscription.isActive();
|
||||
}
|
||||
|
||||
public boolean isPendingBankTransfer() {
|
||||
return activeSubscription != null && Objects.equals(activeSubscription.paymentMethod, PAYMENT_METHOD_SEPA_DEBIT) && activeSubscription.paymentPending;
|
||||
}
|
||||
|
||||
public boolean isInProgress() {
|
||||
return activeSubscription != null && !isActive() && !activeSubscription.isFailedPayment();
|
||||
}
|
||||
@@ -147,6 +153,8 @@ public final class ActiveSubscription {
|
||||
private final boolean willCancelAtPeriodEnd;
|
||||
private final String status;
|
||||
private final Processor processor;
|
||||
private final String paymentMethod;
|
||||
private final boolean paymentPending;
|
||||
|
||||
@JsonCreator
|
||||
public Subscription(@JsonProperty("level") int level,
|
||||
@@ -157,7 +165,9 @@ public final class ActiveSubscription {
|
||||
@JsonProperty("billingCycleAnchor") long billingCycleAnchor,
|
||||
@JsonProperty("cancelAtPeriodEnd") boolean willCancelAtPeriodEnd,
|
||||
@JsonProperty("status") String status,
|
||||
@JsonProperty("processor") String processor)
|
||||
@JsonProperty("processor") String processor,
|
||||
@JsonProperty("paymentMethod") String paymentMethod,
|
||||
@JsonProperty("paymentPending") boolean paymentPending)
|
||||
{
|
||||
this.level = level;
|
||||
this.currency = currency;
|
||||
@@ -168,6 +178,8 @@ public final class ActiveSubscription {
|
||||
this.willCancelAtPeriodEnd = willCancelAtPeriodEnd;
|
||||
this.status = status;
|
||||
this.processor = Processor.fromCode(processor);
|
||||
this.paymentMethod = paymentMethod;
|
||||
this.paymentPending = paymentPending;
|
||||
}
|
||||
|
||||
public int getLevel() {
|
||||
@@ -222,8 +234,15 @@ public final class ActiveSubscription {
|
||||
return processor;
|
||||
}
|
||||
|
||||
public boolean isInProgress() {
|
||||
return !isActive() && !Status.isPaymentFailed(getStatus());
|
||||
public String getPaymentMethod() {
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether the latest invoice for the subscription is in a non-terminal state
|
||||
*/
|
||||
public boolean isPaymentPending() {
|
||||
return paymentPending;
|
||||
}
|
||||
|
||||
public boolean isFailedPayment() {
|
||||
@@ -234,16 +253,18 @@ public final class ActiveSubscription {
|
||||
return Status.getStatus(getStatus()) == Status.CANCELED;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object o) {
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
final Subscription that = (Subscription) o;
|
||||
return level == that.level && endOfCurrentPeriod == that.endOfCurrentPeriod && isActive == that.isActive && billingCycleAnchor == that.billingCycleAnchor && willCancelAtPeriodEnd == that.willCancelAtPeriodEnd && currency
|
||||
.equals(that.currency) && amount.equals(that.amount) && status.equals(that.status);
|
||||
.equals(that.currency) && amount.equals(that.amount) && status.equals(that.status) && Objects.equals(paymentMethod, that.paymentMethod) && paymentPending == that.paymentPending;
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
return Objects.hash(level, currency, amount, endOfCurrentPeriod, isActive, billingCycleAnchor, willCancelAtPeriodEnd, status);
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(level, currency, amount, endOfCurrentPeriod, isActive, billingCycleAnchor, willCancelAtPeriodEnd, status, paymentMethod, paymentPending);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user