Implement bank transfer pending sheet.

This commit is contained in:
Alex Hart
2023-10-11 08:55:38 -04:00
committed by Cody Henthorne
parent c17d6c2334
commit 0dd17673f5
9 changed files with 61 additions and 49 deletions

View File

@@ -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))
}
}

View File

@@ -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))
}
}
}

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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))
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)