mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Implement bank transfer pending sheet.
This commit is contained in:
committed by
Cody Henthorne
parent
c17d6c2334
commit
0dd17673f5
@@ -283,4 +283,8 @@ class GiftFlowConfirmationFragment :
|
||||
override fun onUserCancelledPaymentFlow() {
|
||||
findNavController().popBackStack(R.id.giftFlowConfirmationFragment, false)
|
||||
}
|
||||
|
||||
override fun navigateToDonationPending(gatewayRequest: GatewayRequest) {
|
||||
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToDonationPendingBottomSheet(gatewayRequest))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, false))
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Timeout awaiting for gift token redemption and profile refresh", true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION, false))
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION))
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
Log.w(TAG, "Interrupted awaiting for gift token redemption and profile refresh", true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION, false))
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
@@ -147,7 +148,10 @@ class MonthlyDonationRepository(private val donationsService: DonationsService)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubscriptionLevel(subscriptionLevel: String, uiSessionKey: Long, isLongRunning: Boolean): Completable {
|
||||
fun setSubscriptionLevel(gatewayRequest: GatewayRequest, isLongRunning: Boolean): Completable {
|
||||
val subscriptionLevel = gatewayRequest.level.toString()
|
||||
val uiSessionKey = gatewayRequest.uiSessionKey
|
||||
|
||||
return getOrCreateLevelUpdateOperation(subscriptionLevel)
|
||||
.flatMapCompletable { levelUpdateOperation ->
|
||||
val subscriber = SignalStore.donationsValues().requireSubscriber()
|
||||
@@ -193,6 +197,12 @@ class MonthlyDonationRepository(private val donationsService: DonationsService)
|
||||
}
|
||||
}
|
||||
|
||||
val timeoutError: DonationError = if (isLongRunning) {
|
||||
DonationError.donationPending(DonationErrorSource.SUBSCRIPTION, gatewayRequest)
|
||||
} else {
|
||||
DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION)
|
||||
}
|
||||
|
||||
try {
|
||||
if (countDownLatch.await(10, TimeUnit.SECONDS)) {
|
||||
when (finalJobState) {
|
||||
@@ -206,16 +216,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, isLongRunning))
|
||||
it.onError(timeoutError)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Subscription request response job timed out.", true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION, isLongRunning))
|
||||
it.onError(timeoutError)
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
Log.w(TAG, "Subscription request response interrupted.", e, true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION, isLongRunning))
|
||||
it.onError(timeoutError)
|
||||
}
|
||||
}
|
||||
}.doOnError {
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
@@ -106,23 +107,19 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
||||
}
|
||||
|
||||
fun waitForOneTimeRedemption(
|
||||
price: FiatMoney,
|
||||
gatewayRequest: GatewayRequest,
|
||||
paymentIntentId: String,
|
||||
badgeRecipient: RecipientId,
|
||||
additionalMessage: String?,
|
||||
badgeLevel: Long,
|
||||
donationProcessor: DonationProcessor,
|
||||
uiSessionKey: Long,
|
||||
isLongRunning: Boolean
|
||||
): Completable {
|
||||
val isBoost = badgeRecipient == Recipient.self().id
|
||||
val isBoost = gatewayRequest.recipientId == Recipient.self().id
|
||||
val donationErrorSource: DonationErrorSource = if (isBoost) DonationErrorSource.BOOST else DonationErrorSource.GIFT
|
||||
|
||||
val waitOnRedemption = Completable.create {
|
||||
val donationReceiptRecord = if (isBoost) {
|
||||
DonationReceiptRecord.createForBoost(price)
|
||||
DonationReceiptRecord.createForBoost(gatewayRequest.fiat)
|
||||
} else {
|
||||
DonationReceiptRecord.createForGift(price)
|
||||
DonationReceiptRecord.createForGift(gatewayRequest.fiat)
|
||||
}
|
||||
|
||||
val donationTypeLabel = donationReceiptRecord.type.code.replaceFirstChar { c -> if (c.isLowerCase()) c.titlecase(Locale.US) else c.toString() }
|
||||
@@ -133,9 +130,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, isLongRunning)
|
||||
BoostReceiptRequestResponseJob.createJobChainForBoost(paymentIntentId, donationProcessor, gatewayRequest.uiSessionKey, isLongRunning)
|
||||
} else {
|
||||
BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, badgeRecipient, additionalMessage, badgeLevel, donationProcessor, uiSessionKey, isLongRunning)
|
||||
BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, gatewayRequest.recipientId, gatewayRequest.additionalMessage, gatewayRequest.level, donationProcessor, gatewayRequest.uiSessionKey, isLongRunning)
|
||||
}
|
||||
|
||||
chain.enqueue { _, jobState ->
|
||||
@@ -145,6 +142,12 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
||||
}
|
||||
}
|
||||
|
||||
val timeoutError: DonationError = if (isLongRunning) {
|
||||
DonationError.donationPending(donationErrorSource, gatewayRequest)
|
||||
} else {
|
||||
DonationError.timeoutWaitingForToken(donationErrorSource)
|
||||
}
|
||||
|
||||
try {
|
||||
if (countDownLatch.await(10, TimeUnit.SECONDS)) {
|
||||
when (finalJobState) {
|
||||
@@ -158,16 +161,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, isLongRunning))
|
||||
it.onError(timeoutError)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "$donationTypeLabel job chain timed out waiting for job completion.", true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(donationErrorSource, isLongRunning))
|
||||
it.onError(timeoutError)
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
Log.d(TAG, "$donationTypeLabel job chain interrupted", e, true)
|
||||
it.onError(DonationError.timeoutWaitingForToken(donationErrorSource, isLongRunning))
|
||||
it.onError(timeoutError)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -432,4 +432,8 @@ class DonateToSignalFragment :
|
||||
override fun onUserCancelledPaymentFlow() {
|
||||
findNavController().popBackStack(R.id.donateToSignalFragment, false)
|
||||
}
|
||||
|
||||
override fun navigateToDonationPending(gatewayRequest: GatewayRequest) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToDonationPendingBottomSheet(gatewayRequest))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,11 +210,11 @@ class DonationCheckoutDelegate(
|
||||
|
||||
private var fragment: Fragment? = null
|
||||
private var errorDialog: DialogInterface? = null
|
||||
private var userCancelledFlowCallback: UserCancelledFlowCallback? = null
|
||||
private var errorHandlerCallback: ErrorHandlerCallback? = null
|
||||
|
||||
fun attach(fragment: Fragment, userCancelledFlowCallback: UserCancelledFlowCallback?, uiSessionKey: Long, errorSource: DonationErrorSource, vararg additionalSources: DonationErrorSource) {
|
||||
fun attach(fragment: Fragment, errorHandlerCallback: ErrorHandlerCallback?, uiSessionKey: Long, errorSource: DonationErrorSource, vararg additionalSources: DonationErrorSource) {
|
||||
this.fragment = fragment
|
||||
this.userCancelledFlowCallback = userCancelledFlowCallback
|
||||
this.errorHandlerCallback = errorHandlerCallback
|
||||
|
||||
val disposables = LifecycleDisposable()
|
||||
fragment.viewLifecycleOwner.lifecycle.addObserver(this)
|
||||
@@ -231,7 +231,7 @@ class DonationCheckoutDelegate(
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
errorDialog?.dismiss()
|
||||
fragment = null
|
||||
userCancelledFlowCallback = null
|
||||
errorHandlerCallback = null
|
||||
}
|
||||
|
||||
private fun registerErrorSource(errorSource: DonationErrorSource): Disposable {
|
||||
@@ -264,7 +264,7 @@ class DonationCheckoutDelegate(
|
||||
|
||||
if (throwable is DonationError.BadgeRedemptionError.DonationPending) {
|
||||
Log.d(TAG, "Long-running donation is still pending.", true)
|
||||
// TODO [sepa] Pop donation pending sheet.
|
||||
errorHandlerCallback?.navigateToDonationPending(throwable.gatewayRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -295,11 +295,12 @@ class DonationCheckoutDelegate(
|
||||
}
|
||||
}
|
||||
|
||||
interface UserCancelledFlowCallback {
|
||||
interface ErrorHandlerCallback {
|
||||
fun onUserCancelledPaymentFlow()
|
||||
fun navigateToDonationPending(gatewayRequest: GatewayRequest)
|
||||
}
|
||||
|
||||
interface Callback : UserCancelledFlowCallback {
|
||||
interface Callback : ErrorHandlerCallback {
|
||||
fun navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest)
|
||||
fun navigateToPayPalPaymentInProgress(gatewayRequest: GatewayRequest)
|
||||
fun navigateToCreditCardForm(gatewayRequest: GatewayRequest)
|
||||
|
||||
@@ -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, false))
|
||||
disposables += monthlyDonationRepository.cancelActiveSubscriptionIfNecessary().andThen(monthlyDonationRepository.setSubscriptionLevel(request, false))
|
||||
.subscribeBy(
|
||||
onComplete = {
|
||||
Log.w(TAG, "Completed subscription update", true)
|
||||
@@ -153,13 +153,9 @@ class PayPalPaymentInProgressViewModel(
|
||||
}
|
||||
.flatMapCompletable { response ->
|
||||
oneTimeDonationRepository.waitForOneTimeRedemption(
|
||||
price = request.fiat,
|
||||
gatewayRequest = request,
|
||||
paymentIntentId = response.paymentId,
|
||||
badgeRecipient = request.recipientId,
|
||||
additionalMessage = request.additionalMessage,
|
||||
badgeLevel = request.level,
|
||||
donationProcessor = DonationProcessor.PAYPAL,
|
||||
uiSessionKey = request.uiSessionKey,
|
||||
isLongRunning = false
|
||||
)
|
||||
}
|
||||
@@ -193,7 +189,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, false))
|
||||
disposables += setup.andThen(monthlyDonationRepository.setSubscriptionLevel(request, false))
|
||||
.subscribeBy(
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failure in monthly payment pipeline...", throwable, true)
|
||||
|
||||
@@ -135,7 +135,7 @@ class StripePaymentInProgressViewModel(
|
||||
stripeRepository.createAndConfirmSetupIntent(it, paymentSourceProvider.paymentSourceType as PaymentSourceType.Stripe)
|
||||
}
|
||||
|
||||
val setLevel: Completable = monthlyDonationRepository.setSubscriptionLevel(request.level.toString(), request.uiSessionKey, paymentSourceProvider.paymentSourceType.isLongRunning)
|
||||
val setLevel: Completable = monthlyDonationRepository.setSubscriptionLevel(request, paymentSourceProvider.paymentSourceType.isLongRunning)
|
||||
|
||||
Log.d(TAG, "Starting subscription payment pipeline...", true)
|
||||
store.update { DonationProcessorStage.PAYMENT_PIPELINE }
|
||||
@@ -199,13 +199,9 @@ class StripePaymentInProgressViewModel(
|
||||
.flatMap { stripeRepository.getStatusAndPaymentMethodId(it) }
|
||||
.flatMapCompletable {
|
||||
oneTimeDonationRepository.waitForOneTimeRedemption(
|
||||
price = amount,
|
||||
gatewayRequest = request,
|
||||
paymentIntentId = paymentIntent.intentId,
|
||||
badgeRecipient = request.recipientId,
|
||||
additionalMessage = request.additionalMessage,
|
||||
badgeLevel = request.level,
|
||||
donationProcessor = DonationProcessor.STRIPE,
|
||||
uiSessionKey = request.uiSessionKey,
|
||||
isLongRunning = paymentSource.type.isLongRunning
|
||||
)
|
||||
}
|
||||
@@ -250,7 +246,7 @@ class StripePaymentInProgressViewModel(
|
||||
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, isLongRunning))
|
||||
disposables += monthlyDonationRepository.cancelActiveSubscriptionIfNecessary().andThen(monthlyDonationRepository.setSubscriptionLevel(request, isLongRunning))
|
||||
.subscribeBy(
|
||||
onComplete = {
|
||||
Log.w(TAG, "Completed subscription update", true)
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.signal.donations.StripeError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
|
||||
sealed class DonationError(val source: DonationErrorSource, cause: Throwable) : Exception(cause) {
|
||||
|
||||
@@ -92,7 +93,7 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
||||
* 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."))
|
||||
class DonationPending(source: DonationErrorSource, val gatewayRequest: GatewayRequest) : 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
|
||||
@@ -216,13 +217,10 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
||||
fun invalidCurrencyForOneTimeDonation(source: DonationErrorSource): DonationError = OneTimeDonationError.InvalidCurrencyError(source)
|
||||
|
||||
@JvmStatic
|
||||
fun timeoutWaitingForToken(source: DonationErrorSource, isLongRunning: Boolean): DonationError {
|
||||
return if (isLongRunning) {
|
||||
BadgeRedemptionError.DonationPending(source)
|
||||
} else {
|
||||
BadgeRedemptionError.TimeoutWaitingForTokenError(source)
|
||||
}
|
||||
}
|
||||
fun timeoutWaitingForToken(source: DonationErrorSource): DonationError = BadgeRedemptionError.TimeoutWaitingForTokenError(source)
|
||||
|
||||
@JvmStatic
|
||||
fun donationPending(source: DonationErrorSource, gatewayRequest: GatewayRequest) = BadgeRedemptionError.DonationPending(source, gatewayRequest)
|
||||
|
||||
@JvmStatic
|
||||
fun genericBadgeRedemptionFailure(source: DonationErrorSource): DonationError = BadgeRedemptionError.GenericError(source)
|
||||
|
||||
Reference in New Issue
Block a user