diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowConfirmationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowConfirmationFragment.kt index e7de159e3..0c277d5d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowConfirmationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowConfirmationFragment.kt @@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.keyboard.KeyboardPage import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment +import org.thoughtcrime.securesms.util.Debouncer import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.fragments.requireListener @@ -67,10 +68,12 @@ class GiftFlowConfirmationFragment : private val lifecycleDisposable = LifecycleDisposable() private var errorDialog: DialogInterface? = null private lateinit var processingDonationPaymentDialog: AlertDialog + private lateinit var verifyingRecipientDonationPaymentDialog: AlertDialog private lateinit var donationPaymentComponent: DonationPaymentComponent private lateinit var textInputViewHolder: TextInput.MultilineViewHolder private val eventPublisher = PublishSubject.create() + private val debouncer = Debouncer(100L) override fun bindAdapter(adapter: DSLSettingsAdapter) { RecipientPreference.register(adapter) @@ -85,6 +88,11 @@ class GiftFlowConfirmationFragment : .setCancelable(false) .create() + verifyingRecipientDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext()) + .setView(R.layout.verifying_recipient_payment_dialog) + .setCancelable(false) + .create() + inputAwareLayout = requireView().findViewById(R.id.input_aware_layout) emojiKeyboard = requireView().findViewById(R.id.emoji_drawer) @@ -122,10 +130,17 @@ class GiftFlowConfirmationFragment : lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state -> adapter.submitList(getConfiguration(state).toMappingModelList()) + if (state.stage == GiftFlowState.Stage.RECIPIENT_VERIFICATION) { + debouncer.publish { verifyingRecipientDonationPaymentDialog.show() } + } else { + debouncer.clear() + verifyingRecipientDonationPaymentDialog.dismiss() + } + if (state.stage == GiftFlowState.Stage.PAYMENT_PIPELINE) { processingDonationPaymentDialog.show() } else { - processingDonationPaymentDialog.hide() + processingDonationPaymentDialog.dismiss() } textInputViewHolder.bind( @@ -175,6 +190,8 @@ class GiftFlowConfirmationFragment : super.onDestroyView() textInputViewHolder.onDetachedFromWindow() processingDonationPaymentDialog.dismiss() + debouncer.clear() + verifyingRecipientDonationPaymentDialog.dismiss() } private fun getConfiguration(giftFlowState: GiftFlowState): DSLConfiguration { diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowState.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowState.kt index 25ac3227a..b101e40b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowState.kt @@ -20,8 +20,9 @@ data class GiftFlowState( enum class Stage { INIT, READY, + RECIPIENT_VERIFICATION, TOKEN_REQUEST, PAYMENT_PIPELINE, - FAILURE + FAILURE; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowViewModel.kt index e09b744d0..8781bf51d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowViewModel.kt @@ -4,6 +4,7 @@ import android.content.Intent import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.google.android.gms.wallet.PaymentData +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.CompositeDisposable @@ -128,9 +129,20 @@ class GiftFlowViewModel( fun requestTokenFromGooglePay(label: String) { val giftLevel = store.state.giftLevel ?: return val giftPrice = store.state.giftPrices[store.state.currency] ?: return + val giftRecipient = store.state.recipient?.id ?: return this.giftToPurchase = Gift(giftLevel, giftPrice) - donationPaymentRepository.requestTokenFromGooglePay(giftToPurchase!!.price, label, Gifts.GOOGLE_PAY_REQUEST_CODE) + + store.update { it.copy(stage = GiftFlowState.Stage.RECIPIENT_VERIFICATION) } + disposables += donationPaymentRepository.verifyRecipientIsAllowedToReceiveAGift(giftRecipient) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeBy( + onComplete = { + store.update { it.copy(stage = GiftFlowState.Stage.TOKEN_REQUEST) } + donationPaymentRepository.requestTokenFromGooglePay(giftToPurchase!!.price, label, Gifts.GOOGLE_PAY_REQUEST_CODE) + }, + onError = this::onPaymentFlowError + ) } fun onActivityResult( @@ -153,16 +165,7 @@ class GiftFlowViewModel( store.update { it.copy(stage = GiftFlowState.Stage.PAYMENT_PIPELINE) } donationPaymentRepository.continuePayment(gift.price, paymentData, recipient, store.state.additionalMessage?.toString(), gift.level).subscribeBy( - onError = { throwable -> - store.update { it.copy(stage = GiftFlowState.Stage.READY) } - val donationError: DonationError = if (throwable is DonationError) { - throwable - } else { - Log.w(TAG, "Failed to complete payment or redemption", throwable, true) - DonationError.genericBadgeRedemptionFailure(DonationErrorSource.GIFT) - } - DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError) - }, + onError = this@GiftFlowViewModel::onPaymentFlowError, onComplete = { store.update { it.copy(stage = GiftFlowState.Stage.READY) } eventPublisher.onNext(DonationEvent.PaymentConfirmationSuccess(store.state.giftBadge!!)) @@ -185,6 +188,17 @@ class GiftFlowViewModel( ) } + private fun onPaymentFlowError(throwable: Throwable) { + store.update { it.copy(stage = GiftFlowState.Stage.READY) } + val donationError: DonationError = if (throwable is DonationError) { + throwable + } else { + Log.w(TAG, "Failed to complete payment or redemption", throwable, true) + DonationError.genericBadgeRedemptionFailure(DonationErrorSource.GIFT) + } + DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError) + } + private fun getLoadState( oldState: GiftFlowState, giftPrices: Map? = null, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt index 7c10056df..5a8e192f5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt @@ -60,7 +60,6 @@ import java.util.concurrent.TimeUnit */ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, StripeApi.SetupIntentHelper { - private val application = activity.application private val googlePayApi = GooglePayApi(activity, StripeApi.Gateway(Environment.Donations.STRIPE_CONFIGURATION), Environment.Donations.GOOGLE_PAY_CONFIGURATION) private val stripeApi = StripeApi(Environment.Donations.STRIPE_CONFIGURATION, this, this, ApplicationDependencies.getOkHttpClient()) @@ -92,19 +91,16 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet } /** - * @param price The amount to charce the local user - * @param paymentData PaymentData from Google Pay that describes the payment method - * @param badgeRecipient Who will be getting the badge - * @param additionalMessage An additional message to send along with the badge (only used if badge recipient is not self) + * Verifies that the given recipient is a supported target for a gift. */ - fun continuePayment(price: FiatMoney, paymentData: PaymentData, badgeRecipient: RecipientId, additionalMessage: String?, badgeLevel: Long): Completable { - val verifyRecipient = Completable.fromAction { + fun verifyRecipientIsAllowedToReceiveAGift(badgeRecipient: RecipientId): Completable { + return Completable.fromAction { Log.d(TAG, "Verifying badge recipient $badgeRecipient", true) val recipient = Recipient.resolved(badgeRecipient) if (recipient.isSelf) { - Log.d(TAG, "Badge recipient is self, so this is a boost. Skipping verification.", true) - return@fromAction + Log.d(TAG, "Cannot send a gift to self.", true) + throw DonationError.GiftRecipientVerificationError.SelectedRecipientDoesNotSupportGifts } if (recipient.isGroup || recipient.isDistributionList || recipient.registered != RecipientDatabase.RegisteredState.REGISTERED) { @@ -124,11 +120,19 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet Log.w(TAG, "Failed to retrieve profile for recipient.", e, true) throw DonationError.GiftRecipientVerificationError.FailedToFetchProfile(e) } - } + }.subscribeOn(Schedulers.io()) + } - return verifyRecipient.doOnComplete { - Log.d(TAG, "Creating payment intent for $price...", true) - }.andThen(stripeApi.createPaymentIntent(price, badgeLevel)) + /** + * @param price The amount to charce the local user + * @param paymentData PaymentData from Google Pay that describes the payment method + * @param badgeRecipient Who will be getting the badge + * @param additionalMessage An additional message to send along with the badge (only used if badge recipient is not self) + */ + fun continuePayment(price: FiatMoney, paymentData: PaymentData, badgeRecipient: RecipientId, additionalMessage: String?, badgeLevel: Long): Completable { + Log.d(TAG, "Creating payment intent for $price...", true) + + return stripeApi.createPaymentIntent(price, badgeLevel) .onErrorResumeNext { if (it is DonationError) { Single.error(it) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorParams.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorParams.kt index 5e197dea0..40141d0b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorParams.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorParams.kt @@ -71,12 +71,20 @@ class DonationErrorParams private constructor( } private fun getVerificationErrorParams(context: Context, verificationError: DonationError.GiftRecipientVerificationError, callback: Callback): DonationErrorParams { - return DonationErrorParams( - title = R.string.DonationsErrors__recipient_verification_failed, - message = R.string.DonationsErrors__target_does_not_support_gifting, - positiveAction = callback.onContactSupport(context), - negativeAction = null - ) + return when (verificationError) { + is DonationError.GiftRecipientVerificationError.FailedToFetchProfile -> DonationErrorParams( + title = R.string.DonationsErrors__could_not_verify_recipient, + message = R.string.DonationsErrors__please_check_your_network_connection, + positiveAction = callback.onOk(context), + negativeAction = null + ) + else -> DonationErrorParams( + title = R.string.DonationsErrors__recipient_verification_failed, + message = R.string.DonationsErrors__target_does_not_support_gifting, + positiveAction = callback.onOk(context), + negativeAction = null + ) + } } private fun getDeclinedErrorParams(context: Context, declinedError: DonationError.PaymentSetupError.DeclinedError, callback: Callback): DonationErrorParams { diff --git a/app/src/main/res/layout/verifying_recipient_payment_dialog.xml b/app/src/main/res/layout/verifying_recipient_payment_dialog.xml new file mode 100644 index 000000000..131cca3cc --- /dev/null +++ b/app/src/main/res/layout/verifying_recipient_payment_dialog.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7c3889d09..273785340 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4234,8 +4234,14 @@ Your device doesn\'t support Google Pay, so you can\'t subscribe to earn a badge. You can still support Signal by making a donation on our website. Network error. Check your connection and try again. Retry + Recipient verification failed. + Target does not support gifting. + + Could not verify recipient. + + Please check your network connection and try again. Gift badge @@ -4746,6 +4752,8 @@ One-time donation Add a message + + Verifying recipient… %1$s sent you a gift